@wenathlan/devthink 2.0.15 → 2.0.19
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.
- package/CHANGELOG.md +45 -0
- package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
- package/bridge.d.ts.map +1 -0
- package/bridge.js +1877 -0
- package/bun.d.ts.map +1 -0
- package/bun.js +52329 -0
- package/caps/background.json +712 -0
- package/caps/cli.json +360 -0
- package/caps/library.json +3611 -0
- package/caps/mcp.json +126 -0
- package/caps/pagebridge.json +377 -0
- package/caps/popup.json +51 -0
- package/caps/sidepanel.json +522 -0
- package/checksums.txt +142 -0
- package/cli.d.ts.map +1 -0
- package/{dist/cli.js → cli.js} +2 -2
- package/companion.d.ts.map +1 -0
- package/companion.js +389 -0
- package/crossbrowser.d.ts.map +1 -0
- package/crossbrowser.js +1191 -0
- package/dashdone.d.ts.map +1 -0
- package/dashdone.js +368 -0
- package/deno.d.ts.map +1 -0
- package/deno.js +52325 -0
- package/devthink.umd.js +52346 -0
- package/fixtures/bun.ts +7 -0
- package/fixtures/cjs.js +6 -0
- package/fixtures/deno.ts +6 -0
- package/fixtures/example-org-pagestate.json +22 -0
- package/fixtures/importers/automa-workflow.json +18 -0
- package/fixtures/importers/selenium-side.json +20 -0
- package/fixtures/importers/tabular-plan.csv +7 -0
- package/fixtures/importers/uivision-macro.json +10 -0
- package/fixtures/importers/v1-plan.json +13 -0
- package/fixtures/mcp-client.mjs +14 -0
- package/fixtures/pages/checkout.html +192 -0
- package/fixtures/pages/dashboard.html +110 -0
- package/fixtures/pages/feed.html +113 -0
- package/fixtures/pages/product-grid.html +150 -0
- package/fixtures/plans/form-inventory-plan.json +11 -0
- package/fixtures/plans/release-notes-plan.json +11 -0
- package/fixtures/recipes/agentscompete.json +11 -0
- package/fixtures/recipes/agentsforms.json +11 -0
- package/fixtures/recipes/agentsmonitor.json +11 -0
- package/fixtures/recipes/agentsreview.json +29 -0
- package/fixtures/recipes/agentswarm.json +11 -0
- package/fixtures/recipes/fillcalendar.json +12 -0
- package/fixtures/recipes/fillcheckout.json +13 -0
- package/fixtures/recipes/filldependentdropdowns.json +12 -0
- package/fixtures/recipes/fillfromcsv.json +13 -0
- package/fixtures/recipes/fillloginform.json +12 -0
- package/fixtures/recipes/fillmultistepwizard.json +13 -0
- package/fixtures/recipes/fillregistration.json +12 -0
- package/fixtures/recipes/fillsearchfilters.json +12 -0
- package/fixtures/recipes/filltypeahead.json +11 -0
- package/fixtures/recipes/monitoralerts.json +12 -0
- package/fixtures/recipes/monitoravailability.json +11 -0
- package/fixtures/recipes/monitornetworkquiet.json +12 -0
- package/fixtures/recipes/monitorpagechanges.json +11 -0
- package/fixtures/recipes/monitorpricechange.json +11 -0
- package/fixtures/recipes/monitorschedule.json +12 -0
- package/fixtures/recipes/scrapedynamicprices.json +13 -0
- package/fixtures/recipes/scrapeimagegallery.json +12 -0
- package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
- package/fixtures/recipes/scrapenestedlists.json +12 -0
- package/fixtures/recipes/scrapeproductgrid.json +12 -0
- package/fixtures/recipes/scrapereviews.json +12 -0
- package/fixtures/recipes/scrapesearchresults.json +12 -0
- package/fixtures/recipes/scrapetablepagination.json +12 -0
- package/fixtures/recipes/testaccessibility.json +34 -0
- package/fixtures/recipes/testconsoleerrors.json +12 -0
- package/fixtures/recipes/testdeeplinks.json +12 -0
- package/fixtures/recipes/testformvalidation.json +13 -0
- package/fixtures/recipes/testlayoutshift.json +12 -0
- package/fixtures/recipes/testlinkcheck.json +12 -0
- package/fixtures/recipes/testloadtiming.json +12 -0
- package/fixtures/recipes/testvisualdiff.json +12 -0
- package/fixtures/workflows/changelog-digest-workflow.json +13 -0
- package/fixtures/workflows/form-survey-workflow.json +13 -0
- package/gallery.json +706 -0
- package/hardening.d.ts.map +1 -0
- package/hardening.js +188 -0
- package/headless.d.ts.map +1 -0
- package/headless.js +11522 -0
- package/http.d.ts.map +1 -0
- package/http.js +2165 -0
- package/index.cjs +54682 -0
- package/index.d.ts.map +1 -0
- package/index.js +52310 -0
- package/index.neutral.js +46343 -0
- package/{web/manifest.json → manifest.json} +1 -1
- package/mcp.d.ts.map +1 -0
- package/mcp.js +20515 -0
- package/memory.d.ts.map +1 -0
- package/{memory.ts → memory.js} +2310 -3834
- package/nativehost.template.json +10 -0
- package/node.cjs +54686 -0
- package/node.d.ts.map +1 -0
- package/pack.d.ts.map +1 -0
- package/pack.js +805 -0
- package/package.json +83 -176
- package/policy.d.ts.map +1 -0
- package/{policy.ts → policy.js} +4679 -5073
- package/progress.d.ts.map +1 -0
- package/progress.js +645 -0
- package/protocol.d.ts.map +1 -0
- package/protocol.js +8334 -0
- package/schemas/audit.schema.json +226 -0
- package/schemas/capability.schema.json +113 -0
- package/schemas/envelope.schema.json +162 -0
- package/schemas/memory.schema.json +51 -0
- package/schemas/observation.schema.json +113 -0
- package/schemas/plan.schema.json +208 -0
- package/schemas/progress.schema.json +126 -0
- package/schemas/proposal.schema.json +68 -0
- package/schemas/session.schema.json +82 -0
- package/schemas/tool.schema.json +94 -0
- package/server.d.ts.map +1 -0
- package/server.js +28877 -0
- package/umd-example.html +18 -0
- package/umd.d.ts.map +1 -0
- package/accounts.ts +0 -2087
- package/agent.ts +0 -2331
- package/antigravity.ts +0 -2139
- package/apifreeze.ts +0 -4146
- package/auth.ts +0 -642
- package/background.ts +0 -16837
- package/bridge.ts +0 -2080
- package/build.ts +0 -54
- package/bun.ts +0 -26
- package/capture.ts +0 -1614
- package/checksecrets.ts +0 -72
- package/cjs.ts +0 -9
- package/cli.ts +0 -3500
- package/commands.ts +0 -1101
- package/companion.ts +0 -450
- package/compatibility.ts +0 -37
- package/config.ts +0 -3137
- package/constants.ts +0 -943
- package/core.ts +0 -1473
- package/crossbrowser.ts +0 -1359
- package/dashdone.ts +0 -605
- package/data.ts +0 -943
- package/database.ts +0 -133
- package/debug.ts +0 -850
- package/deno.ts +0 -27
- package/devthink.ts +0 -707
- package/dist/accounts.d.ts +0 -409
- package/dist/agent.d.ts +0 -837
- package/dist/antigravity.d.ts +0 -380
- package/dist/apifreeze.d.ts +0 -113
- package/dist/auth.d.ts +0 -224
- package/dist/capture.d.ts +0 -595
- package/dist/cjs.d.ts +0 -6
- package/dist/commands.d.ts +0 -310
- package/dist/compatibility.d.ts +0 -17
- package/dist/config.d.ts +0 -1853
- package/dist/constants.d.ts +0 -637
- package/dist/core.d.ts +0 -361
- package/dist/data.d.ts +0 -330
- package/dist/database.d.ts +0 -24
- package/dist/debug.d.ts +0 -270
- package/dist/engine.d.ts +0 -74
- package/dist/environments.d.ts +0 -244
- package/dist/evidence.d.ts +0 -277
- package/dist/export.d.ts +0 -311
- package/dist/fingerprint.d.ts +0 -594
- package/dist/flow.d.ts +0 -300
- package/dist/gates.d.ts +0 -103
- package/dist/identity.d.ts +0 -51
- package/dist/ids.d.ts +0 -7
- package/dist/llm.d.ts +0 -652
- package/dist/models.d.ts +0 -1807
- package/dist/net.d.ts +0 -325
- package/dist/neutral.d.ts +0 -41
- package/dist/oauth.d.ts +0 -703
- package/dist/page.d.ts +0 -1166
- package/dist/perf.d.ts +0 -481
- package/dist/plan.d.ts +0 -134
- package/dist/project.d.ts +0 -395
- package/dist/providers.d.ts +0 -37
- package/dist/quota.d.ts +0 -453
- package/dist/recovery.d.ts +0 -621
- package/dist/request.d.ts +0 -1492
- package/dist/run.d.ts +0 -779
- package/dist/runtime.d.ts +0 -212
- package/dist/search.d.ts +0 -375
- package/dist/security.d.ts +0 -534
- package/dist/serve.d.ts +0 -136
- package/dist/session.d.ts +0 -296
- package/dist/storage.d.ts +0 -53
- package/dist/streaming.d.ts +0 -1119
- package/dist/swarm.d.ts +0 -328
- package/dist/tools.d.ts +0 -170
- package/dist/types.d.ts +0 -8357
- package/dist/utils.d.ts +0 -82
- package/dist/version.d.ts +0 -7
- package/dist/versionregistry.d.ts +0 -406
- package/dist/views.d.ts +0 -579
- package/dist/workbenchsession.d.ts +0 -69
- package/dist/workflow.d.ts +0 -649
- package/docs/readme.md +0 -92
- package/docs/schemas/antigravity.json +0 -192
- package/engine.ts +0 -1524
- package/environments.ts +0 -1005
- package/evidence.ts +0 -643
- package/export.ts +0 -889
- package/fingerprint.ts +0 -1458
- package/flow.ts +0 -1190
- package/gates.ts +0 -307
- package/gateway.ts +0 -34
- package/hardening.ts +0 -191
- package/headless.ts +0 -369
- package/http.ts +0 -2110
- package/identity.ts +0 -246
- package/ids.ts +0 -26
- package/index.ts +0 -369
- package/llm.ts +0 -2682
- package/mcp.ts +0 -1835
- package/models.ts +0 -3805
- package/modes.ts +0 -43
- package/net.ts +0 -1214
- package/neutral.ts +0 -362
- package/node.ts +0 -26
- package/oauth.ts +0 -2216
- package/offscreen.ts +0 -170
- package/pack.ts +0 -956
- package/page.ts +0 -7824
- package/pagebridge.ts +0 -1116
- package/perf.ts +0 -957
- package/plan.ts +0 -672
- package/plugin.ts +0 -76
- package/prisma.config.ts +0 -27
- package/progress.ts +0 -1308
- package/project.ts +0 -1291
- package/protocol.ts +0 -4187
- package/providers.ts +0 -246
- package/quota.ts +0 -1887
- package/recovery.ts +0 -2433
- package/request.ts +0 -4482
- package/run.ts +0 -2135
- package/runtime.ts +0 -526
- package/search.ts +0 -1760
- package/security.ts +0 -1673
- package/serve.ts +0 -419
- package/server.ts +0 -5538
- package/session.ts +0 -1162
- package/storage.ts +0 -182
- package/streaming.ts +0 -4101
- package/swarm.ts +0 -884
- package/sync.ts +0 -56
- package/tests/agentcert.test.ts +0 -128
- package/tests/agentfleet.test.ts +0 -214
- package/tests/agentreview.test.ts +0 -241
- package/tests/agentstream.test.ts +0 -335
- package/tests/agentwork.test.ts +0 -356
- package/tests/apifreeze.test.ts +0 -290
- package/tests/apimap.test.ts +0 -112
- package/tests/approvalgate.test.ts +0 -175
- package/tests/artifactmanifest.test.ts +0 -204
- package/tests/attentionfeed.test.ts +0 -163
- package/tests/backgroundruns.test.ts +0 -100
- package/tests/batchscheduling.test.ts +0 -131
- package/tests/blackboard.test.ts +0 -238
- package/tests/bridge.test.ts +0 -1088
- package/tests/browsercoverage.test.ts +0 -134
- package/tests/browserpolyfills.test.ts +0 -124
- package/tests/build.mjs +0 -1260
- package/tests/clientauth.test.ts +0 -223
- package/tests/clitools.test.ts +0 -578
- package/tests/confirmgates.test.ts +0 -190
- package/tests/containerpack.test.ts +0 -143
- package/tests/controlflow.test.ts +0 -2120
- package/tests/coordination.test.ts +0 -345
- package/tests/copyscreen.test.ts +0 -139
- package/tests/core.test.ts +0 -417
- package/tests/costcert.test.ts +0 -95
- package/tests/crossbrowser.test.ts +0 -115
- package/tests/cspaudit.test.ts +0 -79
- package/tests/dashdone.test.ts +0 -615
- package/tests/datagrid.test.ts +0 -137
- package/tests/doccheck.test.ts +0 -107
- package/tests/dombatch.test.ts +0 -109
- package/tests/emulation.test.ts +0 -282
- package/tests/environments.test.ts +0 -259
- package/tests/evidenceviews.test.ts +0 -88
- package/tests/exporttools.test.ts +0 -185
- package/tests/firefoxprep.test.ts +0 -134
- package/tests/flowlibrary.test.ts +0 -242
- package/tests/flowrun.test.ts +0 -303
- package/tests/forensics.test.ts +0 -439
- package/tests/gateway.test.ts +0 -939
- package/tests/headlesslib.test.ts +0 -171
- package/tests/httpstream.test.ts +0 -277
- package/tests/immutablelog.test.ts +0 -157
- package/tests/inboundguard.test.ts +0 -177
- package/tests/lazyload.test.ts +0 -72
- package/tests/librarymodes.test.ts +0 -623
- package/tests/llm.test.ts +0 -696
- package/tests/manifest.test.ts +0 -226
- package/tests/maskinputs.test.ts +0 -150
- package/tests/matrixverify.test.ts +0 -111
- package/tests/mavenpack.test.ts +0 -75
- package/tests/mcpserver.test.ts +0 -704
- package/tests/memory.test.ts +0 -4155
- package/tests/memorycare.test.ts +0 -230
- package/tests/migrateplan.test.ts +0 -349
- package/tests/minimization.test.ts +0 -414
- package/tests/modelroute.test.ts +0 -112
- package/tests/multiagent.test.ts +0 -440
- package/tests/nativebridge.test.ts +0 -807
- package/tests/navigation.test.ts +0 -284
- package/tests/nugetpack.test.ts +0 -83
- package/tests/orchestration.test.ts +0 -449
- package/tests/originpolicy.test.ts +0 -405
- package/tests/outputcompare.test.ts +0 -97
- package/tests/package.test.ts +0 -91
- package/tests/pagebridge.test.ts +0 -5909
- package/tests/pentest.test.ts +0 -75
- package/tests/perfrecords.test.ts +0 -257
- package/tests/permdiff.test.ts +0 -85
- package/tests/phishguard.test.ts +0 -128
- package/tests/pickerviews.test.ts +0 -155
- package/tests/pipelines.test.ts +0 -381
- package/tests/planlint.test.ts +0 -240
- package/tests/planreview.test.ts +0 -383
- package/tests/platformtargets.test.ts +0 -56
- package/tests/policy.test.ts +0 -8284
- package/tests/portability.test.ts +0 -96
- package/tests/progress.test.ts +0 -1036
- package/tests/promptlibrary.test.ts +0 -116
- package/tests/protocol.test.ts +0 -4793
- package/tests/publishingpipeline.test.ts +0 -314
- package/tests/quickactions.test.ts +0 -164
- package/tests/readiness.test.ts +0 -78
- package/tests/recipes.test.ts +0 -163
- package/tests/redactshots.test.ts +0 -216
- package/tests/registernvidiakeys.ts +0 -51
- package/tests/relayserve.test.ts +0 -304
- package/tests/releasenotes.test.ts +0 -276
- package/tests/resilience.test.ts +0 -467
- package/tests/resourceaware.test.ts +0 -187
- package/tests/runbudget.test.ts +0 -125
- package/tests/runhistory.test.ts +0 -299
- package/tests/runreplay.test.ts +0 -113
- package/tests/runresume.test.ts +0 -184
- package/tests/runstate.test.ts +0 -288
- package/tests/runtimeadapters.test.ts +0 -251
- package/tests/safariskeleton.test.ts +0 -115
- package/tests/sandboxframe.test.ts +0 -196
- package/tests/sbom.test.ts +0 -105
- package/tests/secretvault.test.ts +0 -188
- package/tests/selcache.test.ts +0 -111
- package/tests/sessioninterface.test.ts +0 -791
- package/tests/sessions.test.ts +0 -591
- package/tests/siteprefs.test.ts +0 -228
- package/tests/snapshotdelta.test.ts +0 -165
- package/tests/statusviews.test.ts +0 -237
- package/tests/stepmeter.test.ts +0 -200
- package/tests/streamparse.test.ts +0 -163
- package/tests/stylepruning.test.ts +0 -90
- package/tests/surfaces.test.ts +0 -282
- package/tests/sweep.test.ts +0 -78
- package/tests/syncbridge.test.ts +0 -164
- package/tests/taskqueue.test.ts +0 -206
- package/tests/toolcalls.test.ts +0 -336
- package/tests/toolcatalog.test.ts +0 -201
- package/tests/torture-agent.test.ts +0 -706
- package/tests/torture-auth.test.ts +0 -461
- package/tests/torture-commands.test.ts +0 -895
- package/tests/torture-data.test.ts +0 -923
- package/tests/torture-environments.test.ts +0 -285
- package/tests/torture-export.test.ts +0 -263
- package/tests/torture-http.test.ts +0 -1379
- package/tests/torture-llm.test.ts +0 -808
- package/tests/torture-memory.test.ts +0 -1084
- package/tests/torture-plan.test.ts +0 -829
- package/tests/torture-run.test.ts +0 -626
- package/tests/torture-session.test.ts +0 -928
- package/tests/torture-swarm.test.ts +0 -1245
- package/tests/torture-tools.test.ts +0 -964
- package/tests/torture-workflow.test.ts +0 -2354
- package/tests/tourviews.test.ts +0 -52
- package/tests/transparency.test.ts +0 -147
- package/tests/trigger.test.ts +0 -941
- package/tests/v1sunset.test.ts +0 -428
- package/tests/virtlist.test.ts +0 -94
- package/tests/vision.test.ts +0 -540
- package/tests/vsixpack.test.ts +0 -145
- package/tests/webapi.test.ts +0 -523
- package/tests/workflow.test.ts +0 -1785
- package/tests/workfloweditor.test.ts +0 -759
- package/tests/xpipack.test.ts +0 -128
- package/tools.ts +0 -734
- package/types.ts +0 -9908
- package/umd.ts +0 -9
- package/utils.ts +0 -249
- package/version.ts +0 -8
- package/versionregistry.ts +0 -1310
- package/views.ts +0 -2153
- package/vitest.config.ts +0 -12
- package/web/App.tsx +0 -78
- package/web/README.md +0 -47
- package/web/capacitor.config.ts +0 -46
- package/web/const.ts +0 -19
- package/web/control.shell.tsx +0 -66
- package/web/dashboardpage.ts +0 -554
- package/web/db.ts +0 -493
- package/web/design.html +0 -541
- package/web/gatewayview/Gateway.tsx +0 -345
- package/web/gatewayview/config.ts +0 -1042
- package/web/gatewayview/definition.ts +0 -174
- package/web/icons.ts +0 -15
- package/web/optionspage.ts +0 -691
- package/web/popup.ts +0 -1651
- package/web/schema.prisma +0 -176
- package/web/sidepanel.ts +0 -7941
- package/web/sitemanifest.json +0 -13
- package/web/terminal-ui.tsx +0 -622
- package/web/transparencypage.ts +0 -541
- package/web/vite.config.ts +0 -39
- package/web/workspace.ts +0 -23
- package/workbenchmemory.ts +0 -104
- package/workbenchsession.ts +0 -304
- package/workflow.ts +0 -4495
- /package/{dist/bun.d.ts → bun.d.ts} +0 -0
- /package/{dist/cli.d.ts → cli.d.ts} +0 -0
- /package/{dist/companion.d.ts → companion.d.ts} +0 -0
- /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
- /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
- /package/{dist/deno.d.ts → deno.d.ts} +0 -0
- /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
- /package/{dist/headless.d.ts → headless.d.ts} +0 -0
- /package/{dist/http.d.ts → http.d.ts} +0 -0
- /package/{dist/index.d.ts → index.d.ts} +0 -0
- /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
- /package/{dist/memory.d.ts → memory.d.ts} +0 -0
- /package/{dist/node.d.ts → node.d.ts} +0 -0
- /package/{dist/pack.d.ts → pack.d.ts} +0 -0
- /package/{dist/policy.d.ts → policy.d.ts} +0 -0
- /package/{dist/progress.d.ts → progress.d.ts} +0 -0
- /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
- /package/{dist/server.d.ts → server.d.ts} +0 -0
- /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/recovery.ts
DELETED
|
@@ -1,2433 +0,0 @@
|
|
|
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
|
-
}
|