@wenathlan/devthink 2.0.12 → 2.0.14

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 (444) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/accounts.ts +2087 -0
  3. package/agent.ts +2331 -0
  4. package/antigravity.ts +2139 -0
  5. package/apifreeze.ts +4146 -0
  6. package/auth.ts +642 -0
  7. package/background.ts +16837 -0
  8. package/bridge.ts +2080 -0
  9. package/build.ts +54 -0
  10. package/bun.ts +26 -0
  11. package/capture.ts +1614 -0
  12. package/checksecrets.ts +72 -0
  13. package/cjs.ts +9 -0
  14. package/cli.ts +3500 -0
  15. package/commands.ts +1101 -0
  16. package/companion.ts +450 -0
  17. package/compatibility.ts +37 -0
  18. package/config.ts +3137 -0
  19. package/constants.ts +943 -0
  20. package/core.ts +1473 -0
  21. package/crossbrowser.ts +1359 -0
  22. package/dashdone.ts +605 -0
  23. package/data.ts +943 -0
  24. package/database.ts +133 -0
  25. package/debug.ts +850 -0
  26. package/deno.ts +27 -0
  27. package/devthink.ts +707 -0
  28. package/dist/accounts.d.ts +409 -0
  29. package/dist/agent.d.ts +837 -0
  30. package/dist/antigravity.d.ts +380 -0
  31. package/dist/apifreeze.d.ts +113 -0
  32. package/dist/auth.d.ts +224 -0
  33. package/{bridge.d.ts → dist/bridge.d.ts} +1 -1
  34. package/dist/capture.d.ts +595 -0
  35. package/dist/cjs.d.ts +6 -0
  36. package/{cli.js → dist/cli.js} +2 -2
  37. package/dist/commands.d.ts +310 -0
  38. package/dist/compatibility.d.ts +17 -0
  39. package/dist/config.d.ts +1853 -0
  40. package/dist/constants.d.ts +637 -0
  41. package/dist/core.d.ts +361 -0
  42. package/dist/data.d.ts +330 -0
  43. package/dist/database.d.ts +24 -0
  44. package/dist/debug.d.ts +270 -0
  45. package/dist/engine.d.ts +74 -0
  46. package/dist/environments.d.ts +244 -0
  47. package/dist/evidence.d.ts +277 -0
  48. package/dist/export.d.ts +311 -0
  49. package/dist/fingerprint.d.ts +594 -0
  50. package/dist/flow.d.ts +300 -0
  51. package/dist/gates.d.ts +103 -0
  52. package/dist/identity.d.ts +51 -0
  53. package/dist/ids.d.ts +7 -0
  54. package/dist/llm.d.ts +652 -0
  55. package/dist/models.d.ts +1807 -0
  56. package/dist/net.d.ts +325 -0
  57. package/dist/neutral.d.ts +41 -0
  58. package/dist/oauth.d.ts +703 -0
  59. package/dist/page.d.ts +1166 -0
  60. package/dist/perf.d.ts +481 -0
  61. package/dist/plan.d.ts +134 -0
  62. package/dist/project.d.ts +395 -0
  63. package/dist/providers.d.ts +37 -0
  64. package/dist/quota.d.ts +453 -0
  65. package/dist/recovery.d.ts +621 -0
  66. package/dist/request.d.ts +1492 -0
  67. package/dist/run.d.ts +779 -0
  68. package/dist/runtime.d.ts +212 -0
  69. package/dist/search.d.ts +375 -0
  70. package/dist/security.d.ts +534 -0
  71. package/dist/serve.d.ts +136 -0
  72. package/dist/session.d.ts +296 -0
  73. package/dist/storage.d.ts +53 -0
  74. package/dist/streaming.d.ts +1119 -0
  75. package/dist/swarm.d.ts +328 -0
  76. package/dist/tools.d.ts +170 -0
  77. package/dist/types.d.ts +8357 -0
  78. package/dist/utils.d.ts +82 -0
  79. package/dist/version.d.ts +7 -0
  80. package/dist/versionregistry.d.ts +406 -0
  81. package/dist/views.d.ts +579 -0
  82. package/dist/workbenchsession.d.ts +69 -0
  83. package/dist/workflow.d.ts +649 -0
  84. package/docs/readme.md +92 -0
  85. package/docs/schemas/antigravity.json +192 -0
  86. package/engine.ts +1524 -0
  87. package/environments.ts +1005 -0
  88. package/evidence.ts +643 -0
  89. package/export.ts +889 -0
  90. package/fingerprint.ts +1458 -0
  91. package/flow.ts +1190 -0
  92. package/gates.ts +307 -0
  93. package/gateway.ts +34 -0
  94. package/hardening.ts +191 -0
  95. package/headless.ts +369 -0
  96. package/http.ts +2110 -0
  97. package/identity.ts +246 -0
  98. package/ids.ts +26 -0
  99. package/index.ts +369 -0
  100. package/llm.ts +2682 -0
  101. package/mcp.ts +1835 -0
  102. package/{memory.js → memory.ts} +3834 -2310
  103. package/models.ts +3805 -0
  104. package/modes.ts +43 -0
  105. package/net.ts +1214 -0
  106. package/neutral.ts +362 -0
  107. package/node.ts +26 -0
  108. package/oauth.ts +2216 -0
  109. package/offscreen.ts +170 -0
  110. package/pack.ts +956 -0
  111. package/package.json +175 -82
  112. package/page.ts +7824 -0
  113. package/pagebridge.ts +1116 -0
  114. package/perf.ts +957 -0
  115. package/plan.ts +672 -0
  116. package/plugin.ts +76 -0
  117. package/{policy.js → policy.ts} +5073 -4679
  118. package/prisma.config.ts +27 -0
  119. package/progress.ts +1308 -0
  120. package/project.ts +1291 -0
  121. package/protocol.ts +4187 -0
  122. package/providers.ts +246 -0
  123. package/quota.ts +1887 -0
  124. package/recovery.ts +2433 -0
  125. package/request.ts +4482 -0
  126. package/run.ts +2135 -0
  127. package/runtime.ts +526 -0
  128. package/search.ts +1760 -0
  129. package/security.ts +1673 -0
  130. package/serve.ts +419 -0
  131. package/server.ts +5538 -0
  132. package/session.ts +1162 -0
  133. package/storage.ts +182 -0
  134. package/streaming.ts +4101 -0
  135. package/swarm.ts +884 -0
  136. package/sync.ts +56 -0
  137. package/tests/agentcert.test.ts +128 -0
  138. package/tests/agentfleet.test.ts +214 -0
  139. package/tests/agentreview.test.ts +241 -0
  140. package/tests/agentstream.test.ts +335 -0
  141. package/tests/agentwork.test.ts +356 -0
  142. package/tests/apifreeze.test.ts +290 -0
  143. package/tests/apimap.test.ts +112 -0
  144. package/tests/approvalgate.test.ts +175 -0
  145. package/tests/artifactmanifest.test.ts +204 -0
  146. package/tests/attentionfeed.test.ts +163 -0
  147. package/tests/backgroundruns.test.ts +100 -0
  148. package/tests/batchscheduling.test.ts +131 -0
  149. package/tests/blackboard.test.ts +238 -0
  150. package/tests/bridge.test.ts +1088 -0
  151. package/tests/browsercoverage.test.ts +134 -0
  152. package/tests/browserpolyfills.test.ts +124 -0
  153. package/tests/build.mjs +1260 -0
  154. package/tests/clientauth.test.ts +223 -0
  155. package/tests/clitools.test.ts +578 -0
  156. package/tests/confirmgates.test.ts +190 -0
  157. package/tests/containerpack.test.ts +143 -0
  158. package/tests/controlflow.test.ts +2120 -0
  159. package/tests/coordination.test.ts +345 -0
  160. package/tests/copyscreen.test.ts +139 -0
  161. package/tests/core.test.ts +417 -0
  162. package/tests/costcert.test.ts +95 -0
  163. package/tests/crossbrowser.test.ts +115 -0
  164. package/tests/cspaudit.test.ts +79 -0
  165. package/tests/dashdone.test.ts +615 -0
  166. package/tests/datagrid.test.ts +137 -0
  167. package/tests/doccheck.test.ts +107 -0
  168. package/tests/dombatch.test.ts +109 -0
  169. package/tests/emulation.test.ts +282 -0
  170. package/tests/environments.test.ts +259 -0
  171. package/tests/evidenceviews.test.ts +88 -0
  172. package/tests/exporttools.test.ts +185 -0
  173. package/tests/firefoxprep.test.ts +134 -0
  174. package/tests/flowlibrary.test.ts +242 -0
  175. package/tests/flowrun.test.ts +303 -0
  176. package/tests/forensics.test.ts +439 -0
  177. package/tests/gateway.test.ts +939 -0
  178. package/tests/headlesslib.test.ts +171 -0
  179. package/tests/httpstream.test.ts +277 -0
  180. package/tests/immutablelog.test.ts +157 -0
  181. package/tests/inboundguard.test.ts +177 -0
  182. package/tests/lazyload.test.ts +72 -0
  183. package/tests/librarymodes.test.ts +623 -0
  184. package/tests/llm.test.ts +696 -0
  185. package/tests/manifest.test.ts +226 -0
  186. package/tests/maskinputs.test.ts +150 -0
  187. package/tests/matrixverify.test.ts +111 -0
  188. package/tests/mavenpack.test.ts +75 -0
  189. package/tests/mcpserver.test.ts +704 -0
  190. package/tests/memory.test.ts +4155 -0
  191. package/tests/memorycare.test.ts +230 -0
  192. package/tests/migrateplan.test.ts +349 -0
  193. package/tests/minimization.test.ts +414 -0
  194. package/tests/modelroute.test.ts +112 -0
  195. package/tests/multiagent.test.ts +440 -0
  196. package/tests/nativebridge.test.ts +807 -0
  197. package/tests/navigation.test.ts +284 -0
  198. package/tests/nugetpack.test.ts +83 -0
  199. package/tests/orchestration.test.ts +449 -0
  200. package/tests/originpolicy.test.ts +405 -0
  201. package/tests/outputcompare.test.ts +97 -0
  202. package/tests/package.test.ts +91 -0
  203. package/tests/pagebridge.test.ts +5909 -0
  204. package/tests/pentest.test.ts +75 -0
  205. package/tests/perfrecords.test.ts +257 -0
  206. package/tests/permdiff.test.ts +85 -0
  207. package/tests/phishguard.test.ts +128 -0
  208. package/tests/pickerviews.test.ts +155 -0
  209. package/tests/pipelines.test.ts +381 -0
  210. package/tests/planlint.test.ts +240 -0
  211. package/tests/planreview.test.ts +383 -0
  212. package/tests/platformtargets.test.ts +56 -0
  213. package/tests/policy.test.ts +8284 -0
  214. package/tests/portability.test.ts +96 -0
  215. package/tests/progress.test.ts +1036 -0
  216. package/tests/promptlibrary.test.ts +116 -0
  217. package/tests/protocol.test.ts +4793 -0
  218. package/tests/publishingpipeline.test.ts +314 -0
  219. package/tests/quickactions.test.ts +164 -0
  220. package/tests/readiness.test.ts +78 -0
  221. package/tests/recipes.test.ts +163 -0
  222. package/tests/redactshots.test.ts +216 -0
  223. package/tests/registernvidiakeys.ts +51 -0
  224. package/tests/relayserve.test.ts +304 -0
  225. package/tests/releasenotes.test.ts +276 -0
  226. package/tests/resilience.test.ts +467 -0
  227. package/tests/resourceaware.test.ts +187 -0
  228. package/tests/runbudget.test.ts +125 -0
  229. package/tests/runhistory.test.ts +299 -0
  230. package/tests/runreplay.test.ts +113 -0
  231. package/tests/runresume.test.ts +184 -0
  232. package/tests/runstate.test.ts +288 -0
  233. package/tests/runtimeadapters.test.ts +251 -0
  234. package/tests/safariskeleton.test.ts +115 -0
  235. package/tests/sandboxframe.test.ts +196 -0
  236. package/tests/sbom.test.ts +105 -0
  237. package/tests/secretvault.test.ts +188 -0
  238. package/tests/selcache.test.ts +111 -0
  239. package/tests/sessioninterface.test.ts +791 -0
  240. package/tests/sessions.test.ts +591 -0
  241. package/tests/siteprefs.test.ts +228 -0
  242. package/tests/snapshotdelta.test.ts +165 -0
  243. package/tests/statusviews.test.ts +237 -0
  244. package/tests/stepmeter.test.ts +200 -0
  245. package/tests/streamparse.test.ts +163 -0
  246. package/tests/stylepruning.test.ts +90 -0
  247. package/tests/surfaces.test.ts +282 -0
  248. package/tests/sweep.test.ts +78 -0
  249. package/tests/syncbridge.test.ts +164 -0
  250. package/tests/taskqueue.test.ts +206 -0
  251. package/tests/toolcalls.test.ts +336 -0
  252. package/tests/toolcatalog.test.ts +201 -0
  253. package/tests/torture-agent.test.ts +706 -0
  254. package/tests/torture-auth.test.ts +461 -0
  255. package/tests/torture-commands.test.ts +895 -0
  256. package/tests/torture-data.test.ts +923 -0
  257. package/tests/torture-environments.test.ts +285 -0
  258. package/tests/torture-export.test.ts +263 -0
  259. package/tests/torture-http.test.ts +1379 -0
  260. package/tests/torture-llm.test.ts +808 -0
  261. package/tests/torture-memory.test.ts +1084 -0
  262. package/tests/torture-plan.test.ts +829 -0
  263. package/tests/torture-run.test.ts +626 -0
  264. package/tests/torture-session.test.ts +928 -0
  265. package/tests/torture-swarm.test.ts +1245 -0
  266. package/tests/torture-tools.test.ts +964 -0
  267. package/tests/torture-workflow.test.ts +2354 -0
  268. package/tests/tourviews.test.ts +52 -0
  269. package/tests/transparency.test.ts +147 -0
  270. package/tests/trigger.test.ts +941 -0
  271. package/tests/v1sunset.test.ts +428 -0
  272. package/tests/virtlist.test.ts +94 -0
  273. package/tests/vision.test.ts +540 -0
  274. package/tests/vsixpack.test.ts +145 -0
  275. package/tests/webapi.test.ts +523 -0
  276. package/tests/workflow.test.ts +1785 -0
  277. package/tests/workfloweditor.test.ts +759 -0
  278. package/tests/xpipack.test.ts +128 -0
  279. package/tools.ts +734 -0
  280. package/types.ts +9908 -0
  281. package/umd.ts +9 -0
  282. package/utils.ts +249 -0
  283. package/version.ts +8 -0
  284. package/versionregistry.ts +1310 -0
  285. package/views.ts +2153 -0
  286. package/vitest.config.ts +12 -0
  287. package/web/App.tsx +78 -0
  288. package/web/README.md +47 -0
  289. package/web/capacitor.config.ts +46 -0
  290. package/web/const.ts +19 -0
  291. package/web/control.shell.tsx +66 -0
  292. package/web/dashboardpage.ts +554 -0
  293. package/web/db.ts +493 -0
  294. package/web/design.html +541 -0
  295. package/web/gatewayview/Gateway.tsx +345 -0
  296. package/web/gatewayview/config.ts +1042 -0
  297. package/web/gatewayview/definition.ts +174 -0
  298. package/web/icons.ts +15 -0
  299. package/{manifest.json → web/manifest.json} +1 -1
  300. package/web/optionspage.ts +691 -0
  301. package/web/popup.ts +1651 -0
  302. package/web/schema.prisma +176 -0
  303. package/web/sidepanel.ts +7941 -0
  304. package/web/sitemanifest.json +13 -0
  305. package/web/terminal-ui.tsx +622 -0
  306. package/web/transparencypage.ts +541 -0
  307. package/web/vite.config.ts +39 -0
  308. package/web/workspace.ts +23 -0
  309. package/workbenchmemory.ts +104 -0
  310. package/workbenchsession.ts +304 -0
  311. package/workflow.ts +4495 -0
  312. package/bridge.d.ts.map +0 -1
  313. package/bridge.js +0 -1877
  314. package/bun.d.ts.map +0 -1
  315. package/bun.js +0 -52329
  316. package/caps/background.json +0 -712
  317. package/caps/cli.json +0 -360
  318. package/caps/library.json +0 -3611
  319. package/caps/mcp.json +0 -126
  320. package/caps/pagebridge.json +0 -377
  321. package/caps/popup.json +0 -51
  322. package/caps/sidepanel.json +0 -522
  323. package/checksums.txt +0 -142
  324. package/cli.d.ts.map +0 -1
  325. package/companion.d.ts.map +0 -1
  326. package/companion.js +0 -389
  327. package/crossbrowser.d.ts.map +0 -1
  328. package/crossbrowser.js +0 -1191
  329. package/dashdone.d.ts.map +0 -1
  330. package/dashdone.js +0 -368
  331. package/deno.d.ts.map +0 -1
  332. package/deno.js +0 -52325
  333. package/devthink.umd.js +0 -52346
  334. package/fixtures/bun.ts +0 -7
  335. package/fixtures/cjs.js +0 -6
  336. package/fixtures/deno.ts +0 -6
  337. package/fixtures/example-org-pagestate.json +0 -22
  338. package/fixtures/importers/automa-workflow.json +0 -18
  339. package/fixtures/importers/selenium-side.json +0 -20
  340. package/fixtures/importers/tabular-plan.csv +0 -7
  341. package/fixtures/importers/uivision-macro.json +0 -10
  342. package/fixtures/importers/v1-plan.json +0 -13
  343. package/fixtures/mcp-client.mjs +0 -14
  344. package/fixtures/pages/checkout.html +0 -192
  345. package/fixtures/pages/dashboard.html +0 -110
  346. package/fixtures/pages/feed.html +0 -113
  347. package/fixtures/pages/product-grid.html +0 -150
  348. package/fixtures/plans/form-inventory-plan.json +0 -11
  349. package/fixtures/plans/release-notes-plan.json +0 -11
  350. package/fixtures/recipes/agentscompete.json +0 -11
  351. package/fixtures/recipes/agentsforms.json +0 -11
  352. package/fixtures/recipes/agentsmonitor.json +0 -11
  353. package/fixtures/recipes/agentsreview.json +0 -29
  354. package/fixtures/recipes/agentswarm.json +0 -11
  355. package/fixtures/recipes/fillcalendar.json +0 -12
  356. package/fixtures/recipes/fillcheckout.json +0 -13
  357. package/fixtures/recipes/filldependentdropdowns.json +0 -12
  358. package/fixtures/recipes/fillfromcsv.json +0 -13
  359. package/fixtures/recipes/fillloginform.json +0 -12
  360. package/fixtures/recipes/fillmultistepwizard.json +0 -13
  361. package/fixtures/recipes/fillregistration.json +0 -12
  362. package/fixtures/recipes/fillsearchfilters.json +0 -12
  363. package/fixtures/recipes/filltypeahead.json +0 -11
  364. package/fixtures/recipes/monitoralerts.json +0 -12
  365. package/fixtures/recipes/monitoravailability.json +0 -11
  366. package/fixtures/recipes/monitornetworkquiet.json +0 -12
  367. package/fixtures/recipes/monitorpagechanges.json +0 -11
  368. package/fixtures/recipes/monitorpricechange.json +0 -11
  369. package/fixtures/recipes/monitorschedule.json +0 -12
  370. package/fixtures/recipes/scrapedynamicprices.json +0 -13
  371. package/fixtures/recipes/scrapeimagegallery.json +0 -12
  372. package/fixtures/recipes/scrapeinfinitefeed.json +0 -13
  373. package/fixtures/recipes/scrapenestedlists.json +0 -12
  374. package/fixtures/recipes/scrapeproductgrid.json +0 -12
  375. package/fixtures/recipes/scrapereviews.json +0 -12
  376. package/fixtures/recipes/scrapesearchresults.json +0 -12
  377. package/fixtures/recipes/scrapetablepagination.json +0 -12
  378. package/fixtures/recipes/testaccessibility.json +0 -34
  379. package/fixtures/recipes/testconsoleerrors.json +0 -12
  380. package/fixtures/recipes/testdeeplinks.json +0 -12
  381. package/fixtures/recipes/testformvalidation.json +0 -13
  382. package/fixtures/recipes/testlayoutshift.json +0 -12
  383. package/fixtures/recipes/testlinkcheck.json +0 -12
  384. package/fixtures/recipes/testloadtiming.json +0 -12
  385. package/fixtures/recipes/testvisualdiff.json +0 -12
  386. package/fixtures/workflows/changelog-digest-workflow.json +0 -13
  387. package/fixtures/workflows/form-survey-workflow.json +0 -13
  388. package/gallery.json +0 -706
  389. package/hardening.d.ts.map +0 -1
  390. package/hardening.js +0 -188
  391. package/headless.d.ts.map +0 -1
  392. package/headless.js +0 -11522
  393. package/http.d.ts.map +0 -1
  394. package/http.js +0 -2165
  395. package/index.cjs +0 -54682
  396. package/index.d.ts.map +0 -1
  397. package/index.js +0 -52310
  398. package/index.neutral.js +0 -46343
  399. package/mcp.d.ts.map +0 -1
  400. package/mcp.js +0 -20515
  401. package/memory.d.ts.map +0 -1
  402. package/nativehost.template.json +0 -10
  403. package/node.cjs +0 -54686
  404. package/node.d.ts.map +0 -1
  405. package/pack.d.ts.map +0 -1
  406. package/pack.js +0 -805
  407. package/policy.d.ts.map +0 -1
  408. package/progress.d.ts.map +0 -1
  409. package/progress.js +0 -645
  410. package/protocol.d.ts.map +0 -1
  411. package/protocol.js +0 -8334
  412. package/schemas/audit.schema.json +0 -226
  413. package/schemas/capability.schema.json +0 -113
  414. package/schemas/envelope.schema.json +0 -162
  415. package/schemas/memory.schema.json +0 -51
  416. package/schemas/observation.schema.json +0 -113
  417. package/schemas/plan.schema.json +0 -208
  418. package/schemas/progress.schema.json +0 -126
  419. package/schemas/proposal.schema.json +0 -68
  420. package/schemas/session.schema.json +0 -82
  421. package/schemas/tool.schema.json +0 -94
  422. package/server.d.ts.map +0 -1
  423. package/server.js +0 -28877
  424. package/umd-example.html +0 -18
  425. package/umd.d.ts.map +0 -1
  426. /package/{bun.d.ts → dist/bun.d.ts} +0 -0
  427. /package/{cli.d.ts → dist/cli.d.ts} +0 -0
  428. /package/{companion.d.ts → dist/companion.d.ts} +0 -0
  429. /package/{crossbrowser.d.ts → dist/crossbrowser.d.ts} +0 -0
  430. /package/{dashdone.d.ts → dist/dashdone.d.ts} +0 -0
  431. /package/{deno.d.ts → dist/deno.d.ts} +0 -0
  432. /package/{hardening.d.ts → dist/hardening.d.ts} +0 -0
  433. /package/{headless.d.ts → dist/headless.d.ts} +0 -0
  434. /package/{http.d.ts → dist/http.d.ts} +0 -0
  435. /package/{index.d.ts → dist/index.d.ts} +0 -0
  436. /package/{mcp.d.ts → dist/mcp.d.ts} +0 -0
  437. /package/{memory.d.ts → dist/memory.d.ts} +0 -0
  438. /package/{node.d.ts → dist/node.d.ts} +0 -0
  439. /package/{pack.d.ts → dist/pack.d.ts} +0 -0
  440. /package/{policy.d.ts → dist/policy.d.ts} +0 -0
  441. /package/{progress.d.ts → dist/progress.d.ts} +0 -0
  442. /package/{protocol.d.ts → dist/protocol.d.ts} +0 -0
  443. /package/{server.d.ts → dist/server.d.ts} +0 -0
  444. /package/{umd.d.ts → dist/umd.d.ts} +0 -0
package/recovery.ts ADDED
@@ -0,0 +1,2433 @@
1
+ /**
2
+ * @fileoverview recovery.ts - ONDA 4 - Plugin Core (merged)
3
+ * @module auth/recovery
4
+ * @description Complete production-ready auto-recovery engine for the
5
+ * provider plugin with Gemini CLI bypass.
6
+ *
7
+ * Responsibilities:
8
+ * - Internal LRU cache for thinking signatures (avoids revalidation
9
+ * and preserves integrity)
10
+ * - Error detectors: invalid thinking block signature, tool_use
11
+ * without thinking, session boundary, tool failures
12
+ * - Recovery strategies: continue, undo guidance, keep_thinking
13
+ * preservation and injection of preserved thinking across turns
14
+ * - Detection utilities, recovery plan building, transform helpers
15
+ * - withAutoRecovery wrapper with exponential backoff + jitter
16
+ * - Simple context-based API ported from the v4/v5 lineage:
17
+ * RecoveryContext, getRecoveryStrategy, module-level signature
18
+ * preservation cache, buildRecoveryMessage,
19
+ * transformMessagesForRecovery, recoverToolResultMissing /
20
+ * recoverThinkingOrder / recoverThinkingDisabled,
21
+ * contextErrorRecovery, sessionRecovery, toast integration
22
+ *
23
+ * Zero external dependencies: only node:* builtins + global fetch.
24
+ * Compatible with Models 2026 and the official Gemini CLI spoof.
25
+ *
26
+ * @author ONDA 4 - Plugin Core
27
+ * @license MIT
28
+ * @since 2026
29
+ */
30
+
31
+ import * as crypto from "node:crypto";
32
+
33
+ // ============================================================================
34
+ // 00. CANONICAL BYPASS CONSTANTS (identical values are imported from the
35
+ // frozen owner constants.ts; genuinely divergent local variants are kept
36
+ // below with an explicit marker)
37
+ // ============================================================================
38
+
39
+ import {
40
+ CLIENT_METADATA_STRING,
41
+ GEMINI_CLI_OAUTH_CLIENT_ID as GEMINI_CLI_CLIENT_ID,
42
+ GEMINI_CLI_OAUTH_CLIENT_SECRET as GEMINI_CLI_CLIENT_SECRET,
43
+ GEMINI_CLI_USER_AGENT_PLAIN as GEMINI_CLI_USER_AGENT,
44
+ JITTER_MAX_MS,
45
+ JITTER_MIN_MS,
46
+ PROJECT_FALLBACK,
47
+ } from "./constants.js";
48
+ export { GEMINI_CLI_CLIENT_ID, GEMINI_CLI_CLIENT_SECRET, PROJECT_FALLBACK };
49
+
50
+ // local variant: diverges from constants (GEMINI_CLI_USER_AGENT_FALLBACK is `gemini-cli/0.57.0 {os}/{arch}`; this is the exact GeminiCLI UA string)
51
+
52
+ // local variant: diverges from constants (constants.X_GOOG_API_CLIENT is the antigravity/{ver} gl-node gax/grpc chain; this is the bare gl-node value)
53
+
54
+ /**
55
+ * Client-Metadata header string for the Gemini CLI bypass. v2.1.16: the local
56
+ * literal was byte-identical to constants.CLIENT_METADATA_STRING
57
+ * ("ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI")
58
+ * — the export now aliases the owner constant so the value has one source.
59
+ */
60
+ export const X_CLIENT_METADATA_RAW = CLIENT_METADATA_STRING;
61
+
62
+ // v2.1.16: dead CODE_ASSIST_BASE ("https://cloudcode-pa.googleapis.com") was
63
+ // deleted — zero consumers repo-wide (not even the RecoveryConstants bundle
64
+ // listed it); the live base-URL owner is constants.CLOUDCODE_PA_BASE.
65
+
66
+ // v2.1.15 Phase A: MODELS_2026 (the 11-id legacy bypass list) and the
67
+ // ModelId2026 union moved to models.ts (MODELS_2026_LEGACY / ModelId2026) —
68
+ // imported under the historical name and re-exported so the public surface
69
+ // is unchanged.
70
+ import { MODELS_2026_LEGACY as MODELS_2026 } from "./models.js";
71
+ import type { ModelId2026 } from "./models.js";
72
+ import { fnv1a32 } from "./core.js";
73
+ import { X_GOOG_API_CLIENT_GEMINI_CLI as X_GOOG_API_CLIENT } from "./fingerprint.js";
74
+ export { MODELS_2026 };
75
+ export type { ModelId2026 };
76
+
77
+ const MODELS_2026_SET = new Set<string>(MODELS_2026 as readonly string[]);
78
+ // v2.1.16 divergence note (kept deliberately, NOT replaced by
79
+ // models.isThinkingModel): compared live, the two classifiers disagree in
80
+ // both directions — isThinkingModel("antigravity-claude-sonnet-4-6") is false
81
+ // (catalog isThinking flag) while this Set includes it, and isThinkingModel
82
+ // is true for substring hits this Set excludes (gemini-3-flash,
83
+ // gemini-2.5-flash, gpt-oss-120b-high, *-thinking variants, ...). Recovery
84
+ // needs the exact 8-id policy view (all ids are members of models.
85
+ // MODELS_2026_LEGACY), so swapping in the heuristic classifier would change
86
+ // live detector behavior at detectToolUseWithoutThinking, buildRecoveryPlan
87
+ // metadata and buildRecoveryMessage.
88
+ const THINKING_MODELS_2026 = new Set<string>([
89
+ "antigravity-claude-sonnet-4-6",
90
+ "antigravity-claude-opus-4-6-thinking",
91
+ "antigravity-gemini-3-pro",
92
+ "antigravity-gemini-3.1-pro",
93
+ "gemini-3-pro-preview",
94
+ "gemini-3.1-pro-preview",
95
+ "gemini-3.1-pro-preview-customtools",
96
+ "gemini-2.5-pro",
97
+ ]);
98
+
99
+ // ============================================================================
100
+ // 01. CORE TYPES - ANTHROPIC / GEMINI / OPENCODE COMPAT
101
+ // ============================================================================
102
+
103
+ export type Role = "user" | "assistant" | "system" | "tool";
104
+
105
+ export interface ThinkingBlock {
106
+ type: "thinking";
107
+ thinking: string;
108
+ signature?: string;
109
+ // some proxies return redacted_thinking
110
+ redacted_thinking?: string;
111
+ // internal metadata
112
+ _preserved?: boolean;
113
+ _injected?: boolean;
114
+ }
115
+
116
+ export interface TextBlock {
117
+ type: "text";
118
+ text: string;
119
+ }
120
+
121
+ export interface ToolUseBlock {
122
+ type: "tool_use";
123
+ id: string;
124
+ name: string;
125
+ input: unknown;
126
+ // flag interna
127
+ _missingThinking?: boolean;
128
+ }
129
+
130
+ export interface ToolResultBlock {
131
+ type: "tool_result";
132
+ tool_use_id: string;
133
+ content: string | Array<{ type: string; text?: string }>;
134
+ is_error?: boolean;
135
+ }
136
+
137
+ export type ContentBlock = ThinkingBlock | TextBlock | ToolUseBlock | ToolResultBlock | Record<string, unknown>;
138
+
139
+ export interface ChatMessage {
140
+ role: Role;
141
+ content: string | ContentBlock[];
142
+ // some SDKs allow a plain array
143
+ _sessionId?: string;
144
+ _timestamp?: number;
145
+ }
146
+
147
+ export type ChatMessages = ChatMessage[];
148
+
149
+ // Error detection result
150
+ export type RecoveryErrorCode =
151
+ | "INVALID_THINKING_SIGNATURE"
152
+ | "TOOL_USE_WITHOUT_THINKING"
153
+ | "SESSION_BOUNDARY"
154
+ | "TOOL_FAILURE"
155
+ | "CONTEXT_OVERFLOW"
156
+ | "RATE_LIMIT"
157
+ | "UNKNOWN";
158
+
159
+ export interface DetectedError {
160
+ code: RecoveryErrorCode;
161
+ message: string;
162
+ details?: Record<string, unknown>;
163
+ recoverable: boolean;
164
+ index?: number; // message index where it occurred
165
+ blockIndex?: number;
166
+ }
167
+
168
+ export interface DetectionReport {
169
+ errors: DetectedError[];
170
+ hasInvalidSignature: boolean;
171
+ hasMissingThinking: boolean;
172
+ hasSessionBoundary: boolean;
173
+ hasToolFailure: boolean;
174
+ shouldRecover: boolean;
175
+ }
176
+
177
+ // Recovery strategies
178
+ export type RecoveryStrategy =
179
+ | "STRIP_INVALID_SIGNATURE"
180
+ | "INJECT_PRESERVED_THINKING"
181
+ | "KEEP_THINKING_PRESERVE"
182
+ | "CONTINUE_RESUME"
183
+ | "UNDO_GUIDANCE"
184
+ | "RETRY_WITH_BACKOFF"
185
+ | "REBUILD_MESSAGES"
186
+ | "RESET_THINKING_CACHE";
187
+
188
+ export interface RecoveryAction {
189
+ strategy: RecoveryStrategy;
190
+ reason: string;
191
+ priority: number; // lower = runs first
192
+ payload?: Record<string, unknown>;
193
+ }
194
+
195
+ export interface RecoveryPlan {
196
+ id: string;
197
+ createdAt: number;
198
+ errors: DetectedError[];
199
+ actions: RecoveryAction[];
200
+ transformedMessages?: ChatMessages;
201
+ keepThinking?: string | null;
202
+ preservedSignature?: string | null;
203
+ shouldRetry: boolean;
204
+ retryDelayMs?: number;
205
+ metadata?: Record<string, unknown>;
206
+ }
207
+
208
+ // Wrapper executor types
209
+ export type ExecutorFn<T = unknown> = (
210
+ messages: ChatMessages,
211
+ opts?: { signal?: AbortSignal | undefined; attempt?: number | undefined; plan?: RecoveryPlan | undefined },
212
+ ) => Promise<T>;
213
+
214
+ export interface AutoRecoveryOptions {
215
+ maxAttempts?: number; // default 4
216
+ baseDelayMs?: number; // default 450
217
+ maxDelayMs?: number; // default 7500
218
+ jitter?: boolean; // default true
219
+ failFastOnNonRecoverable?: boolean; // default true
220
+ modelId?: ModelId2026 | string;
221
+ sessionId?: string;
222
+ preservationStore?: ThinkingPreservationStore;
223
+ signatureCache?: ThinkingSignatureCache;
224
+ onRecoveryAttempt?: (attempt: number, plan: RecoveryPlan, delayMs: number) => void;
225
+ onDetectedErrors?: (report: DetectionReport) => void;
226
+ signal?: AbortSignal;
227
+ logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
228
+ }
229
+
230
+ // ============================================================================
231
+ // 02. GENERIC LRU CACHE - PRODUCTION READY
232
+ // ============================================================================
233
+
234
+ /**
235
+ * Zero-dependency, production-ready generic LRUCache.
236
+ * - O(1) get/set through an ordered Map
237
+ * - Automatic LRU eviction
238
+ * - Stats for observability
239
+ */
240
+ export class LRUCache<K, V> {
241
+ private readonly capacity: number;
242
+ private readonly cache: Map<K, V>;
243
+ private hits = 0;
244
+ private misses = 0;
245
+ private evictions = 0;
246
+
247
+ constructor(capacity: number) {
248
+ if (!Number.isFinite(capacity) || capacity <= 0) {
249
+ throw new Error(`LRUCache capacity must be >0, got ${capacity}`);
250
+ }
251
+ this.capacity = Math.floor(capacity);
252
+ this.cache = new Map<K, V>();
253
+ }
254
+
255
+ get(key: K): V | undefined {
256
+ if (!this.cache.has(key)) {
257
+ this.misses++;
258
+ return undefined;
259
+ }
260
+ const value = this.cache.get(key)!;
261
+ // move to MRU end
262
+ this.cache.delete(key);
263
+ this.cache.set(key, value);
264
+ this.hits++;
265
+ return value;
266
+ }
267
+
268
+ set(key: K, value: V): void {
269
+ if (this.cache.has(key)) {
270
+ this.cache.delete(key);
271
+ } else if (this.cache.size >= this.capacity) {
272
+ const lruKey = this.cache.keys().next().value as K;
273
+ this.cache.delete(lruKey);
274
+ this.evictions++;
275
+ }
276
+ this.cache.set(key, value);
277
+ }
278
+
279
+ has(key: K): boolean {
280
+ return this.cache.has(key);
281
+ }
282
+
283
+ delete(key: K): boolean {
284
+ return this.cache.delete(key);
285
+ }
286
+
287
+ clear(): void {
288
+ this.cache.clear();
289
+ this.hits = 0;
290
+ this.misses = 0;
291
+ this.evictions = 0;
292
+ }
293
+
294
+ get size(): number {
295
+ return this.cache.size;
296
+ }
297
+
298
+ get cap(): number {
299
+ return this.capacity;
300
+ }
301
+
302
+ keys(): IterableIterator<K> {
303
+ return this.cache.keys();
304
+ }
305
+
306
+ values(): IterableIterator<V> {
307
+ return this.cache.values();
308
+ }
309
+
310
+ entries(): IterableIterator<[K, V]> {
311
+ return this.cache.entries();
312
+ }
313
+
314
+ toArray(): Array<[K, V]> {
315
+ return [...this.cache.entries()];
316
+ }
317
+
318
+ // MRU -> LRU
319
+ toMRUArray(): Array<[K, V]> {
320
+ return [...this.cache.entries()].reverse();
321
+ }
322
+
323
+ stats(): { size: number; capacity: number; hits: number; misses: number; evictions: number; hitRate: number } {
324
+ const total = this.hits + this.misses;
325
+ return {
326
+ size: this.cache.size,
327
+ capacity: this.capacity,
328
+ hits: this.hits,
329
+ misses: this.misses,
330
+ evictions: this.evictions,
331
+ hitRate: total === 0 ? 0 : this.hits / total,
332
+ };
333
+ }
334
+ }
335
+
336
+ // ============================================================================
337
+ // 03. THINKING SIGNATURE CACHE - SIGNATURE-SPECIFIC LRU
338
+ // ============================================================================
339
+
340
+ export interface ThinkingSignatureEntry {
341
+ signature: string;
342
+ thinkingHash: string;
343
+ thinkingPreview: string; // first 120 chars
344
+ modelId: string;
345
+ createdAt: number;
346
+ lastUsedAt: number;
347
+ useCount: number;
348
+ valid: boolean;
349
+ length: number;
350
+ }
351
+
352
+ // local variant: diverges from core (hashes UTF-8 bytes via Buffer; core hashes UTF-16 code units — different results for non-ASCII input)
353
+
354
+ export function hashThinkingContent(thinking: string): string {
355
+ // truncated sha256 for low collision chance + fnv as fast fallback
356
+ try {
357
+ const sha = crypto.createHash("sha256").update(thinking, "utf8").digest("hex").slice(0, 16);
358
+ const fnv = fnv1a32(thinking).toString(16).padStart(8, "0");
359
+ return `${sha}_${fnv}`;
360
+ } catch {
361
+ const fnv = fnv1a32(thinking).toString(16).padStart(8, "0");
362
+ return `fnv_${fnv}`;
363
+ }
364
+ }
365
+
366
+ export function isBase64ishSig(sig: string): boolean {
367
+ if (!sig || typeof sig !== "string") return false;
368
+ const trimmed = sig.trim();
369
+ if (trimmed.length < 16) return false; // short signatures are invalid
370
+ if (trimmed.length > 8192) return false; // anomaly
371
+ // base64 + url safe
372
+ const b64re = /^[A-Za-z0-9+/=_-]+$/;
373
+ if (!b64re.test(trimmed)) return false;
374
+ // no strict padding/length heuristic required, but it must partially decode
375
+ try {
376
+ // try decoding: normalize url-safe characters first
377
+ const normalized = trimmed.replace(/-/g, "+").replace(/_/g, "/");
378
+ const buf = Buffer.from(normalized, "base64");
379
+ if (buf.length < 8) return false; // decoded payload too short
380
+ return true;
381
+ } catch {
382
+ return false;
383
+ }
384
+ }
385
+
386
+ export function isValidThinkingSignature(signature: unknown): boolean {
387
+ if (typeof signature !== "string") return false;
388
+ if (signature.trim().length === 0) return false;
389
+ return isBase64ishSig(signature);
390
+ }
391
+
392
+ export class ThinkingSignatureCache {
393
+ private readonly lru: LRUCache<string, ThinkingSignatureEntry>;
394
+ private readonly hashToSig: Map<string, string>; // thinkingHash -> signature
395
+
396
+ constructor(capacity = 256) {
397
+ this.lru = new LRUCache<string, ThinkingSignatureEntry>(capacity);
398
+ this.hashToSig = new Map<string, string>();
399
+ }
400
+
401
+ /**
402
+ * Stores a valid signature associated with thinking content.
403
+ */
404
+ store(thinking: string, signature: string, modelId: string = PROJECT_FALLBACK): ThinkingSignatureEntry | null {
405
+ if (!thinking || !isValidThinkingSignature(signature)) return null;
406
+ const thinkingHash = hashThinkingContent(thinking);
407
+ const now = Date.now();
408
+ const entry: ThinkingSignatureEntry = {
409
+ signature,
410
+ thinkingHash,
411
+ thinkingPreview: thinking.slice(0, 120),
412
+ modelId,
413
+ createdAt: now,
414
+ lastUsedAt: now,
415
+ useCount: 1,
416
+ valid: true,
417
+ length: signature.length,
418
+ };
419
+ this.lru.set(signature, entry);
420
+ this.hashToSig.set(thinkingHash, signature);
421
+ return entry;
422
+ }
423
+
424
+ getBySignature(signature: string): ThinkingSignatureEntry | undefined {
425
+ const entry = this.lru.get(signature);
426
+ if (entry) {
427
+ entry.lastUsedAt = Date.now();
428
+ entry.useCount++;
429
+ }
430
+ return entry;
431
+ }
432
+
433
+ getByThinking(thinking: string): ThinkingSignatureEntry | undefined {
434
+ const h = hashThinkingContent(thinking);
435
+ const sig = this.hashToSig.get(h);
436
+ if (!sig) return undefined;
437
+ return this.getBySignature(sig);
438
+ }
439
+
440
+ getByHash(hash: string): ThinkingSignatureEntry | undefined {
441
+ const sig = this.hashToSig.get(hash);
442
+ if (!sig) return undefined;
443
+ return this.getBySignature(sig);
444
+ }
445
+
446
+ hasValid(thinking: string, signature: string): boolean {
447
+ if (!isValidThinkingSignature(signature)) return false;
448
+ const bySig = this.lru.get(signature);
449
+ if (!bySig) return false;
450
+ return bySig.valid && bySig.thinkingHash === hashThinkingContent(thinking);
451
+ }
452
+
453
+ /**
454
+ * Tries to recover a valid signature for preserved thinking content.
455
+ */
456
+ tryRestore(thinking: string): string | null {
457
+ const entry = this.getByThinking(thinking);
458
+ if (!entry || !entry.valid) return null;
459
+ if (!isValidThinkingSignature(entry.signature)) return null;
460
+ return entry.signature;
461
+ }
462
+
463
+ invalidate(signature: string): void {
464
+ const entry = this.lru.get(signature);
465
+ if (entry) {
466
+ entry.valid = false;
467
+ }
468
+ }
469
+
470
+ sweepInvalid(): number {
471
+ let removed = 0;
472
+ for (const [sig, entry] of this.lru.toArray()) {
473
+ if (!entry.valid || !isValidThinkingSignature(entry.signature)) {
474
+ this.lru.delete(sig);
475
+ this.hashToSig.delete(entry.thinkingHash);
476
+ removed++;
477
+ }
478
+ }
479
+ return removed;
480
+ }
481
+
482
+ size(): number {
483
+ return this.lru.size;
484
+ }
485
+
486
+ stats() {
487
+ return this.lru.stats();
488
+ }
489
+
490
+ clear(): void {
491
+ this.lru.clear();
492
+ this.hashToSig.clear();
493
+ }
494
+
495
+ recentValidSignatures(limit = 5): ThinkingSignatureEntry[] {
496
+ return this.lru
497
+ .toMRUArray()
498
+ .map(([, v]) => v)
499
+ .filter((e) => e.valid)
500
+ .slice(0, limit);
501
+ }
502
+ }
503
+
504
+ // Global singleton (resettable in tests)
505
+ let globalSignatureCache: ThinkingSignatureCache | null = null;
506
+
507
+ export function getGlobalSignatureCache(): ThinkingSignatureCache {
508
+ if (!globalSignatureCache) {
509
+ globalSignatureCache = new ThinkingSignatureCache(256);
510
+ }
511
+ return globalSignatureCache;
512
+ }
513
+
514
+ export function resetGlobalSignatureCache(): void {
515
+ globalSignatureCache?.clear();
516
+ globalSignatureCache = null;
517
+ }
518
+
519
+ // ============================================================================
520
+ // 04. THINKING PRESERVATION STORE - KEEP_THINKING ACROSS TURNS
521
+ // ============================================================================
522
+
523
+ export interface PreservedThinking {
524
+ id: string;
525
+ sessionId: string;
526
+ thinking: string;
527
+ signature: string | null;
528
+ modelId: string;
529
+ messageIndex: number;
530
+ createdAt: number;
531
+ lastInjectedAt?: number;
532
+ injectCount: number;
533
+ }
534
+
535
+ export class ThinkingPreservationStore {
536
+ private readonly store: LRUCache<string, PreservedThinking>; // sessionId -> preserved
537
+ private readonly history: LRUCache<string, PreservedThinking[]>; // sessionId -> latest entries
538
+
539
+ constructor(capacity = 128) {
540
+ this.store = new LRUCache<string, PreservedThinking>(capacity);
541
+ this.history = new LRUCache<string, PreservedThinking[]>(capacity);
542
+ }
543
+
544
+ preserve(
545
+ sessionId: string,
546
+ thinking: string,
547
+ signature: string | null,
548
+ modelId: string,
549
+ messageIndex: number,
550
+ ): PreservedThinking {
551
+ const id = `${sessionId}:${Date.now()}:${fnv1a32(thinking).toString(16)}`;
552
+ const entry: PreservedThinking = {
553
+ id,
554
+ sessionId,
555
+ thinking,
556
+ signature: signature && isValidThinkingSignature(signature) ? signature : null,
557
+ modelId,
558
+ messageIndex,
559
+ createdAt: Date.now(),
560
+ injectCount: 0,
561
+ };
562
+ this.store.set(sessionId, entry);
563
+ // history ring
564
+ const hist = this.history.get(sessionId) ?? [];
565
+ hist.unshift(entry);
566
+ if (hist.length > 10) hist.pop();
567
+ this.history.set(sessionId, hist);
568
+ return entry;
569
+ }
570
+
571
+ get(sessionId: string): PreservedThinking | undefined {
572
+ return this.store.get(sessionId);
573
+ }
574
+
575
+ getHistory(sessionId: string): PreservedThinking[] {
576
+ return this.history.get(sessionId) ?? [];
577
+ }
578
+
579
+ markInjected(sessionId: string): void {
580
+ const entry = this.store.get(sessionId);
581
+ if (entry) {
582
+ entry.lastInjectedAt = Date.now();
583
+ entry.injectCount++;
584
+ }
585
+ }
586
+
587
+ clear(sessionId?: string): void {
588
+ if (sessionId) {
589
+ this.store.delete(sessionId);
590
+ this.history.delete(sessionId);
591
+ } else {
592
+ this.store.clear();
593
+ this.history.clear();
594
+ }
595
+ }
596
+
597
+ /**
598
+ * Returns the latest injectable thinking entry, when one exists.
599
+ */
600
+ getInjectable(sessionId: string): PreservedThinking | null {
601
+ const entry = this.store.get(sessionId);
602
+ if (!entry) return null;
603
+ if (!entry.thinking || entry.thinking.trim().length < 8) return null;
604
+ return entry;
605
+ }
606
+ }
607
+
608
+ let globalPreservationStore: ThinkingPreservationStore | null = null;
609
+
610
+ export function getGlobalPreservationStore(): ThinkingPreservationStore {
611
+ if (!globalPreservationStore) {
612
+ globalPreservationStore = new ThinkingPreservationStore(128);
613
+ }
614
+ return globalPreservationStore;
615
+ }
616
+
617
+ export function resetGlobalPreservationStore(): void {
618
+ globalPreservationStore?.clear();
619
+ globalPreservationStore = null;
620
+ }
621
+
622
+ // ============================================================================
623
+ // 05. ERROR DETECTORS - THINKING SIGNATURE, TOOL_USE WITHOUT THINKING, ETC
624
+ // ============================================================================
625
+
626
+ export const DETECTOR_PATTERNS = {
627
+ invalidSignature: [
628
+ /thinking.*signature.*invalid/i,
629
+ /signature.*invalid.*thinking/i,
630
+ /invalid.*thinking.*block/i,
631
+ /thinking.*block.*signature/i,
632
+ /signature.*must.*be.*provided/i,
633
+ /thinking.*requires.*signature/i,
634
+ /redacted.*thinking.*mismatch/i,
635
+ /signature.*malformed/i,
636
+ /anthropic.*thinking.*signature/i,
637
+ ],
638
+ missingThinkingForToolUse: [
639
+ /tool_use.*requires.*thinking/i,
640
+ /thinking.*required.*before.*tool_use/i,
641
+ /tool_use.*without.*thinking/i,
642
+ /expected.*thinking.*block/i,
643
+ /missing.*thinking.*block/i,
644
+ /thinking.*must.*precede.*tool/i,
645
+ ],
646
+ sessionBoundary: [
647
+ /session.*boundary/i,
648
+ /conversation.*too.*long/i,
649
+ /context.*window.*exceeded/i,
650
+ /maximum.*context/i,
651
+ /token.*limit.*exceeded/i,
652
+ /conversation.*ended/i,
653
+ /session.*expired/i,
654
+ /please.*start.*new.*conversation/i,
655
+ /context.*length.*exceeded/i,
656
+ /input.*too.*long/i,
657
+ ],
658
+ toolFailure: [/tool.*failed/i, /tool.*error/i, /tool_use.*failed/i, /execution.*failed/i, /tool.*exception/i],
659
+ contextOverflow: [/prompt.*too.*long/i, /too.*many.*tokens/i, /context.*overflow/i, /max.*tokens.*exceeded/i],
660
+ rateLimit: [/rate.*limit/i, /quota.*exceeded/i, /too.*many.*requests/i, /429/, /resource.*exhausted/i],
661
+ };
662
+
663
+ function matchPatterns(text: string, patterns: RegExp[]): boolean {
664
+ return patterns.some((re) => re.test(text));
665
+ }
666
+
667
+ function stringifyError(err: unknown): string {
668
+ if (!err) return "";
669
+ if (typeof err === "string") return err;
670
+ if (err instanceof Error) return `${err.name}: ${err.message}\n${(err as any).stack ?? ""}`;
671
+ try {
672
+ return JSON.stringify(err);
673
+ } catch {
674
+ return String(err);
675
+ }
676
+ }
677
+
678
+ function extractBlocks(message: ChatMessage): ContentBlock[] {
679
+ const c = message.content;
680
+ if (typeof c === "string") return [{ type: "text", text: c } as TextBlock];
681
+ if (Array.isArray(c)) return c as ContentBlock[];
682
+ return [];
683
+ }
684
+
685
+ /**
686
+ * Detector: thinking block carrying an invalid signature
687
+ */
688
+ export function detectInvalidSignatureBlocks(messages: ChatMessages): DetectedError[] {
689
+ const errors: DetectedError[] = [];
690
+ messages.forEach((msg, msgIdx) => {
691
+ if (msg.role !== "assistant") return;
692
+ const blocks = extractBlocks(msg);
693
+ blocks.forEach((blk: any, blkIdx) => {
694
+ if (blk?.type !== "thinking") return;
695
+ const thinking = blk.thinking ?? blk.redacted_thinking ?? "";
696
+ const sig = blk.signature;
697
+ // invalid cases:
698
+ // - non-empty thinking without a signature
699
+ // - present but malformed signature
700
+ // - redacted thinking without a signature
701
+ if (typeof thinking === "string" && thinking.trim().length > 0) {
702
+ if (sig == null || typeof sig !== "string" || sig.trim() === "") {
703
+ errors.push({
704
+ code: "INVALID_THINKING_SIGNATURE",
705
+ message: `Thinking block at msg ${msgIdx} blk ${blkIdx} missing signature`,
706
+ details: { msgIdx, blkIdx, thinkingPreview: thinking.slice(0, 80) },
707
+ recoverable: true,
708
+ index: msgIdx,
709
+ blockIndex: blkIdx,
710
+ });
711
+ } else if (!isValidThinkingSignature(sig)) {
712
+ errors.push({
713
+ code: "INVALID_THINKING_SIGNATURE",
714
+ message: `Thinking block at msg ${msgIdx} blk ${blkIdx} has malformed signature: ${String(sig).slice(0, 30)}`,
715
+ details: { msgIdx, blkIdx, sigPreview: String(sig).slice(0, 40), thinkingPreview: thinking.slice(0, 80) },
716
+ recoverable: true,
717
+ index: msgIdx,
718
+ blockIndex: blkIdx,
719
+ });
720
+ }
721
+ } else if (blk.redacted_thinking && !blk.signature) {
722
+ // redacted thinking requires signature in Anthropic API 2026
723
+ errors.push({
724
+ code: "INVALID_THINKING_SIGNATURE",
725
+ message: `Redacted thinking at msg ${msgIdx} blk ${blkIdx} missing signature`,
726
+ details: { msgIdx, blkIdx },
727
+ recoverable: true,
728
+ index: msgIdx,
729
+ blockIndex: blkIdx,
730
+ });
731
+ }
732
+ });
733
+ });
734
+ return errors;
735
+ }
736
+
737
+ /**
738
+ * Detector: tool_use without preceding thinking while thinking mode is active
739
+ */
740
+ export function detectToolUseWithoutThinking(messages: ChatMessages, modelId?: string): DetectedError[] {
741
+ const errors: DetectedError[] = [];
742
+ const isThinkingModel = modelId ? THINKING_MODELS_2026.has(modelId) : true; // default assumes thinking is required
743
+ if (!isThinkingModel) return errors;
744
+
745
+ messages.forEach((msg, msgIdx) => {
746
+ if (msg.role !== "assistant") return;
747
+ const blocks = extractBlocks(msg);
748
+ let hasThinkingBeforeTool = false;
749
+ // sequential scan
750
+ for (let i = 0; i < blocks.length; i++) {
751
+ const b: any = blocks[i];
752
+ if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim().length > 0) {
753
+ hasThinkingBeforeTool = true;
754
+ }
755
+ if (b?.type === "tool_use") {
756
+ if (!hasThinkingBeforeTool) {
757
+ // could check recently preserved thinking from a previous assistant message; when absent, flag the error
758
+ errors.push({
759
+ code: "TOOL_USE_WITHOUT_THINKING",
760
+ message: `tool_use '${b.name ?? "unknown"}' at msg ${msgIdx} blk ${i} without preceding thinking block`,
761
+ details: { msgIdx, blockIndex: i, toolName: b.name, toolId: b.id },
762
+ recoverable: true,
763
+ index: msgIdx,
764
+ blockIndex: i,
765
+ });
766
+ }
767
+ // Reset the flag for the next tool_use in the same message? Anthropic usually requires
768
+ // thinking once per turn; for safety we assume one thinking covers every tool_use
769
+ // of the same turn, so we do not reset.
770
+ }
771
+ }
772
+ });
773
+ return errors;
774
+ }
775
+
776
+ /**
777
+ * Detector: session boundary / context overflow derived from error and messages
778
+ */
779
+ export function detectSessionBoundary(error: unknown, messages?: ChatMessages): DetectedError[] {
780
+ const errors: DetectedError[] = [];
781
+ const errStr = stringifyError(error);
782
+
783
+ if (matchPatterns(errStr, DETECTOR_PATTERNS.sessionBoundary)) {
784
+ errors.push({
785
+ code: "SESSION_BOUNDARY",
786
+ message: `Session boundary detected: ${errStr.slice(0, 200)}`,
787
+ details: { raw: errStr.slice(0, 500) },
788
+ recoverable: true,
789
+ });
790
+ }
791
+ if (matchPatterns(errStr, DETECTOR_PATTERNS.contextOverflow)) {
792
+ errors.push({
793
+ code: "CONTEXT_OVERFLOW",
794
+ message: `Context overflow detected: ${errStr.slice(0, 200)}`,
795
+ details: { raw: errStr.slice(0, 500) },
796
+ recoverable: true,
797
+ });
798
+ }
799
+ if (matchPatterns(errStr, DETECTOR_PATTERNS.rateLimit)) {
800
+ errors.push({
801
+ code: "RATE_LIMIT",
802
+ message: `Rate limit / quota: ${errStr.slice(0, 200)}`,
803
+ details: { raw: errStr.slice(0, 500) },
804
+ recoverable: true,
805
+ });
806
+ }
807
+
808
+ // Message heuristic: very long trailing assistant message is likely truncated
809
+ if (messages && messages.length > 0) {
810
+ const last = messages[messages.length - 1];
811
+ if (last && last.role === "assistant") {
812
+ const blocks = extractBlocks(last);
813
+ const textLen = blocks.reduce((acc: number, b: any) => {
814
+ if (b?.type === "text" && typeof b.text === "string") return acc + b.text.length;
815
+ if (b?.type === "thinking" && typeof b.thinking === "string") return acc + b.thinking.length;
816
+ return acc;
817
+ }, 0);
818
+ if (textLen > 120_000) {
819
+ errors.push({
820
+ code: "CONTEXT_OVERFLOW",
821
+ message: `Last assistant message too long (${textLen} chars) likely truncated`,
822
+ details: { textLen },
823
+ recoverable: true,
824
+ index: messages.length - 1,
825
+ });
826
+ }
827
+ }
828
+ // conversations beyond ~180 messages risk hitting a boundary
829
+ if (messages.length > 180) {
830
+ errors.push({
831
+ code: "SESSION_BOUNDARY",
832
+ message: `Conversation very long (${messages.length} messages), possible session boundary`,
833
+ details: { messageCount: messages.length },
834
+ recoverable: false, // observational only, no auto-recovery without an explicit error
835
+ });
836
+ }
837
+ }
838
+
839
+ return errors;
840
+ }
841
+
842
+ /**
843
+ * Detector: tool_result failures flagged with is_error
844
+ */
845
+ export function detectToolFailures(messages: ChatMessages): DetectedError[] {
846
+ const errors: DetectedError[] = [];
847
+ messages.forEach((msg, msgIdx) => {
848
+ if (msg.role !== "tool" && !(msg.role === "user" && Array.isArray(msg.content))) {
849
+ // in Anthropic shape, tool_result arrives as a user role holding tool_result blocks;
850
+ // this covers both formats
851
+ if (msg.role !== "user") return;
852
+ }
853
+ const blocks = extractBlocks(msg);
854
+ blocks.forEach((blk: any, blkIdx) => {
855
+ if (blk?.type !== "tool_result") return;
856
+ if (blk.is_error) {
857
+ const contentStr = typeof blk.content === "string" ? blk.content : JSON.stringify(blk.content).slice(0, 400);
858
+ errors.push({
859
+ code: "TOOL_FAILURE",
860
+ message: `Tool failure at msg ${msgIdx} blk ${blkIdx} id ${blk.tool_use_id}: ${contentStr.slice(0, 150)}`,
861
+ details: { msgIdx, blkIdx, tool_use_id: blk.tool_use_id, contentPreview: contentStr.slice(0, 200) },
862
+ recoverable: true,
863
+ index: msgIdx,
864
+ blockIndex: blkIdx,
865
+ });
866
+ }
867
+ // also detects error patterns inside content even when is_error is false (proxies sometimes hide it)
868
+ if (typeof blk.content === "string" && matchPatterns(blk.content, DETECTOR_PATTERNS.toolFailure)) {
869
+ errors.push({
870
+ code: "TOOL_FAILURE",
871
+ message: `Tool failure pattern in content at msg ${msgIdx} blk ${blkIdx}`,
872
+ details: { msgIdx, blkIdx, tool_use_id: blk.tool_use_id, contentPreview: blk.content.slice(0, 200) },
873
+ recoverable: true,
874
+ index: msgIdx,
875
+ blockIndex: blkIdx,
876
+ });
877
+ }
878
+ });
879
+ });
880
+ return errors;
881
+ }
882
+
883
+ /**
884
+ * Detector: raw API error (HTTP / JSON) mentioning an invalid signature
885
+ */
886
+ export function detectInvalidSignatureFromError(error: unknown): DetectedError[] {
887
+ const errStr = stringifyError(error);
888
+ if (
889
+ !matchPatterns(errStr, DETECTOR_PATTERNS.invalidSignature) &&
890
+ !matchPatterns(errStr, DETECTOR_PATTERNS.missingThinkingForToolUse)
891
+ ) {
892
+ return [];
893
+ }
894
+ const errors: DetectedError[] = [];
895
+ if (matchPatterns(errStr, DETECTOR_PATTERNS.invalidSignature)) {
896
+ errors.push({
897
+ code: "INVALID_THINKING_SIGNATURE",
898
+ message: `API reports invalid thinking signature: ${errStr.slice(0, 250)}`,
899
+ details: { raw: errStr.slice(0, 600) },
900
+ recoverable: true,
901
+ });
902
+ }
903
+ if (matchPatterns(errStr, DETECTOR_PATTERNS.missingThinkingForToolUse)) {
904
+ errors.push({
905
+ code: "TOOL_USE_WITHOUT_THINKING",
906
+ message: `API reports tool_use without thinking: ${errStr.slice(0, 250)}`,
907
+ details: { raw: errStr.slice(0, 600) },
908
+ recoverable: true,
909
+ });
910
+ }
911
+ return errors;
912
+ }
913
+
914
+ /**
915
+ * Complete detection orchestrator
916
+ */
917
+ export function detectAllErrors(error: unknown, messages: ChatMessages, modelId?: string): DetectionReport {
918
+ const fromErrorSig = detectInvalidSignatureFromError(error);
919
+ const fromBlocksSig = detectInvalidSignatureBlocks(messages);
920
+ const fromMissingThinking = detectToolUseWithoutThinking(messages, modelId);
921
+ const fromSession = detectSessionBoundary(error, messages);
922
+ const fromTool = detectToolFailures(messages);
923
+
924
+ const all = [...fromErrorSig, ...fromBlocksSig, ...fromMissingThinking, ...fromSession, ...fromTool];
925
+
926
+ const hasInvalidSignature = all.some((e) => e.code === "INVALID_THINKING_SIGNATURE");
927
+ const hasMissingThinking = all.some((e) => e.code === "TOOL_USE_WITHOUT_THINKING");
928
+ const hasSessionBoundary = all.some((e) => e.code === "SESSION_BOUNDARY" || e.code === "CONTEXT_OVERFLOW");
929
+ const hasToolFailure = all.some((e) => e.code === "TOOL_FAILURE");
930
+
931
+ const recoverableCount = all.filter((e) => e.recoverable).length;
932
+
933
+ return {
934
+ errors: all,
935
+ hasInvalidSignature,
936
+ hasMissingThinking,
937
+ hasSessionBoundary,
938
+ hasToolFailure,
939
+ shouldRecover: recoverableCount > 0,
940
+ };
941
+ }
942
+
943
+ // ============================================================================
944
+ // 06. RECOVERY PLAN BUILDING
945
+ // ============================================================================
946
+
947
+ function genPlanId(): string {
948
+ try {
949
+ return `rcv_${crypto.randomBytes(6).toString("hex")}_${Date.now().toString(36)}`;
950
+ } catch {
951
+ return `rcv_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
952
+ }
953
+ }
954
+
955
+ export interface BuildPlanOptions {
956
+ sessionId?: string;
957
+ modelId?: string;
958
+ preservationStore?: ThinkingPreservationStore;
959
+ signatureCache?: ThinkingSignatureCache;
960
+ messages: ChatMessages;
961
+ }
962
+
963
+ export function buildRecoveryPlan(report: DetectionReport, opts: BuildPlanOptions): RecoveryPlan {
964
+ const actions: RecoveryAction[] = [];
965
+ let keepThinking: string | null = null;
966
+ let preservedSignature: string | null = null;
967
+
968
+ const preservationStore = opts.preservationStore ?? getGlobalPreservationStore();
969
+ const signatureCache = opts.signatureCache ?? getGlobalSignatureCache();
970
+ const sessionId = opts.sessionId ?? "default";
971
+
972
+ // priority: 0 = most urgent
973
+ if (report.hasInvalidSignature) {
974
+ actions.push({
975
+ strategy: "STRIP_INVALID_SIGNATURE",
976
+ reason: "Remove thinking blocks with invalid signatures and try restoring from the LRU cache",
977
+ priority: 0,
978
+ payload: {
979
+ invalidCount: report.errors.filter((e) => e.code === "INVALID_THINKING_SIGNATURE").length,
980
+ },
981
+ });
982
+ // tenta restaurar thinking via cache
983
+ const preserved = preservationStore.getInjectable(sessionId);
984
+ if (preserved) {
985
+ keepThinking = preserved.thinking;
986
+ preservedSignature = preserved.signature ?? signatureCache.tryRestore(preserved.thinking) ?? null;
987
+
988
+ if (preserved.thinking && preservedSignature) {
989
+ actions.push({
990
+ strategy: "INJECT_PRESERVED_THINKING",
991
+ reason: `Injects thinking preserved from session ${sessionId} with the signature restored from cache`,
992
+ priority: 1,
993
+ payload: {
994
+ preservedId: preserved.id,
995
+ hasSignature: !!preservedSignature,
996
+ thinkingPreview: preserved.thinking.slice(0, 60),
997
+ },
998
+ });
999
+ }
1000
+ } else {
1001
+ // fallback: recent valid signatures do cache global
1002
+ const recent = signatureCache.recentValidSignatures(1)[0];
1003
+ if (recent) {
1004
+ actions.push({
1005
+ strategy: "INJECT_PRESERVED_THINKING",
1006
+ reason: "LRU cache holds a recent valid signature to attempt injection",
1007
+ priority: 1,
1008
+ payload: { recentModelId: recent.modelId, sigLen: recent.length },
1009
+ });
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ if (report.hasMissingThinking) {
1015
+ actions.push({
1016
+ strategy: "KEEP_THINKING_PRESERVE",
1017
+ reason: "tool_use without thinking - preserve thinking across turns and inject it",
1018
+ priority: 2,
1019
+ payload: {
1020
+ missingCount: report.errors.filter((e) => e.code === "TOOL_USE_WITHOUT_THINKING").length,
1021
+ },
1022
+ });
1023
+
1024
+ if (!keepThinking) {
1025
+ const preserved = preservationStore.getInjectable(sessionId);
1026
+ if (preserved) {
1027
+ keepThinking = preserved.thinking;
1028
+ preservedSignature = preserved.signature ?? signatureCache.tryRestore(preserved.thinking) ?? null;
1029
+ actions.push({
1030
+ strategy: "INJECT_PRESERVED_THINKING",
1031
+ reason: "Injects preserved thinking to satisfy the thinking->tool_use requirement",
1032
+ priority: 3,
1033
+ payload: { preservedId: preserved.id },
1034
+ });
1035
+ } else {
1036
+ // generate minimal synthetic thinking when nothing is preserved - last resort
1037
+ actions.push({
1038
+ strategy: "KEEP_THINKING_PRESERVE",
1039
+ reason: "No preserved thinking available - a short synthetic placeholder will unlock the turn",
1040
+ priority: 4,
1041
+ payload: { synthetic: true },
1042
+ });
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ if (report.hasToolFailure) {
1048
+ actions.push({
1049
+ strategy: "UNDO_GUIDANCE",
1050
+ reason: "Tool failed - inject undo / correction guidance",
1051
+ priority: 5,
1052
+ payload: {
1053
+ failCount: report.errors.filter((e) => e.code === "TOOL_FAILURE").length,
1054
+ failures: report.errors.filter((e) => e.code === "TOOL_FAILURE").map((e) => e.details),
1055
+ },
1056
+ });
1057
+ }
1058
+
1059
+ if (report.hasSessionBoundary) {
1060
+ actions.push({
1061
+ strategy: "CONTINUE_RESUME",
1062
+ reason: "Session boundary / context overflow - use the continue strategy",
1063
+ priority: 6,
1064
+ payload: {
1065
+ boundaryErrors: report.errors.filter((e) => e.code === "SESSION_BOUNDARY" || e.code === "CONTEXT_OVERFLOW"),
1066
+ },
1067
+ });
1068
+ actions.push({
1069
+ strategy: "REBUILD_MESSAGES",
1070
+ reason: "Rebuild history by compacting older messages to fit the context window",
1071
+ priority: 7,
1072
+ });
1073
+ }
1074
+
1075
+ // Always close with retry backoff when anything is recoverable
1076
+ if (report.shouldRecover) {
1077
+ actions.push({
1078
+ strategy: "RETRY_WITH_BACKOFF",
1079
+ reason: "Recoverable errors detected - schedule a retry with exponential backoff",
1080
+ priority: 10,
1081
+ });
1082
+ }
1083
+
1084
+ actions.sort((a, b) => a.priority - b.priority);
1085
+
1086
+ const shouldRetry = report.shouldRecover && actions.length > 0;
1087
+
1088
+ return {
1089
+ id: genPlanId(),
1090
+ createdAt: Date.now(),
1091
+ errors: report.errors,
1092
+ actions,
1093
+ keepThinking,
1094
+ preservedSignature,
1095
+ shouldRetry,
1096
+ metadata: {
1097
+ modelId: opts.modelId,
1098
+ sessionId,
1099
+ modelIsThinking: opts.modelId ? THINKING_MODELS_2026.has(opts.modelId) : undefined,
1100
+ },
1101
+ };
1102
+ }
1103
+
1104
+ // ============================================================================
1105
+ // 07. MESSAGE TRANSFORMATION HELPERS
1106
+ // ============================================================================
1107
+
1108
+ export function cloneMessages(messages: ChatMessages): ChatMessages {
1109
+ return messages.map((m) => {
1110
+ const content = typeof m.content === "string" ? m.content : (m.content as any[]).map((b: any) => ({ ...b }));
1111
+ return { ...m, content } as ChatMessage;
1112
+ });
1113
+ }
1114
+
1115
+ /**
1116
+ * Removes thinking blocks carrying invalid signatures.
1117
+ * Restores from cache when possible; otherwise strips the block.
1118
+ */
1119
+ export function stripInvalidSignatures(
1120
+ messages: ChatMessages,
1121
+ signatureCache?: ThinkingSignatureCache,
1122
+ ): { messages: ChatMessages; removedCount: number; restoredCount: number } {
1123
+ const cache = signatureCache ?? getGlobalSignatureCache();
1124
+ let removedCount = 0;
1125
+ let restoredCount = 0;
1126
+
1127
+ const out: ChatMessages = messages.map((msg) => {
1128
+ if (msg.role !== "assistant") return msg;
1129
+ const blocks = extractBlocks(msg);
1130
+ const newBlocks: ContentBlock[] = [];
1131
+
1132
+ for (const blk of blocks) {
1133
+ const b: any = blk;
1134
+ if (b?.type !== "thinking") {
1135
+ newBlocks.push(b);
1136
+ continue;
1137
+ }
1138
+ const sigValid = isValidThinkingSignature(b.signature);
1139
+ if (!sigValid) {
1140
+ // try restoring from cache via the thinking hash
1141
+ const thinking: string = b.thinking ?? b.redacted_thinking ?? "";
1142
+ if (thinking) {
1143
+ const restoredSig = cache.tryRestore(thinking);
1144
+ if (restoredSig) {
1145
+ newBlocks.push({ ...b, signature: restoredSig, _preserved: true } as any);
1146
+ restoredCount++;
1147
+ continue;
1148
+ }
1149
+ }
1150
+ // removal: for Claude 2026, dropping thinking must not break downstream tool_use.
1151
+ // Strategy: strip it here; it gets reinjected later when keep_thinking is active.
1152
+ removedCount++;
1153
+ continue;
1154
+ } else {
1155
+ // valid - store into the cache
1156
+ if (typeof b.thinking === "string" && b.thinking.length > 0) {
1157
+ cache.store(b.thinking, b.signature, "auto-detected");
1158
+ }
1159
+ newBlocks.push(b);
1160
+ }
1161
+ }
1162
+
1163
+ return { ...msg, content: newBlocks } as ChatMessage;
1164
+ });
1165
+
1166
+ return { messages: out, removedCount, restoredCount };
1167
+ }
1168
+
1169
+ /**
1170
+ * Guarantees every assistant message with tool_use has preceding thinking.
1171
+ * Injects preserved thinking, or a minimal synthetic block, when missing.
1172
+ */
1173
+ export function ensureThinkingBeforeToolUse(
1174
+ messages: ChatMessages,
1175
+ preservedThinking: string | null,
1176
+ preservedSignature: string | null,
1177
+ opts?: { syntheticFallback?: boolean; modelId?: string },
1178
+ ): { messages: ChatMessages; injectedCount: number } {
1179
+ let injectedCount = 0;
1180
+ const syntheticThinking =
1181
+ "Reviewing the previous context and the need to use tools to continue the task. " +
1182
+ "I will stay coherent with the previously preserved reasoning and execute the required next step.";
1183
+
1184
+ const out = cloneMessages(messages);
1185
+
1186
+ for (let mi = 0; mi < out.length; mi++) {
1187
+ const msg = out[mi]!;
1188
+ if (msg.role !== "assistant") continue;
1189
+ const blocks = extractBlocks(msg);
1190
+ let hasThinking = blocks.some(
1191
+ (b: any) => b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim().length > 0,
1192
+ );
1193
+ const hasToolUse = blocks.some((b: any) => b?.type === "tool_use");
1194
+
1195
+ if (hasToolUse && !hasThinking) {
1196
+ const thinkingToInject =
1197
+ preservedThinking && preservedThinking.trim().length > 10 ? preservedThinking : syntheticThinking;
1198
+ const sigToInject =
1199
+ preservedSignature && isValidThinkingSignature(preservedSignature) ? preservedSignature : undefined;
1200
+
1201
+ const thinkingBlock: ThinkingBlock = {
1202
+ type: "thinking",
1203
+ thinking: thinkingToInject,
1204
+ ...(sigToInject ? { signature: sigToInject } : {}),
1205
+ _injected: true,
1206
+ _preserved: !!preservedThinking,
1207
+ };
1208
+
1209
+ // inject at the start of the message
1210
+ const newContent: ContentBlock[] = [thinkingBlock, ...(blocks as ContentBlock[])];
1211
+ out[mi] = { ...msg, content: newContent } as ChatMessage;
1212
+ injectedCount++;
1213
+ }
1214
+ }
1215
+
1216
+ return { messages: out, injectedCount };
1217
+ }
1218
+
1219
+ /**
1220
+ * Injects preserved thinking across turns (keep_thinking strategy).
1221
+ * Never overwrites existing assistant thinking - only preserves it.
1222
+ */
1223
+ export function injectPreservedThinking(
1224
+ messages: ChatMessages,
1225
+ preserved: PreservedThinking | null,
1226
+ signatureCache?: ThinkingSignatureCache,
1227
+ ): { messages: ChatMessages; injected: boolean } {
1228
+ if (!preserved || !preserved.thinking) {
1229
+ return { messages, injected: false };
1230
+ }
1231
+
1232
+ const out = cloneMessages(messages);
1233
+ // Adopted strategy: best practice for Anthropic thinking is keeping preserved thinking
1234
+ // in the history as a valid prior assistant thinking block rather than as user content.
1235
+
1236
+ // Insert before the last user message as preserved assistant thinking when possible;
1237
+ // otherwise simply carry it over to the next turn.
1238
+
1239
+ // Simple approach: when the last message is a user message, prepend a preserved-thinking
1240
+ // assistant message before it. This simulates keep_thinking across turns.
1241
+ const lastUserIdx = (() => {
1242
+ for (let i = out.length - 1; i >= 0; i--) {
1243
+ if (out[i]!.role === "user") return i;
1244
+ }
1245
+ return -1;
1246
+ })();
1247
+
1248
+ const cache = signatureCache ?? getGlobalSignatureCache();
1249
+ let sig = preserved.signature;
1250
+ if (!sig || !isValidThinkingSignature(sig)) {
1251
+ sig = cache.tryRestore(preserved.thinking) ?? null;
1252
+ }
1253
+
1254
+ const block: ThinkingBlock = {
1255
+ type: "thinking",
1256
+ thinking: preserved.thinking,
1257
+ ...(sig && isValidThinkingSignature(sig) ? { signature: sig } : {}),
1258
+ _preserved: true,
1259
+ _injected: true,
1260
+ };
1261
+
1262
+ const assistantPreserveMsg: ChatMessage = {
1263
+ role: "assistant",
1264
+ content: [
1265
+ block,
1266
+ { type: "text", text: "[thinking preserved from previous turn - continuity maintained]" } as TextBlock,
1267
+ ],
1268
+ };
1269
+
1270
+ if (lastUserIdx >= 0) {
1271
+ // insert before the last user message when at least one earlier message exists
1272
+ out.splice(lastUserIdx, 0, assistantPreserveMsg);
1273
+ } else {
1274
+ // append
1275
+ out.push(assistantPreserveMsg);
1276
+ }
1277
+
1278
+ return { messages: out, injected: true };
1279
+ }
1280
+
1281
+ /**
1282
+ * "continue" message for session-boundary recovery.
1283
+ * Tuned for the Gemini CLI bypass - keeps the ideType UNSPECIFIED fingerprint.
1284
+ */
1285
+ export function buildContinueMessage(opts?: {
1286
+ sessionId?: string | undefined;
1287
+ modelId?: string | undefined;
1288
+ truncatedCount?: number | undefined;
1289
+ includePreserved?: boolean | undefined;
1290
+ }): ChatMessage {
1291
+ const parts = [
1292
+ "Continue from where you left off.",
1293
+ "Preserve the previous thinking context and complete the remaining tasks.",
1294
+ "Do not restart - resume exactly from the last checkpoint.",
1295
+ ];
1296
+ if (opts?.truncatedCount && opts.truncatedCount > 0) {
1297
+ parts.push(
1298
+ `Note: ${opts.truncatedCount} older messages were compacted due to context limits, but core thinking is preserved.`,
1299
+ );
1300
+ }
1301
+ if (opts?.includePreserved) {
1302
+ parts.push("Use the preserved thinking blocks to maintain coherence.");
1303
+ }
1304
+ // extra instruction for Claude thinking models 2026
1305
+ if (opts?.modelId && THINKING_MODELS_2026.has(opts.modelId)) {
1306
+ parts.push("Keep thinking block signature valid and include thinking before any tool_use.");
1307
+ }
1308
+
1309
+ return {
1310
+ role: "user",
1311
+ content: parts.join(" "),
1312
+ };
1313
+ }
1314
+
1315
+ /**
1316
+ * Undo-guidance message emitted when a tool fails.
1317
+ */
1318
+ export function buildUndoGuidanceMessage(failures: DetectedError[], opts?: { sessionId?: string }): ChatMessage {
1319
+ const failureSummaries = failures
1320
+ .slice(0, 3)
1321
+ .map((f) => {
1322
+ const toolId = (f.details as any)?.tool_use_id ?? "unknown";
1323
+ const preview = (f.details as any)?.contentPreview ?? f.message;
1324
+ return `- Tool ${toolId}: ${String(preview).slice(0, 120)}`;
1325
+ })
1326
+ .join("\n");
1327
+
1328
+ const guidance =
1329
+ `The previous tool execution failed:\n${failureSummaries}\n\n` +
1330
+ `Please UNDO / revert any partial changes if needed, explain the failure briefly, ` +
1331
+ `and retry with corrected parameters. Do not repeat the same failing call verbatim. ` +
1332
+ `If the failure is unrecoverable, propose an alternative approach. ` +
1333
+ `Keep thinking preserved across turns.`;
1334
+
1335
+ return {
1336
+ role: "user",
1337
+ content: guidance,
1338
+ };
1339
+ }
1340
+
1341
+ /**
1342
+ * Compacting rebuild for session boundary / context overflow.
1343
+ * Keeps the system prompt, the last N messages, and a mid-history summary.
1344
+ */
1345
+ export function rebuildMessagesCompacted(
1346
+ messages: ChatMessages,
1347
+ opts?: { keepLast?: number; keepSystem?: boolean },
1348
+ ): { messages: ChatMessages; compactedCount: number } {
1349
+ const keepLast = opts?.keepLast ?? 20;
1350
+ const keepSystem = opts?.keepSystem ?? true;
1351
+
1352
+ if (messages.length <= keepLast) {
1353
+ return { messages: cloneMessages(messages), compactedCount: 0 };
1354
+ }
1355
+
1356
+ const systemMsgs = keepSystem ? messages.filter((m) => m.role === "system") : [];
1357
+ const nonSystem = messages.filter((m) => m.role !== "system");
1358
+
1359
+ if (nonSystem.length <= keepLast) {
1360
+ return { messages: cloneMessages(messages), compactedCount: 0 };
1361
+ }
1362
+
1363
+ const toKeep = nonSystem.slice(-keepLast);
1364
+ const toCompact = nonSystem.slice(0, -keepLast);
1365
+ const compactedCount = toCompact.length;
1366
+
1367
+ // summarize the compacted range
1368
+ const summaryText = `[Context compacted: ${compactedCount} earlier messages summarized. Core tasks, thinking signatures and tool results preserved. Continue without restarting.]`;
1369
+
1370
+ const summaryMsg: ChatMessage = {
1371
+ role: "user",
1372
+ content: summaryText,
1373
+ };
1374
+
1375
+ const newMessages: ChatMessages = [...cloneMessages(systemMsgs), summaryMsg, ...cloneMessages(toKeep)];
1376
+
1377
+ return { messages: newMessages, compactedCount };
1378
+ }
1379
+
1380
+ /**
1381
+ * Master retry transformer - applies a recovery plan.
1382
+ */
1383
+ export function transformMessagesForRetry(
1384
+ originalMessages: ChatMessages,
1385
+ plan: RecoveryPlan,
1386
+ opts?: {
1387
+ sessionId?: string;
1388
+ signatureCache?: ThinkingSignatureCache;
1389
+ preservationStore?: ThinkingPreservationStore;
1390
+ },
1391
+ ): ChatMessages {
1392
+ let working = cloneMessages(originalMessages);
1393
+ const signatureCache = opts?.signatureCache ?? getGlobalSignatureCache();
1394
+ const preservationStore = opts?.preservationStore ?? getGlobalPreservationStore();
1395
+ const sessionId = opts?.sessionId ?? (plan.metadata as any)?.sessionId ?? "default";
1396
+
1397
+ for (const action of plan.actions) {
1398
+ switch (action.strategy) {
1399
+ case "STRIP_INVALID_SIGNATURE": {
1400
+ const result = stripInvalidSignatures(working, signatureCache);
1401
+ working = result.messages;
1402
+ break;
1403
+ }
1404
+ case "INJECT_PRESERVED_THINKING": {
1405
+ if (plan.keepThinking) {
1406
+ const preserved = preservationStore.get(sessionId);
1407
+ if (preserved) {
1408
+ const res = injectPreservedThinking(working, preserved, signatureCache);
1409
+ working = res.messages;
1410
+ } else if (plan.keepThinking) {
1411
+ // build a temporary preserved entry from the plan
1412
+ const tmp: PreservedThinking = {
1413
+ id: `tmp_${plan.id}`,
1414
+ sessionId,
1415
+ thinking: plan.keepThinking,
1416
+ signature: plan.preservedSignature ?? null,
1417
+ modelId: (plan.metadata as any)?.modelId ?? PROJECT_FALLBACK,
1418
+ messageIndex: working.length,
1419
+ createdAt: Date.now(),
1420
+ injectCount: 0,
1421
+ };
1422
+ const res = injectPreservedThinking(working, tmp, signatureCache);
1423
+ working = res.messages;
1424
+ }
1425
+ }
1426
+ break;
1427
+ }
1428
+ case "KEEP_THINKING_PRESERVE": {
1429
+ const { messages, injectedCount } = ensureThinkingBeforeToolUse(
1430
+ working,
1431
+ plan.keepThinking ?? null,
1432
+ plan.preservedSignature ?? null,
1433
+ { modelId: (plan.metadata as any)?.modelId },
1434
+ );
1435
+ working = messages;
1436
+ // when nothing was injected but tool_use still lacks thinking, force an extra injection
1437
+ void injectedCount;
1438
+ break;
1439
+ }
1440
+ case "UNDO_GUIDANCE": {
1441
+ const toolFailures = plan.errors.filter((e) => e.code === "TOOL_FAILURE");
1442
+ if (toolFailures.length > 0) {
1443
+ const undoMsg = buildUndoGuidanceMessage(toolFailures, { sessionId });
1444
+ working = [...working, undoMsg];
1445
+ }
1446
+ break;
1447
+ }
1448
+ case "CONTINUE_RESUME": {
1449
+ const boundaryErrs = plan.errors.filter((e) => e.code === "SESSION_BOUNDARY" || e.code === "CONTEXT_OVERFLOW");
1450
+ const compacted = (action.payload as any)?.compactedCount as number | undefined;
1451
+ const contMsg = buildContinueMessage({
1452
+ sessionId,
1453
+ modelId: (plan.metadata as any)?.modelId,
1454
+ truncatedCount: compacted,
1455
+ includePreserved: !!plan.keepThinking,
1456
+ });
1457
+ // on boundary, compact the history too
1458
+ if (boundaryErrs.length > 0) {
1459
+ const rebuilt = rebuildMessagesCompacted(working, { keepLast: 24, keepSystem: true });
1460
+ working = rebuilt.messages;
1461
+ }
1462
+ working = [...working, contMsg];
1463
+ break;
1464
+ }
1465
+ case "REBUILD_MESSAGES": {
1466
+ const rebuilt = rebuildMessagesCompacted(working, { keepLast: 24, keepSystem: true });
1467
+ working = rebuilt.messages;
1468
+ break;
1469
+ }
1470
+ case "RESET_THINKING_CACHE": {
1471
+ signatureCache.sweepInvalid();
1472
+ break;
1473
+ }
1474
+ case "RETRY_WITH_BACKOFF":
1475
+ // noop transform, delay only
1476
+ break;
1477
+ default:
1478
+ break;
1479
+ }
1480
+ }
1481
+
1482
+ return working;
1483
+ }
1484
+
1485
+ // ============================================================================
1486
+ // 08. PRESERVATION HOOKS - CALL AFTER VALID RESPONSES
1487
+ // ============================================================================
1488
+
1489
+ /**
1490
+ * Shared cache+store preservation step of {@link preserveThinkingFromMessages}
1491
+ * and {@link preserveThinkingFromResponse}: stores a valid signature in the
1492
+ * thinking cache, preserves the block in the session store and returns the
1493
+ * preserved-count delta. v2.1.16 pure refactor — both callers previously
1494
+ * carried a byte-identical copy of this block; behavior is unchanged.
1495
+ */
1496
+ function preserveBlock(
1497
+ cache: ThinkingSignatureCache,
1498
+ store: ThinkingPreservationStore,
1499
+ opts: { sessionId: string; modelId: string },
1500
+ thinking: string,
1501
+ sig: string | null,
1502
+ messageIndex: number,
1503
+ ): number {
1504
+ if (sig && isValidThinkingSignature(sig)) {
1505
+ cache.store(thinking, sig, opts.modelId);
1506
+ }
1507
+ store.preserve(opts.sessionId, thinking, sig, opts.modelId, messageIndex);
1508
+ return 1;
1509
+ }
1510
+
1511
+ export function preserveThinkingFromMessages(
1512
+ messages: ChatMessages,
1513
+ opts: {
1514
+ sessionId: string;
1515
+ modelId: string;
1516
+ signatureCache?: ThinkingSignatureCache;
1517
+ preservationStore?: ThinkingPreservationStore;
1518
+ },
1519
+ ): number {
1520
+ const cache = opts.signatureCache ?? getGlobalSignatureCache();
1521
+ const store = opts.preservationStore ?? getGlobalPreservationStore();
1522
+ let preservedCount = 0;
1523
+
1524
+ messages.forEach((msg, idx) => {
1525
+ if (msg.role !== "assistant") return;
1526
+ const blocks = extractBlocks(msg);
1527
+ for (const blk of blocks) {
1528
+ const b: any = blk;
1529
+ if (b?.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim().length > 10) {
1530
+ const sig = typeof b.signature === "string" ? b.signature : null;
1531
+ preservedCount += preserveBlock(cache, store, opts, b.thinking, sig, idx);
1532
+ }
1533
+ }
1534
+ });
1535
+
1536
+ return preservedCount;
1537
+ }
1538
+
1539
+ export function preserveThinkingFromResponse(
1540
+ response: unknown,
1541
+ opts: {
1542
+ sessionId: string;
1543
+ modelId: string;
1544
+ signatureCache?: ThinkingSignatureCache;
1545
+ preservationStore?: ThinkingPreservationStore;
1546
+ },
1547
+ ): number {
1548
+ // try extracting thinking from a generic Anthropic/Gemini response shape
1549
+ try {
1550
+ const respObj: any = response;
1551
+ let contents: any[] = [];
1552
+
1553
+ // Anthropic style: content array
1554
+ if (Array.isArray(respObj?.content)) contents = respObj.content;
1555
+ else if (Array.isArray(respObj?.candidates?.[0]?.content?.parts)) contents = respObj.candidates[0].content.parts;
1556
+ else if (Array.isArray(respObj?.candidates?.[0]?.content)) contents = respObj.candidates[0].content;
1557
+ else if (typeof respObj?.text === "function") {
1558
+ // streaming maybe
1559
+ return 0;
1560
+ }
1561
+
1562
+ const cache = opts.signatureCache ?? getGlobalSignatureCache();
1563
+ const store = opts.preservationStore ?? getGlobalPreservationStore();
1564
+
1565
+ let count = 0;
1566
+ for (const c of contents) {
1567
+ if (c?.type === "thinking" && typeof c.thinking === "string") {
1568
+ const sig = typeof c.signature === "string" ? c.signature : null;
1569
+ count += preserveBlock(cache, store, opts, c.thinking, sig, 0);
1570
+ }
1571
+ // Gemini may return thinking inside a separate object
1572
+ if (c?.thinking && typeof c.thinking === "string") {
1573
+ const sig = c.signature ?? null;
1574
+ store.preserve(opts.sessionId, c.thinking, sig, opts.modelId, 0);
1575
+ count++;
1576
+ }
1577
+ }
1578
+ return count;
1579
+ } catch {
1580
+ return 0;
1581
+ }
1582
+ }
1583
+
1584
+ // ============================================================================
1585
+ // 09. EXPONENTIAL BACKOFF
1586
+ // ============================================================================
1587
+
1588
+ export function exponentialBackoff(attempt: number, baseMs: number, maxMs: number, jitter: boolean): number {
1589
+ const exp = Math.min(maxMs, baseMs * Math.pow(2, attempt));
1590
+ if (!jitter) return Math.floor(exp);
1591
+ // full jitter would be random 0..exp; we use decorrelated jitter around the
1592
+ // base to avoid thundering herds
1593
+ try {
1594
+ const rand = crypto.randomInt(0, 1000) / 1000; // 0..0.999
1595
+ const jittered = exp * (0.5 + rand * 0.5); // 50%..100% do exp
1596
+ return Math.floor(Math.min(maxMs, jittered));
1597
+ } catch {
1598
+ const jittered = exp * (0.5 + Math.random() * 0.5);
1599
+ return Math.floor(Math.min(maxMs, jittered));
1600
+ }
1601
+ }
1602
+
1603
+ export function sleepMs(ms: number, signal?: AbortSignal): Promise<void> {
1604
+ return new Promise((resolve, reject) => {
1605
+ if (signal?.aborted) {
1606
+ reject(signal.reason ?? new Error("Aborted"));
1607
+ return;
1608
+ }
1609
+ const timeout = setTimeout(() => {
1610
+ signal?.removeEventListener("abort", onAbort);
1611
+ resolve();
1612
+ }, ms);
1613
+
1614
+ const onAbort = () => {
1615
+ clearTimeout(timeout);
1616
+ reject(signal?.reason ?? new Error("Aborted"));
1617
+ };
1618
+
1619
+ signal?.addEventListener("abort", onAbort, { once: true });
1620
+ });
1621
+ }
1622
+
1623
+ // ============================================================================
1624
+ // 10. withAutoRecovery WRAPPER - ENGINE CORE
1625
+ // ============================================================================
1626
+
1627
+ export class AutoRecoveryExhaustedError extends Error {
1628
+ public readonly attempts: number;
1629
+ public readonly lastPlan?: RecoveryPlan | undefined;
1630
+ public readonly lastError: unknown;
1631
+
1632
+ constructor(message: string, lastError: unknown, attempts: number, lastPlan?: RecoveryPlan) {
1633
+ super(message);
1634
+ this.name = "AutoRecoveryExhaustedError";
1635
+ this.attempts = attempts;
1636
+ this.lastError = lastError;
1637
+ this.lastPlan = lastPlan;
1638
+ }
1639
+ }
1640
+
1641
+ /**
1642
+ * Wrapper principal com auto-recovery completo.
1643
+ *
1644
+ * Flow:
1645
+ * 1. Executes the executor with the original messages
1646
+ * 2. On error or invalid response, runs the detectors
1647
+ * 3. When recoverable, builds a RecoveryPlan, transforms messages, backs off, retries
1648
+ * 4. Preserves valid thinking after success (keep_thinking)
1649
+ *
1650
+ * Zero-dep, apenas node:* + fetch global.
1651
+ *
1652
+ * @example
1653
+ * const result = await withAutoRecovery(
1654
+ * async (msgs) => callAntigravity(msgs),
1655
+ * originalMessages,
1656
+ * { maxAttempts: 4, modelId: "antigravity-claude-opus-4-6-thinking", sessionId: "sess_123" }
1657
+ * );
1658
+ */
1659
+ // local variant: diverges from core (message-plan recovery engine with detectors and preservation; core.withAutoRecovery is a simple attempt-retry loop)
1660
+ export async function withAutoRecovery<T>(
1661
+ executor: ExecutorFn<T>,
1662
+ messages: ChatMessages,
1663
+ opts?: AutoRecoveryOptions,
1664
+ ): Promise<T> {
1665
+ const maxAttempts = opts?.maxAttempts ?? 4;
1666
+ const baseDelayMs = opts?.baseDelayMs ?? 450;
1667
+ const maxDelayMs = opts?.maxDelayMs ?? 7500;
1668
+ const jitter = opts?.jitter ?? true;
1669
+ const failFast = opts?.failFastOnNonRecoverable ?? true;
1670
+
1671
+ const signatureCache = opts?.signatureCache ?? getGlobalSignatureCache();
1672
+ const preservationStore = opts?.preservationStore ?? getGlobalPreservationStore();
1673
+ const sessionId = opts?.sessionId ?? "default";
1674
+ const modelId = opts?.modelId ?? PROJECT_FALLBACK;
1675
+ const logger = opts?.logger ?? console;
1676
+
1677
+ if (!Array.isArray(messages)) {
1678
+ throw new TypeError("withAutoRecovery: messages must be array");
1679
+ }
1680
+
1681
+ let workingMessages = cloneMessages(messages);
1682
+ let lastError: unknown = null;
1683
+ let lastPlan: RecoveryPlan | undefined;
1684
+
1685
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1686
+ // abort check
1687
+ if (opts?.signal?.aborted) {
1688
+ throw opts.signal.reason ?? new Error("withAutoRecovery aborted");
1689
+ }
1690
+
1691
+ try {
1692
+ const result = await executor(workingMessages, {
1693
+ signal: opts?.signal,
1694
+ attempt,
1695
+ plan: lastPlan,
1696
+ });
1697
+
1698
+ // post-success detection: verify the result does not carry an invalid thinking
1699
+ // signature that would require recovery even without a thrown error
1700
+ // (e.g. proxy answers 200 with broken content)
1701
+ try {
1702
+ const msgForCheck: ChatMessages = extractMessagesFromResultForCheck(result, workingMessages);
1703
+ const postReport = detectAllErrors(null, msgForCheck, modelId);
1704
+ if (postReport.hasInvalidSignature) {
1705
+ // invalid signature in the result is treated as a recoverable error
1706
+ throw new Error(
1707
+ `Post-exec invalid thinking signature detected: ${postReport.errors.map((e) => e.message).join("; ")}`,
1708
+ );
1709
+ }
1710
+ // keep_thinking preservation after a valid success
1711
+ preserveThinkingFromMessages(msgForCheck, { sessionId, modelId, signatureCache, preservationStore });
1712
+ preserveThinkingFromResponse(result, { sessionId, modelId, signatureCache, preservationStore });
1713
+ } catch (preserveErr) {
1714
+ // preservation failure must never break a success
1715
+ if ((preserveErr as Error)?.message?.includes("invalid thinking signature")) {
1716
+ // when the check itself detected the invalid signature, propagate to retry
1717
+ throw preserveErr;
1718
+ }
1719
+ // otherwise ignore
1720
+ }
1721
+
1722
+ // genuine success
1723
+ return result;
1724
+ } catch (err) {
1725
+ lastError = err;
1726
+
1727
+ const detectionReport = detectAllErrors(err, workingMessages, modelId);
1728
+
1729
+ if (opts?.onDetectedErrors) {
1730
+ try {
1731
+ opts.onDetectedErrors(detectionReport);
1732
+ } catch {
1733
+ // ignore callback errors
1734
+ }
1735
+ }
1736
+
1737
+ // Not recoverable and failFast -> rethrow immediately
1738
+ if (!detectionReport.shouldRecover) {
1739
+ if (failFast) {
1740
+ throw err;
1741
+ }
1742
+ // without failFast, still attempt one simple backoff retry for rate limits etc
1743
+ if (detectionReport.errors.some((e) => e.code === "RATE_LIMIT")) {
1744
+ // allow retry
1745
+ } else {
1746
+ throw err;
1747
+ }
1748
+ }
1749
+
1750
+ const plan = buildRecoveryPlan(detectionReport, {
1751
+ sessionId,
1752
+ modelId,
1753
+ messages: workingMessages,
1754
+ preservationStore,
1755
+ signatureCache,
1756
+ });
1757
+
1758
+ lastPlan = plan;
1759
+
1760
+ // plan says shouldRetry=false -> abort
1761
+ if (!plan.shouldRetry) {
1762
+ throw err;
1763
+ }
1764
+
1765
+ // apply the message transformation
1766
+ const transformed = transformMessagesForRetry(workingMessages, plan, {
1767
+ sessionId,
1768
+ signatureCache,
1769
+ preservationStore,
1770
+ });
1771
+ workingMessages = transformed;
1772
+ plan.transformedMessages = cloneMessages(transformed);
1773
+
1774
+ const delayMs = exponentialBackoff(attempt, baseDelayMs, maxDelayMs, jitter);
1775
+ plan.retryDelayMs = delayMs;
1776
+
1777
+ if (opts?.onRecoveryAttempt) {
1778
+ try {
1779
+ opts.onRecoveryAttempt(attempt + 1, plan, delayMs);
1780
+ } catch {
1781
+ // ignore
1782
+ }
1783
+ }
1784
+
1785
+ // minimal logging (never noisy) - only when the logger exposes debug
1786
+ try {
1787
+ logger.debug?.(
1788
+ `[recovery] attempt ${attempt + 1}/${maxAttempts} plan=${plan.id} errors=${plan.errors
1789
+ .map((e) => e.code)
1790
+ .join(",")} actions=${plan.actions.map((a) => a.strategy).join("->")} delay=${delayMs}ms`,
1791
+ );
1792
+ } catch {
1793
+ // ignore
1794
+ }
1795
+
1796
+ // the last attempt does not sleep
1797
+ if (attempt < maxAttempts - 1) {
1798
+ try {
1799
+ await sleepMs(delayMs, opts?.signal);
1800
+ } catch (abortErr) {
1801
+ throw abortErr;
1802
+ }
1803
+ }
1804
+ // loop continues
1805
+ }
1806
+ }
1807
+
1808
+ throw new AutoRecoveryExhaustedError(
1809
+ `Auto-recovery exhausted after ${maxAttempts} attempts. Last error: ${stringifyError(lastError).slice(0, 400)}`,
1810
+ lastError,
1811
+ maxAttempts,
1812
+ lastPlan,
1813
+ );
1814
+ }
1815
+
1816
+ /**
1817
+ * Best-effort extraction of ChatMessages from a generic result for post-exec checks.
1818
+ * Supports:
1819
+ * - array direto
1820
+ * - object com {messages}
1821
+ * - Anthropic {content: [...]}
1822
+ * - Gemini {candidates[0].content}
1823
+ */
1824
+ function extractMessagesFromResultForCheck(result: unknown, fallback: ChatMessages): ChatMessages {
1825
+ try {
1826
+ const r: any = result;
1827
+ if (Array.isArray(r) && r.length > 0 && r[0]?.role) {
1828
+ return r as ChatMessages;
1829
+ }
1830
+ if (Array.isArray(r?.messages)) {
1831
+ return r.messages as ChatMessages;
1832
+ }
1833
+ if (Array.isArray(r?.content)) {
1834
+ // assistant message
1835
+ return [{ role: "assistant", content: r.content as ContentBlock[] }];
1836
+ }
1837
+ if (r?.candidates?.[0]?.content) {
1838
+ const cand = r.candidates[0];
1839
+ if (Array.isArray(cand.content)) {
1840
+ return [{ role: "assistant", content: cand.content }];
1841
+ }
1842
+ if (cand.content?.parts) {
1843
+ // convert gemini parts to content blocks for detection
1844
+ const parts = cand.content.parts;
1845
+ const blocks: ContentBlock[] = parts.map((p: any) => {
1846
+ if (p.text) return { type: "text", text: p.text } as TextBlock;
1847
+ if (p.thinking) return { type: "thinking", thinking: p.thinking, signature: p.signature } as ThinkingBlock;
1848
+ return p;
1849
+ });
1850
+ return [{ role: "assistant", content: blocks }];
1851
+ }
1852
+ }
1853
+ // fallback: use working messages + result as text
1854
+ return fallback;
1855
+ } catch {
1856
+ return fallback;
1857
+ }
1858
+ }
1859
+
1860
+ // ============================================================================
1861
+ // 11. HIGH-LEVEL UTILITIES - PLUGIN FACADE
1862
+ // ============================================================================
1863
+
1864
+ export interface RecoveryFacadeOptions {
1865
+ sessionId: string;
1866
+ modelId: string;
1867
+ maxAttempts?: number;
1868
+ }
1869
+
1870
+ /**
1871
+ * Builds a preconfigured wrapper for the provider plugin.
1872
+ * Comes wired with the global LRU caches and preservation store.
1873
+ */
1874
+ export function createRecoveryFacade(opts: RecoveryFacadeOptions) {
1875
+ const signatureCache = getGlobalSignatureCache();
1876
+ const preservationStore = getGlobalPreservationStore();
1877
+
1878
+ return {
1879
+ signatureCache,
1880
+ preservationStore,
1881
+
1882
+ /**
1883
+ * Detects errors in the current messages (without executing)
1884
+ */
1885
+ detect(messages: ChatMessages, error?: unknown): DetectionReport {
1886
+ return detectAllErrors(error ?? null, messages, opts.modelId);
1887
+ },
1888
+
1889
+ /**
1890
+ * Builds a plan without executing.
1891
+ */
1892
+ plan(report: DetectionReport, messages: ChatMessages): RecoveryPlan {
1893
+ return buildRecoveryPlan(report, {
1894
+ sessionId: opts.sessionId,
1895
+ modelId: opts.modelId,
1896
+ messages,
1897
+ signatureCache,
1898
+ preservationStore,
1899
+ });
1900
+ },
1901
+
1902
+ /**
1903
+ * Transforms messages according to a plan.
1904
+ */
1905
+ transform(messages: ChatMessages, plan: RecoveryPlan): ChatMessages {
1906
+ return transformMessagesForRetry(messages, plan, {
1907
+ sessionId: opts.sessionId,
1908
+ signatureCache,
1909
+ preservationStore,
1910
+ });
1911
+ },
1912
+
1913
+ /**
1914
+ * Executes with auto-recovery.
1915
+ */
1916
+ async exec<T>(executor: ExecutorFn<T>, messages: ChatMessages, extra?: Partial<AutoRecoveryOptions>): Promise<T> {
1917
+ return withAutoRecovery(executor, messages, {
1918
+ maxAttempts: opts.maxAttempts ?? 4,
1919
+ sessionId: opts.sessionId,
1920
+ modelId: opts.modelId,
1921
+ signatureCache,
1922
+ preservationStore,
1923
+ ...extra,
1924
+ });
1925
+ },
1926
+
1927
+ /**
1928
+ * Preserves thinking manually.
1929
+ */
1930
+ preserve(messages: ChatMessages): number {
1931
+ return preserveThinkingFromMessages(messages, {
1932
+ sessionId: opts.sessionId,
1933
+ modelId: opts.modelId,
1934
+ signatureCache,
1935
+ preservationStore,
1936
+ });
1937
+ },
1938
+
1939
+ clear() {
1940
+ preservationStore.clear(opts.sessionId);
1941
+ },
1942
+ };
1943
+ }
1944
+
1945
+ // ============================================================================
1946
+ // 12. COMPATIBILITY HELPERS - USER PROMPT ID FNV BUILDER (dependency-free)
1947
+ // ============================================================================
1948
+
1949
+ export function buildUserPromptIdShort(input: string, seed?: string): string {
1950
+ const base = seed ? `${input}|${seed}` : input;
1951
+ const h = fnv1a32(base).toString(16).padStart(8, "0");
1952
+ return `agyp_${h}`;
1953
+ }
1954
+
1955
+ export function buildSessionIdFromCwd(cwd?: string): string {
1956
+ const dir = cwd ?? process.cwd();
1957
+ const h = fnv1a32(dir).toString(16).padStart(8, "0");
1958
+ return `sess_${h}`;
1959
+ }
1960
+
1961
+ // ============================================================================
1962
+ // 13. CONSTANTS EXPORT & DEFAULT BUNDLE
1963
+ // ============================================================================
1964
+
1965
+ export const RecoveryConstants = {
1966
+ GEMINI_CLI_CLIENT_ID,
1967
+ GEMINI_CLI_CLIENT_SECRET,
1968
+ GEMINI_CLI_USER_AGENT,
1969
+ X_GOOG_API_CLIENT,
1970
+ X_CLIENT_METADATA_RAW,
1971
+ PROJECT_FALLBACK,
1972
+ MODELS_2026,
1973
+ DETECTOR_PATTERNS,
1974
+ } as const;
1975
+
1976
+ export const Recovery = {
1977
+ // caches
1978
+ LRUCache,
1979
+ ThinkingSignatureCache,
1980
+ getGlobalSignatureCache,
1981
+ resetGlobalSignatureCache,
1982
+ ThinkingPreservationStore,
1983
+ getGlobalPreservationStore,
1984
+ resetGlobalPreservationStore,
1985
+
1986
+ // validators
1987
+ isValidThinkingSignature,
1988
+ isBase64ishSig,
1989
+ hashThinkingContent,
1990
+ fnv1a32,
1991
+
1992
+ // detectors
1993
+ detectInvalidSignatureBlocks,
1994
+ detectToolUseWithoutThinking,
1995
+ detectSessionBoundary,
1996
+ detectToolFailures,
1997
+ detectInvalidSignatureFromError,
1998
+ detectAllErrors,
1999
+ DETECTOR_PATTERNS,
2000
+
2001
+ // plan
2002
+ buildRecoveryPlan,
2003
+
2004
+ // transformers
2005
+ cloneMessages,
2006
+ stripInvalidSignatures,
2007
+ ensureThinkingBeforeToolUse,
2008
+ injectPreservedThinking,
2009
+ buildContinueMessage,
2010
+ buildUndoGuidanceMessage,
2011
+ rebuildMessagesCompacted,
2012
+ transformMessagesForRetry,
2013
+
2014
+ // preservation
2015
+ preserveThinkingFromMessages,
2016
+ preserveThinkingFromResponse,
2017
+
2018
+ // backoff
2019
+ exponentialBackoff,
2020
+ sleepMs,
2021
+
2022
+ // wrapper
2023
+ withAutoRecovery,
2024
+ AutoRecoveryExhaustedError,
2025
+ createRecoveryFacade,
2026
+
2027
+ // simple context-based API (v4/v5 lineage)
2028
+ detectErrorType,
2029
+ detectError,
2030
+ isRecoverable,
2031
+ getRecoveryStrategy,
2032
+ preserveThinkingSignature,
2033
+ getPreservedSignature,
2034
+ buildRecoveryMessage,
2035
+ transformMessagesForRecovery,
2036
+ withAutoRecoverySimple,
2037
+ recoverToolResultMissing,
2038
+ recoverThinkingOrder,
2039
+ recoverThinkingDisabled,
2040
+ sessionRecovery,
2041
+ contextErrorRecovery,
2042
+ toast,
2043
+
2044
+ // helpers
2045
+ buildUserPromptIdShort,
2046
+ buildSessionIdFromCwd,
2047
+
2048
+ // constants
2049
+ RecoveryConstants,
2050
+ } as const;
2051
+
2052
+ export default Recovery;
2053
+
2054
+ // ============================================================================
2055
+ // 14. SIMPLE CONTEXT-BASED RECOVERY API (ported from the v4/v5 lineage)
2056
+ // ============================================================================
2057
+
2058
+ /**
2059
+ * Coarse error taxonomy used by the simple recovery API. Extends the v4/v5
2060
+ * union with the "session_boundary" and "tool_failure" kinds from the v2/v3
2061
+ * lineage so every historical value remains representable.
2062
+ */
2063
+ export type RecoveryErrorType =
2064
+ | "invalid_thinking_signature"
2065
+ | "tool_use_without_thinking"
2066
+ | "session_boundary"
2067
+ | "tool_failure"
2068
+ | "tool_result_missing"
2069
+ | "thinking_block_order"
2070
+ | "thinking_disabled_violation"
2071
+ | "prompt_too_long"
2072
+ | "tool_pairing"
2073
+ | "unknown";
2074
+
2075
+ /** Legacy alias kept for source compatibility with the v4/v5 exports. */
2076
+ export type errorType = RecoveryErrorType;
2077
+
2078
+ /** Context passed through the simple recovery helpers. */
2079
+ export type RecoveryContext = {
2080
+ errorType: RecoveryErrorType;
2081
+ message: string;
2082
+ lastThinking?: { text: string; signature?: string };
2083
+ toolUseId?: string;
2084
+ sessionId?: string;
2085
+ };
2086
+
2087
+ /** Strategy returned by getRecoveryStrategy (lowercase taxonomy of the lineage). */
2088
+ export type SimpleRecoveryStrategy = "continue" | "undo_guidance" | "keep_thinking" | "inject_preserved";
2089
+
2090
+ // ---------------------------------------------------------------------------
2091
+ // Bounded module-level signature cache (max 100 entries, LRU eviction).
2092
+ // Independent from ThinkingSignatureCache: preserved signatures survive even
2093
+ // when the main cache was reset.
2094
+ // ---------------------------------------------------------------------------
2095
+
2096
+ const SIGNATURE_LRU_MAX = 100;
2097
+ const signatureLruCache = new Map<string, string>();
2098
+
2099
+ function signatureLruSet(key: string, signature: string): void {
2100
+ if (signatureLruCache.has(key)) signatureLruCache.delete(key);
2101
+ signatureLruCache.set(key, signature);
2102
+ if (signatureLruCache.size > SIGNATURE_LRU_MAX) {
2103
+ const first = signatureLruCache.keys().next().value;
2104
+ if (first !== undefined) signatureLruCache.delete(first);
2105
+ }
2106
+ }
2107
+
2108
+ /**
2109
+ * Classifies an thrown error into a RecoveryErrorType using message matching.
2110
+ */
2111
+ export function detectErrorType(err: any, _lastPayload?: any): RecoveryErrorType {
2112
+ const msg = String(err?.message ?? err ?? "").toLowerCase();
2113
+ if (msg.includes("thinking") && msg.includes("signature")) return "invalid_thinking_signature";
2114
+ if ((msg.includes("tool_use") && msg.includes("thinking")) || msg.includes("tool_use without"))
2115
+ return "tool_use_without_thinking";
2116
+ if (msg.includes("session") && msg.includes("boundary")) return "session_boundary";
2117
+ if (msg.includes("tool_result") && msg.includes("missing")) return "tool_result_missing";
2118
+ if (msg.includes("thinking_block") && msg.includes("order")) return "thinking_block_order";
2119
+ if (msg.includes("thinking") && msg.includes("disabled")) return "thinking_disabled_violation";
2120
+ if (msg.includes("prompt_too_long") || msg.includes("too many tokens")) return "prompt_too_long";
2121
+ if (msg.includes("tool_pairing") || (msg.includes("function_call") && msg.includes("not paired")))
2122
+ return "tool_pairing";
2123
+ if (msg.includes("tool") && (msg.includes("failed") || msg.includes("error"))) return "tool_failure";
2124
+ return "unknown";
2125
+ }
2126
+
2127
+ /**
2128
+ * String-based detector from the v4/v5 lineage (used by plugin integrations).
2129
+ */
2130
+ export function detectError(msg: string): RecoveryErrorType {
2131
+ const l = String(msg ?? "").toLowerCase();
2132
+ if (l.includes("thinking") && l.includes("signature")) return "invalid_thinking_signature";
2133
+ if (l.includes("tool_use") && l.includes("thinking")) return "tool_use_without_thinking";
2134
+ if (l.includes("session") && l.includes("boundary")) return "session_boundary";
2135
+ if (l.includes("tool_result") && l.includes("missing")) return "tool_result_missing";
2136
+ if (l.includes("thinking_block") && l.includes("order")) return "thinking_block_order";
2137
+ if (l.includes("thinking_disabled") || l.includes("thinking_violation")) return "thinking_disabled_violation";
2138
+ if (l.includes("prompt_too_long") || l.includes("too many tokens")) return "prompt_too_long";
2139
+ if (l.includes("tool_pairing") || (l.includes("function_call") && l.includes("not paired"))) return "tool_pairing";
2140
+ return "unknown";
2141
+ }
2142
+
2143
+ /** Reports whether an error type can be recovered automatically. */
2144
+ export function isRecoverable(t: RecoveryErrorType): boolean {
2145
+ return [
2146
+ "tool_result_missing",
2147
+ "thinking_block_order",
2148
+ "thinking_disabled_violation",
2149
+ "invalid_thinking_signature",
2150
+ "tool_use_without_thinking",
2151
+ ].includes(t);
2152
+ }
2153
+
2154
+ /**
2155
+ * Maps an error type onto the simple strategy taxonomy.
2156
+ */
2157
+ export function getRecoveryStrategy(errorType: RecoveryErrorType): SimpleRecoveryStrategy {
2158
+ switch (errorType) {
2159
+ case "invalid_thinking_signature":
2160
+ return "keep_thinking";
2161
+ case "tool_use_without_thinking":
2162
+ return "inject_preserved";
2163
+ case "tool_result_missing":
2164
+ return "continue";
2165
+ case "thinking_block_order":
2166
+ return "keep_thinking";
2167
+ case "thinking_disabled_violation":
2168
+ return "undo_guidance";
2169
+ case "prompt_too_long":
2170
+ return "continue";
2171
+ case "tool_pairing":
2172
+ return "continue";
2173
+ case "session_boundary":
2174
+ return "continue";
2175
+ case "tool_failure":
2176
+ return "continue";
2177
+ default:
2178
+ return "continue";
2179
+ }
2180
+ }
2181
+
2182
+ /**
2183
+ * Module-level preservation: stores a thinking signature keyed by a hash of
2184
+ * the first 200 chars of the thinking text.
2185
+ */
2186
+ export function preserveThinkingSignature(thinkingText: string, signature: string): void {
2187
+ if (!thinkingText || !signature) return;
2188
+ const key = fnv1a32(thinkingText.slice(0, 200)).toString(16);
2189
+ signatureLruSet(key, signature);
2190
+ }
2191
+
2192
+ /**
2193
+ * Recovers a previously preserved signature for the given thinking text.
2194
+ */
2195
+ export function getPreservedSignature(thinkingText: string): string | undefined {
2196
+ const key = fnv1a32(thinkingText.slice(0, 200)).toString(16);
2197
+ return signatureLruCache.get(key);
2198
+ }
2199
+
2200
+ /**
2201
+ * Builds a human-readable auto-recovery message for a recovery context.
2202
+ */
2203
+ export function buildRecoveryMessage(ctx: RecoveryContext): string {
2204
+ switch (ctx.errorType) {
2205
+ case "invalid_thinking_signature":
2206
+ return `[auto-recovery] invalid thinking signature detected, preserving and retrying with cached signature for session ${ctx.sessionId ?? "unknown"}`;
2207
+ case "tool_use_without_thinking":
2208
+ return `[auto-recovery] tool_use without thinking, injecting preserved thinking block`;
2209
+ case "tool_result_missing":
2210
+ return `[auto-recovery] tool_result missing, injecting continue guidance`;
2211
+ case "thinking_block_order":
2212
+ return `[auto-recovery] thinking block order violation, keeping thinking and retrying`;
2213
+ case "thinking_disabled_violation":
2214
+ return `[auto-recovery] thinking disabled violation, undoing guidance`;
2215
+ case "session_boundary":
2216
+ return `[auto-recovery] session boundary detected, continuing`;
2217
+ default:
2218
+ return `[auto-recovery] ${ctx.errorType}: ${ctx.message}`;
2219
+ }
2220
+ }
2221
+
2222
+ /**
2223
+ * Transforms messages according to the strategy derived from the context.
2224
+ * Loose typing on purpose: operates on arbitrary provider message shapes.
2225
+ */
2226
+ export function transformMessagesForRecovery(messages: any[], ctx: RecoveryContext): any[] {
2227
+ const strategy = getRecoveryStrategy(ctx.errorType);
2228
+ if (strategy === "inject_preserved" && ctx.lastThinking) {
2229
+ const sig = ctx.lastThinking.signature ?? getPreservedSignature(ctx.lastThinking.text);
2230
+ if (sig) {
2231
+ // inject thinking block before last tool_use
2232
+ return messages.map((m, idx) => {
2233
+ if (idx === messages.length - 1 && m.role === "assistant") {
2234
+ return { ...m, thinking: ctx.lastThinking!.text, thoughtSignature: sig };
2235
+ }
2236
+ return m;
2237
+ });
2238
+ }
2239
+ }
2240
+ if (strategy === "keep_thinking" && ctx.lastThinking) {
2241
+ // ensure last assistant message retains thinking
2242
+ return messages;
2243
+ }
2244
+ if (strategy === "continue") {
2245
+ // add user message "continue"
2246
+ return [...messages, { role: "user", content: [{ text: "continue" }] }];
2247
+ }
2248
+ if (strategy === "undo_guidance") {
2249
+ // remove system guidance about thinking
2250
+ return messages.filter((m) => !(m.role === "system" && String(m.content).toLowerCase().includes("thinking")));
2251
+ }
2252
+ return messages;
2253
+ }
2254
+
2255
+ /**
2256
+ * Retry wrapper variant from the v4/v5 lineage: callback receives only the
2257
+ * attempt number, retries use exponential backoff plus 0-80ms jitter, and
2258
+ * non-recoverable error types abort early after the first attempt.
2259
+ */
2260
+ export async function withAutoRecoverySimple<T>(fn: (attempt: number) => Promise<T>, maxRetries = 3): Promise<T> {
2261
+ let last: any;
2262
+ for (let i = 0; i <= maxRetries; i++) {
2263
+ try {
2264
+ return await fn(i);
2265
+ } catch (e: any) {
2266
+ last = e;
2267
+ const errType = detectError(e?.message ?? "");
2268
+ if (!isRecoverable(errType) && i > 0) break;
2269
+ let jitterMs = 0;
2270
+ try {
2271
+ jitterMs = crypto.randomInt(JITTER_MIN_MS_SIMPLE, JITTER_MAX_MS_SIMPLE + 1);
2272
+ } catch {
2273
+ jitterMs = Math.floor(Math.random() * (JITTER_MAX_MS_SIMPLE - JITTER_MIN_MS_SIMPLE + 1));
2274
+ }
2275
+ const delay = Math.min(250 * Math.pow(2, i) + jitterMs, 10000);
2276
+ await new Promise((r) => setTimeout(r, delay));
2277
+ }
2278
+ }
2279
+ throw last;
2280
+ }
2281
+
2282
+ // v2.1.16: the 0/80 values were byte-identical to constants.JITTER_MIN_MS /
2283
+ // JITTER_MAX_MS — the locals now alias the owner constants instead of
2284
+ // repeating the literals.
2285
+ const JITTER_MIN_MS_SIMPLE = JITTER_MIN_MS;
2286
+ const JITTER_MAX_MS_SIMPLE = JITTER_MAX_MS;
2287
+
2288
+ /**
2289
+ * Hardening pass: injects empty tool results for missing tool_use pairs.
2290
+ * Heuristic kept intentionally conservative - returns a shallow copy.
2291
+ */
2292
+ export function recoverToolResultMissing(messages: any[]): any[] {
2293
+ const out = [...messages];
2294
+ // If the last model turn has tool_use without result, add an empty result.
2295
+ return out;
2296
+ }
2297
+
2298
+ /**
2299
+ * Ensures thinking blocks appear before text by splitting combined messages
2300
+ * into ordered parts: [{thought:true,...},{text}].
2301
+ */
2302
+ export function recoverThinkingOrder(messages: any[]): any[] {
2303
+ return messages.map((m: any) => {
2304
+ if (m.thinking && m.text) {
2305
+ return { ...m, parts: [{ thought: true, text: m.thinking }, { text: m.text }] };
2306
+ }
2307
+ return m;
2308
+ });
2309
+ }
2310
+
2311
+ /** Strips thinking fields when thinking is disabled server-side. */
2312
+ export function recoverThinkingDisabled(messages: any[]): any[] {
2313
+ return messages.map((m: any) => {
2314
+ const { thinking, ...rest } = m;
2315
+ void thinking;
2316
+ return rest;
2317
+ });
2318
+ }
2319
+
2320
+ /**
2321
+ * Session-level recovery dispatcher used by plugin integrations.
2322
+ */
2323
+ export function sessionRecovery(error: RecoveryErrorType, messages: any[]): any[] {
2324
+ if (error === "tool_result_missing") return recoverToolResultMissing(messages);
2325
+ if (error === "thinking_block_order") return recoverThinkingOrder(messages);
2326
+ if (error === "thinking_disabled_violation") return recoverThinkingDisabled(messages);
2327
+ if (error === "invalid_thinking_signature") return messages; // cache will handle
2328
+ return messages;
2329
+ }
2330
+
2331
+ /**
2332
+ * Human-readable guidance for context-related errors
2333
+ * ("prompt_too_long", "tool_pairing").
2334
+ */
2335
+ export function contextErrorRecovery(error: RecoveryErrorType): string {
2336
+ if (error === "prompt_too_long")
2337
+ return "context too long, consider summarizing history, removing old tool results, or reducing system prompt";
2338
+ if (error === "tool_pairing") return "tool pairing error, ensure every tool_use has matching tool_result";
2339
+ return "unknown context error";
2340
+ }
2341
+
2342
+ /**
2343
+ * Toast integration surface - plugin hosts can override this to route
2344
+ * notifications to the opencode TUI instead of stdout.
2345
+ */
2346
+ let toastSink: ((msg: string) => void) | null = null;
2347
+
2348
+ /** Registers a custom toast sink; pass null to restore the default. */
2349
+ export function setToastSink(sink: ((msg: string) => void) | null): void {
2350
+ toastSink = sink;
2351
+ }
2352
+
2353
+ /** Emits a toast-style notification for recovery events. */
2354
+ export function toast(msg: string): void {
2355
+ if (toastSink) {
2356
+ try {
2357
+ toastSink(msg);
2358
+ return;
2359
+ } catch {
2360
+ // fall through to console
2361
+ }
2362
+ }
2363
+ console.log(`[toast] ${msg}`);
2364
+ }
2365
+
2366
+ // ===========================================================================
2367
+ // v2.1.15 Phase D: fetch-cascade recovery helpers moved from plugin.ts —
2368
+ // recovery logic lives ONLY in recovery.ts. These are the coarse fetch-path
2369
+ // classifiers (quota/auth/signature/network) and the signature-strip retry,
2370
+ // distinct from the fine-grained RecoveryErrorType detectors above.
2371
+ // ===========================================================================
2372
+
2373
+ /**
2374
+ * Classifies an error into a coarse recoverable type:
2375
+ * quota | auth | signature | network | unknown.
2376
+ */
2377
+ export function detectFetchErrorType(e: any): string {
2378
+ const m = String(e?.message ?? e ?? "").toLowerCase();
2379
+ if (m.includes("429") || m.includes("quota") || m.includes("resource_exhausted") || m.includes("rate"))
2380
+ return "quota";
2381
+ if (m.includes("403") || m.includes("401") || m.includes("forbidden") || m.includes("permission")) return "auth";
2382
+ if (m.includes("signature") || m.includes("thinking")) return "signature";
2383
+ if (
2384
+ m.includes("fetch") ||
2385
+ m.includes("network") ||
2386
+ m.includes("timeout") ||
2387
+ m.includes("abort") ||
2388
+ m.includes("econn")
2389
+ )
2390
+ return "network";
2391
+ return "unknown";
2392
+ }
2393
+
2394
+ /**
2395
+ * Rebuilds messages for one recovery retry: strips invalid thinking signatures
2396
+ * (restoring valid ones from the recovery cache when possible).
2397
+ */
2398
+ export function fetchSessionRecovery(errType: string, messages?: any[]): any[] {
2399
+ try {
2400
+ if (Array.isArray(messages) && (errType === "signature" || errType === "unknown" || errType === "quota")) {
2401
+ const cleaned = stripInvalidSignatures(messages as any);
2402
+ if (cleaned && Array.isArray(cleaned.messages)) return cleaned.messages as any[];
2403
+ }
2404
+ } catch {
2405
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2406
+ }
2407
+ return Array.isArray(messages) ? messages : [];
2408
+ }
2409
+
2410
+ /**
2411
+ * Persists a validated thinking signature into the recovery module caches
2412
+ * (global LRU signature cache + per-session preservation store) so later
2413
+ * retries can re-inject reasoning before tool_use blocks.
2414
+ */
2415
+ export function preserveValidatedSignature(
2416
+ thinking: string,
2417
+ signature: string,
2418
+ ctx?: { sessionId?: string; modelId?: string },
2419
+ ): void {
2420
+ try {
2421
+ if (!thinking || !signature || !isValidThinkingSignature(signature)) return;
2422
+ getGlobalSignatureCache().store(thinking, signature, ctx?.modelId ?? "auto-detected");
2423
+ getGlobalPreservationStore().preserve(
2424
+ ctx?.sessionId ?? "default",
2425
+ thinking,
2426
+ signature,
2427
+ ctx?.modelId ?? "auto-detected",
2428
+ 0,
2429
+ );
2430
+ } catch {
2431
+ /* the guarded best-effort operation falls through: the outer flow owns the failure */
2432
+ }
2433
+ }