@iislee/opencodex 2.11.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,19 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { atomicWriteFileAsync, getConfigDir, type AtomicWriteAsyncTestSeam } from "../config";
1
5
  import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
2
6
 
3
7
  /**
4
- * Antigravity (Cloud Code Assist) thoughtSignature reasoning-replay cache.
8
+ * Google-family thoughtSignature reasoning-replay cache.
5
9
  *
6
10
  * Gemini-3 interleaved thinking is stateless upstream: each model content part carries a
7
11
  * `thoughtSignature` that MUST be echoed back on the matching part in the next request, or the
8
12
  * upstream rejects the turn (HTTP 400). We observe signatures on the response stream, cache them
9
13
  * per `model + session`, and re-inject them into the outgoing `request.contents` on the next turn.
10
14
  *
11
- * Mirrors CLIProxyAPI `internal/runtime/executor/antigravity_reasoning_replay.go`. Gemini-only;
15
+ * Mirrors CLIProxyAPI `internal/runtime/executor/antigravity_reasoning_replay.go` and is also used
16
+ * by Vertex with a transport/project/location-prefixed model identity. Gemini-only;
12
17
  * Claude-on-Antigravity uses inline signature sanitization instead (see google-antigravity-wire).
13
18
  */
14
19
 
15
20
  interface ReplayCall {
16
21
  signature: string;
22
+ signatures?: string[];
17
23
  sizeBytes: number;
18
24
  touchedAtMs: number;
19
25
  }
@@ -24,9 +30,12 @@ interface ReplayEntry {
24
30
  bytes: number;
25
31
  expiresAtMs: number;
26
32
  oldestAtMs: number | null;
33
+ /** Most recent observe/apply activity; orders sessions under the snapshot cap. */
34
+ lastActiveAtMs: number;
27
35
  }
28
36
 
29
37
  const MIN_SIGNATURE_LEN = 16;
38
+ const MAX_SIGNATURES_PER_CALL = 32;
30
39
  const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
31
40
  export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
32
41
  const REPLAY_EVICT_BATCH = 128;
@@ -34,6 +43,18 @@ const REPLAY_MAX_CALLS_PER_SESSION = 256;
34
43
  export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 2 * 1024 * 1024;
35
44
  export const ANTIGRAVITY_REPLAY_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
36
45
  const REPLAY_MAX_SIGNATURE_BYTES = 64 * 1024;
46
+ /** Fixed 64-hex outer key length, counted once per session entry. */
47
+ const REPLAY_SESSION_KEY_BYTES = 64;
48
+ const REPLAY_SNAPSHOT_FILE = "antigravity-replay.json";
49
+ const REPLAY_SNAPSHOT_VERSION = 1;
50
+ const REPLAY_SNAPSHOT_DEBOUNCE_MS = 2_000;
51
+ /** Upper bound on flush retries: mutations arriving faster than writes complete
52
+ * must not let shutdown hang; the last completed write is already on disk. */
53
+ const REPLAY_FLUSH_MAX_ATTEMPTS = 8;
54
+ /** Write bound for the durable snapshot (mirrors the responses-state cap). */
55
+ const REPLAY_SNAPSHOT_MAX_BYTES = 24 * 1024 * 1024;
56
+ /** Refuse-to-parse ceiling for an existing snapshot file. */
57
+ const REPLAY_SNAPSHOT_REFUSE_BYTES = 32 * 1024 * 1024;
37
58
 
38
59
  interface ReplayLimits {
39
60
  maxCallsPerSession: number;
@@ -53,38 +74,448 @@ let replayLimits = { ...DEFAULT_REPLAY_LIMITS };
53
74
  let replayBytes = 0;
54
75
  let replayOldestSessionKey: string | undefined;
55
76
  let replayOldestAt: number | null = null;
77
+ let replaySnapshotLoaded = false;
78
+ let replaySnapshotPersistTimer: ReturnType<typeof setTimeout> | null = null;
79
+ let replaySnapshotPersistGate: Promise<void> = Promise.resolve();
80
+ /** Mutation generation: bumped on every cache change that needs persisting. */
81
+ let replayMutationGeneration = 0;
82
+ /** Generation of the data the last successful snapshot write actually captured. */
83
+ let replayWrittenGeneration = 0;
84
+ let replaySnapshotWriteSeam: AtomicWriteAsyncTestSeam | undefined;
85
+ let replaySnapshotLoadDiscarded = false;
86
+ let replaySnapshotMaxBytes = REPLAY_SNAPSHOT_MAX_BYTES;
87
+
88
+ function replaySnapshotPath(): string {
89
+ return join(getConfigDir(), REPLAY_SNAPSHOT_FILE);
90
+ }
91
+
92
+ function loadReplaySnapshotEntry(key: string, value: unknown): void {
93
+ if (!/^[0-9a-f]{64}$/.test(key)) return;
94
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
95
+ const rec = value as { byCall?: unknown; expiresAtMs?: unknown; lastActiveAtMs?: unknown };
96
+ if (typeof rec.expiresAtMs !== "number" || !Number.isFinite(rec.expiresAtMs)) return;
97
+ // Expired sessions are dropped at load; a stale snapshot is self-healing.
98
+ if (rec.expiresAtMs <= Date.now()) {
99
+ replaySnapshotLoadDiscarded = true;
100
+ return;
101
+ }
102
+ if (!Array.isArray(rec.byCall)) return;
103
+ const byCall = new Map<string, ReplayCall>();
104
+ let bytes = REPLAY_SESSION_KEY_BYTES;
105
+ for (const pair of rec.byCall) {
106
+ if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
107
+ if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
108
+ const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
109
+ if (!call || typeof call !== "object" || Array.isArray(call)) continue;
110
+ if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
111
+ if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
112
+ let sigs: string[] = [];
113
+ if (Array.isArray(call.signatures)) {
114
+ for (const s of call.signatures) {
115
+ if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
116
+ sigs.push(s);
117
+ }
118
+ }
119
+ }
120
+ if (sigs.length === 0) {
121
+ sigs = [call.signature];
122
+ } else if (!sigs.includes(call.signature)) {
123
+ sigs.push(call.signature);
124
+ }
125
+ if (sigs.length > MAX_SIGNATURES_PER_CALL) {
126
+ sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
127
+ }
128
+ let totalSigBytes = 0;
129
+ let anySigOversized = false;
130
+ for (const s of sigs) {
131
+ const sb = utf8.encode(s).byteLength;
132
+ if (sb > replayLimits.maxSignatureBytes) {
133
+ anySigOversized = true;
134
+ break;
135
+ }
136
+ totalSigBytes += sb;
137
+ }
138
+ if (anySigOversized) continue;
139
+ const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
140
+ if (callBytes > replayLimits.maxBytesPerSession) continue;
141
+ // A duplicated call key would overstate entry.bytes (the map keeps only the
142
+ // last value) and could evict valid sessions; keep the first occurrence.
143
+ if (byCall.has(pair[0])) {
144
+ replaySnapshotLoadDiscarded = true;
145
+ continue;
146
+ }
147
+ byCall.set(pair[0], {
148
+ signature: call.signature,
149
+ signatures: sigs,
150
+ sizeBytes: callBytes,
151
+ touchedAtMs: call.touchedAtMs,
152
+ });
153
+ bytes += callBytes;
154
+ }
155
+ if (byCall.size === 0) {
156
+ replaySnapshotLoadDiscarded = true;
157
+ return;
158
+ }
159
+ const lastActiveAtMs = typeof rec.lastActiveAtMs === "number" && Number.isFinite(rec.lastActiveAtMs)
160
+ ? rec.lastActiveAtMs
161
+ : rec.expiresAtMs;
162
+ const entry: ReplayEntry = { byCall, bytes, expiresAtMs: rec.expiresAtMs, oldestAtMs: null, lastActiveAtMs };
163
+ const loadedCallCount = entry.byCall.size;
164
+ // Account BEFORE trimming: evictInnerCalls decrements the global byte count
165
+ // through deleteReplayCall, so the entry must already be on the books.
166
+ replayCache.set(key, entry);
167
+ replayBytes += entry.bytes;
168
+ // Same per-session caps as live writes, in case limits changed across versions.
169
+ evictInnerCalls(entry);
170
+ if (entry.byCall.size < loadedCallCount) replaySnapshotLoadDiscarded = true;
171
+ if (entry.byCall.size === 0) {
172
+ deleteReplaySession(key);
173
+ return;
174
+ }
175
+ refreshReplaySessionCandidate(key, entry);
176
+ }
177
+
178
+ /**
179
+ * Lazy load of the durable snapshot on first cache access, so signatures observed
180
+ * before a proxy restart are available again once the session id re-derives to the
181
+ * same key (the session id is anchored on the first user message text). Load is
182
+ * best-effort: missing, corrupt, or oversized files start the cache empty.
183
+ */
184
+ function ensureReplaySnapshotLoaded(): void {
185
+ if (replaySnapshotLoaded) return;
186
+ replaySnapshotLoaded = true;
187
+ replaySnapshotLoadDiscarded = false;
188
+ try {
189
+ const path = replaySnapshotPath();
190
+ if (!existsSync(path)) return;
191
+ const stat = statSync(path);
192
+ // Bound the read BEFORE parse: the 24 MiB write cap constrains snapshots this
193
+ // process wrote, not a pre-existing oversized file.
194
+ if (!stat.isFile() || stat.size > REPLAY_SNAPSHOT_REFUSE_BYTES) return;
195
+ const raw = JSON.parse(readFileSync(path, "utf8")) as { version?: unknown; sessions?: unknown };
196
+ if (raw.version !== REPLAY_SNAPSHOT_VERSION || !Array.isArray(raw.sessions)) return;
197
+ for (const entry of raw.sessions) {
198
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") continue;
199
+ loadReplaySnapshotEntry(entry[0], entry[1]);
200
+ }
201
+ } catch {
202
+ // Missing/corrupt snapshot: start empty.
203
+ }
204
+ const sessionsAfterLoad = replayCache.size;
205
+ // Load-time admission must respect the same global caps as live writes.
206
+ evictIfNeeded();
207
+ if (replayCache.size < sessionsAfterLoad) replaySnapshotLoadDiscarded = true;
208
+ // Persist the cleaned state once: expired/over-limit sessions must not stay
209
+ // on disk and be re-parsed (and re-dropped) after every restart.
210
+ if (replaySnapshotLoadDiscarded) markReplayDirty();
211
+ enforceAppOwnedMemoryBudget();
212
+ }
213
+
214
+ function markReplayDirty(): void {
215
+ replayMutationGeneration += 1;
216
+ if (replaySnapshotPersistTimer) return;
217
+ replaySnapshotPersistTimer = setTimeout(() => {
218
+ void persistReplaySnapshotNow().catch(() => {
219
+ // Redacted static message only: never echo the underlying error, which
220
+ // can carry paths or other environment details.
221
+ console.warn("[antigravity] replay snapshot persist failed; cached signatures will not survive a restart");
222
+ });
223
+ }, REPLAY_SNAPSHOT_DEBOUNCE_MS);
224
+ (replaySnapshotPersistTimer as { unref?: () => void }).unref?.();
225
+ }
226
+
227
+ async function persistReplaySnapshotNow(): Promise<void> {
228
+ if (replaySnapshotPersistTimer) {
229
+ clearTimeout(replaySnapshotPersistTimer);
230
+ replaySnapshotPersistTimer = null;
231
+ }
232
+ // Serialize writers so a flush and a debounced write cannot race on temps/ACL.
233
+ const previous = replaySnapshotPersistGate;
234
+ let release!: () => void;
235
+ replaySnapshotPersistGate = new Promise<void>(resolve => { release = resolve; });
236
+ await previous;
237
+ const writeGeneration = replayMutationGeneration;
238
+ try {
239
+ const sessions: Array<[string, unknown]> = [];
240
+ // Account for the bytes actually written, not just the entries: the document
241
+ // is `{"version":N,"sessions":[...]}`, so the framing and the comma between
242
+ // entries count against the cap too. Summing per-entry sizes alone let the
243
+ // written file exceed replaySnapshotMaxBytes by a margin that grew with every
244
+ // additional session.
245
+ const prefix = `{"version":${JSON.stringify(REPLAY_SNAPSHOT_VERSION)},"sessions":[`;
246
+ const suffix = "]}";
247
+ const serialized: string[] = [];
248
+ let total = Buffer.byteLength(prefix, "utf8") + Buffer.byteLength(suffix, "utf8");
249
+ // Most-recently-active first so the sessions that survive the snapshot
250
+ // byte cap are the ones actually in use (Map order is insertion order).
251
+ for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
252
+ const byCall = [...entry.byCall].map(([callKey, call]) => [
253
+ callKey,
254
+ {
255
+ signature: call.signature,
256
+ ...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
257
+ touchedAtMs: call.touchedAtMs,
258
+ },
259
+ ]);
260
+ const persistEntry: [string, unknown] = [key, {
261
+ byCall,
262
+ expiresAtMs: entry.expiresAtMs,
263
+ lastActiveAtMs: entry.lastActiveAtMs,
264
+ }];
265
+ const encoded = JSON.stringify(persistEntry);
266
+ const size = Buffer.byteLength(encoded, "utf8") + (serialized.length > 0 ? 1 : 0);
267
+ if (total + size > replaySnapshotMaxBytes) break;
268
+ total += size;
269
+ serialized.push(encoded);
270
+ sessions.push(persistEntry);
271
+ }
272
+ serialized.reverse();
273
+ const document = `${prefix}${serialized.join(",")}${suffix}`;
274
+ // Defensive: the admitted entries are what we serialize, so this can only
275
+ // trip if the accounting above and the payload below ever drift apart.
276
+ if (Buffer.byteLength(document, "utf8") > replaySnapshotMaxBytes) {
277
+ throw new Error("Antigravity replay snapshot exceeded its configured byte cap.");
278
+ }
279
+ mkdirSync(dirname(replaySnapshotPath()), { recursive: true, mode: 0o700 });
280
+ try { chmodSync(dirname(replaySnapshotPath()), 0o700); } catch { /* best-effort (e.g. Windows) */ }
281
+ await atomicWriteFileAsync(
282
+ replaySnapshotPath(),
283
+ document,
284
+ undefined,
285
+ replaySnapshotWriteSeam,
286
+ );
287
+ replayWrittenGeneration = writeGeneration;
288
+ } finally {
289
+ release();
290
+ }
291
+ }
292
+
293
+ /** Flush any pending debounced snapshot write (graceful shutdown / deterministic tests). */
294
+ export async function flushAntigravityReplay(): Promise<void> {
295
+ // Persist until the durable generation catches up with the latest mutation:
296
+ // a change that lands while a writer is in flight must not be lost when
297
+ // shutdown exits right after the first write completes.
298
+ for (let attempt = 0; attempt < REPLAY_FLUSH_MAX_ATTEMPTS; attempt += 1) {
299
+ if (replaySnapshotPersistTimer || replayMutationGeneration > replayWrittenGeneration) {
300
+ await persistReplaySnapshotNow();
301
+ }
302
+ // No pending timer: still await any in-flight write so shutdown does not race it.
303
+ await replaySnapshotPersistGate;
304
+ if (replayMutationGeneration === replayWrittenGeneration && replaySnapshotPersistTimer === null) return;
305
+ }
306
+ // Budget exhausted without convergence. Resolving here would tell
307
+ // drainAndShutdown() the snapshot is durable while the latest thought
308
+ // signature may never have reached disk, so shutdown diagnostics would claim
309
+ // a durability we cannot demonstrate. Reject instead, with fixed text: the
310
+ // shutdown path logs this message, so it must not carry session, model, or
311
+ // signature detail.
312
+ throw new Error("Antigravity replay snapshot flush did not converge.");
313
+ }
314
+
315
+ /**
316
+ * Fixed-size identity for a (model, sessionId) pair: SHA-256 over
317
+ * length-prefixed UTF-16 code units fed incrementally (no separator ambiguity
318
+ * — `("a\0b","c")` and `("a","b\0c")` derive different keys — and no raw
319
+ * model/session strings retained as Map keys, which the byte caps never
320
+ * counted).
321
+ */
322
+ /**
323
+ * Injective string feed for key derivation: length-prefixed in CODE UNITS,
324
+ * then each code unit as two little-endian bytes. TextEncoder/UTF-8 would
325
+ * fold lone surrogates into U+FFFD, colliding distinct strings (e.g.
326
+ * "�" and "�") into the same key.
327
+ */
328
+ function updateHashWithString(hash: ReturnType<typeof createHash>, value: string): void {
329
+ hash.update(String(value.length));
330
+ hash.update("\0");
331
+ const buf = Buffer.allocUnsafe(8192);
332
+ let offset = 0;
333
+ for (let index = 0; index < value.length; index += 1) {
334
+ buf.writeUInt16LE(value.charCodeAt(index), offset);
335
+ offset += 2;
336
+ if (offset === buf.length) {
337
+ hash.update(buf);
338
+ offset = 0;
339
+ }
340
+ }
341
+ if (offset > 0) hash.update(buf.subarray(0, offset));
342
+ }
56
343
 
57
344
  function replayKey(model: string, sessionId: string): string {
58
- return `${model}::session:${sessionId}`;
345
+ const hash = createHash("sha256");
346
+ updateHashWithString(hash, model);
347
+ updateHashWithString(hash, sessionId);
348
+ return hash.digest("hex");
349
+ }
350
+
351
+ /** Canonical output exceeding this budget is rejected DURING the walk — the
352
+ * pre-fix path materialized an unbounded canonical string before admission. */
353
+ const REPLAY_MAX_CANONICAL_ARGS_BYTES = 64 * 1024;
354
+ const CANONICAL_OVERFLOW = Symbol("canonical-overflow");
355
+
356
+ /** Byte-identical output to the old recursive canonicalJson, written incrementally. */
357
+ /**
358
+ * Key-count pre-check: every object key costs at least 4 canonical bytes
359
+ * (two quotes, colon, separator), so a wider object ALWAYS overflows the
360
+ * canonical budget — skip the sort and the walk. Object.keys allocation
361
+ * itself is linear and irreducible in JS (there is no streaming key API),
362
+ * but it is transient and never sorted or walked past this bound.
363
+ */
364
+ const CANONICAL_MAX_KEYS_PER_OBJECT = REPLAY_MAX_CANONICAL_ARGS_BYTES / 4;
365
+
366
+ /** Test-only scan instrumentation: proves overflow aborts the walk near the
367
+ * cap instead of scanning/materializing the whole input. */
368
+ let canonicalScanUnitsForTests = 0;
369
+ export function canonicalScanUnitsForTestsValue(): number {
370
+ return canonicalScanUnitsForTests;
371
+ }
372
+ export function resetCanonicalScanUnitsForTests(): void {
373
+ canonicalScanUnitsForTests = 0;
374
+ }
375
+
376
+ const MAX_CANONICAL_DEPTH = 128;
377
+
378
+ function writeCanonicalJson(value: unknown, sink: (chunk: string) => void, depth = 0): void {
379
+ canonicalScanUnitsForTests += 1;
380
+ // Depth overflow is the same class as byte overflow: skip replay for this
381
+ // call instead of exhausting the stack on a pathological argument shape.
382
+ if (depth > MAX_CANONICAL_DEPTH) throw CANONICAL_OVERFLOW;
383
+ if (typeof value === "string") {
384
+ writeJsonStringEscaped(value, sink);
385
+ return;
386
+ }
387
+ if (value === null || typeof value !== "object") {
388
+ sink(JSON.stringify(value) ?? "null");
389
+ return;
390
+ }
391
+ if (Array.isArray(value)) {
392
+ sink("[");
393
+ for (let index = 0; index < value.length; index += 1) {
394
+ if (index > 0) sink(",");
395
+ // Array.prototype.map parity: holes produce NOTHING between the commas
396
+ // (old output `[1,,3]`), while an explicit undefined element is "null".
397
+ if (index in value) writeCanonicalJson(value[index], sink, depth + 1);
398
+ }
399
+ sink("]");
400
+ return;
401
+ }
402
+ const keys = Object.keys(value as Record<string, unknown>);
403
+ if (keys.length > CANONICAL_MAX_KEYS_PER_OBJECT) throw CANONICAL_OVERFLOW;
404
+ keys.sort();
405
+ sink("{");
406
+ keys.forEach((k, index) => {
407
+ if (index > 0) sink(",");
408
+ writeJsonStringEscaped(k, sink);
409
+ sink(":");
410
+ writeCanonicalJson((value as Record<string, unknown>)[k], sink, depth + 1);
411
+ });
412
+ sink("}");
59
413
  }
60
414
 
61
- /** Recursively canonicalize a JSON value: object keys sorted, arrays preserved. */
62
- function canonicalJson(value: unknown): string {
63
- if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
64
- if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
65
- const entries = Object.keys(value as Record<string, unknown>).sort()
66
- .map(k => `${JSON.stringify(k)}:${canonicalJson((value as Record<string, unknown>)[k])}`);
67
- return `{${entries.join(",")}}`;
415
+ /**
416
+ * JSON.stringify string escaping, streamed in small chunks so the budget can
417
+ * reject mid-string calling JSON.stringify on a multi-MiB primitive would
418
+ * materialize its full escaped form before the sink could refuse it.
419
+ * Semantics mirror JSON.stringify for strings exactly (ES2019 JSON
420
+ * superset): quotes/backslash and control characters are escaped, LONE
421
+ * surrogates become \uXXXX, and valid surrogate pairs pass through raw.
422
+ */
423
+ function writeJsonStringEscaped(value: string, sink: (chunk: string) => void): void {
424
+ sink('"');
425
+ let buffer = "";
426
+ for (const cp of value) {
427
+ canonicalScanUnitsForTests += 1;
428
+ const code = cp.codePointAt(0)!;
429
+ let escaped: string;
430
+ if (cp === '"') escaped = '\\"';
431
+ else if (cp === "\\") escaped = "\\\\";
432
+ else if (cp === "\b") escaped = "\\b";
433
+ else if (cp === "\f") escaped = "\\f";
434
+ else if (cp === "\n") escaped = "\\n";
435
+ else if (cp === "\r") escaped = "\\r";
436
+ else if (cp === "\t") escaped = "\\t";
437
+ else if (code < 0x20) escaped = `\\u${code.toString(16).padStart(4, "0")}`;
438
+ // Lone surrogates: JSON.stringify emits \uXXXX (a raw one would decode
439
+ // back as U+FFFD and collide with real U+FFFD content).
440
+ else if (code >= 0xd800 && code <= 0xdfff) escaped = `\\u${code.toString(16).padStart(4, "0")}`;
441
+ else escaped = cp;
442
+ buffer += escaped;
443
+ if (buffer.length >= 4096) {
444
+ sink(buffer);
445
+ buffer = "";
446
+ }
447
+ }
448
+ if (buffer.length > 0) sink(buffer);
449
+ sink('"');
68
450
  }
69
451
 
70
- /** Stable identity for a functionCall part: name + recursively canonicalized args. */
452
+ /** Bounded canonicalization: null on overflow (skip replay for that call). */
453
+ function canonicalJsonBounded(value: unknown, maxBytes: number): string | null {
454
+ let written = 0;
455
+ const parts: string[] = [];
456
+ const sink = (chunk: string) => {
457
+ written += utf8.encode(chunk).byteLength;
458
+ if (written > maxBytes) throw CANONICAL_OVERFLOW;
459
+ parts.push(chunk);
460
+ };
461
+ try {
462
+ writeCanonicalJson(value, sink);
463
+ } catch (error) {
464
+ if (error === CANONICAL_OVERFLOW) return null;
465
+ throw error;
466
+ }
467
+ return parts.join("");
468
+ }
469
+
470
+ /**
471
+ * Stable identity for a functionCall part: fixed-size SHA-256 over
472
+ * length-prefixed name + canonical args. Overflow during canonicalization
473
+ * skips replay for that call (never materializes an unbounded string); other
474
+ * canonicalization failures keep the old name-only fallback semantics.
475
+ */
71
476
  function functionCallKey(name: unknown, args: unknown): string | undefined {
72
477
  if (typeof name !== "string" || name.length === 0) return undefined;
73
- let argsKey = "";
478
+ let canonical: string | null;
74
479
  try {
75
- argsKey = canonicalJson(args ?? {});
480
+ canonical = canonicalJsonBounded(args ?? {}, REPLAY_MAX_CANONICAL_ARGS_BYTES);
76
481
  } catch {
77
- argsKey = "";
482
+ canonical = "";
78
483
  }
79
- return `${name}::${argsKey}`;
484
+ if (canonical === null) return undefined;
485
+ const hash = createHash("sha256");
486
+ updateHashWithString(hash, name);
487
+ updateHashWithString(hash, canonical);
488
+ return hash.digest("hex");
489
+ }
490
+
491
+ /** Test-only key-derivation seam: the fixed-key regression cannot go red
492
+ * through snapshot.bytes (that metric never counted raw outer keys). */
493
+ export function antigravityReplayKeyForTests(model: string, sessionId: string): string {
494
+ return replayKey(model, sessionId);
495
+ }
496
+
497
+ export function antigravityFunctionCallKeyForTests(name: unknown, args: unknown): string | undefined {
498
+ return functionCallKey(name, args);
499
+ }
500
+
501
+ /** Test-only bounded-canonicalization seam: proves mid-walk rejection without
502
+ * materializing the escaped form (allocation guard). */
503
+ export function antigravityCanonicalJsonBoundedForTests(value: unknown, maxBytes: number): string | null {
504
+ return canonicalJsonBounded(value, maxBytes);
505
+ }
506
+
507
+ /** Test-only: the ACTUAL internal session keys, so tests can prove raw
508
+ * model/session strings are never retained as Map keys. */
509
+ export function antigravityReplaySessionKeysForTests(): string[] {
510
+ return [...replayCache.keys()];
80
511
  }
81
512
 
82
513
  function extractSignature(part: Record<string, unknown>): string | undefined {
83
514
  const direct = part.thoughtSignature ?? part.thought_signature;
84
- if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
515
+ if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN && direct !== THOUGHT_SIGNATURE_BYPASS) return direct;
85
516
  const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
86
517
  const nested = extra?.google?.thought_signature;
87
- if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
518
+ if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN && nested !== THOUGHT_SIGNATURE_BYPASS) return nested;
88
519
  return undefined;
89
520
  }
90
521
 
@@ -120,7 +551,31 @@ function refreshReplaySessionCandidate(key: string, entry: ReplayEntry): void {
120
551
  }
121
552
 
122
553
  function deleteExpiredReplaySessions(now: number): void {
123
- for (const [key, entry] of replayCache) if (entry.expiresAtMs <= now) deleteReplaySession(key);
554
+ let deleted = false;
555
+ for (const [key, entry] of replayCache) {
556
+ if (entry.expiresAtMs > now) continue;
557
+ deleteReplaySession(key);
558
+ deleted = true;
559
+ }
560
+ // Expiry is a durable mutation too: rewrite the snapshot so opaque thought
561
+ // signatures do not remain at rest after their in-memory TTL has elapsed.
562
+ if (deleted) markReplayDirty();
563
+ }
564
+
565
+ /**
566
+ * The lazy per-call expiry scan is O(sessions); at the 10,240-session cap
567
+ * every observe/apply would rescan the whole map — O(n²) under load. The 60s
568
+ * state-store sweeper is already the periodic expiry authority, so lazy scans
569
+ * are throttled to at most one per interval (expired entries may linger a few
570
+ * extra seconds; TTL is fuzzy at that scale by design).
571
+ */
572
+ const LAZY_SWEEP_INTERVAL_MS = 30_000;
573
+ let lastLazySweepAt = Number.NEGATIVE_INFINITY;
574
+
575
+ function deleteExpiredReplaySessionsThrottled(now: number): void {
576
+ if (now - lastLazySweepAt < LAZY_SWEEP_INTERVAL_MS) return;
577
+ lastLazySweepAt = now;
578
+ deleteExpiredReplaySessions(now);
124
579
  }
125
580
 
126
581
  export function sweepExpiredAntigravityReplay(now = Date.now()): number {
@@ -169,48 +624,183 @@ export function antigravityUsesReplayCache(model: string): boolean {
169
624
  return !/claude/i.test(model);
170
625
  }
171
626
 
627
+ /**
628
+ * Gemini 3 rejects a turn whose FIRST functionCall part carries no thought signature. When
629
+ * neither the wire metadata nor the replay cache can supply a real one, this is the official
630
+ * validator-bypass token.
631
+ */
632
+ const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
633
+
634
+ /**
635
+ * True when the model speaks the Gemini wire dialect that requires a thought signature on the
636
+ * first functionCall of a turn — and therefore accepts the validator-bypass sentinel.
637
+ *
638
+ * Deliberately NOT `antigravityUsesReplayCache`. That predicate is broad on purpose (every
639
+ * non-Claude model participates in signature replay), and reusing it for the sentinel is how a
640
+ * Gemini-only control token was observed being injected into `gpt-oss-120b-medium`. Replaying a
641
+ * signature upstream gave us is harmless for any model; *fabricating* a Gemini token is not.
642
+ *
643
+ * The identity must be REDUCED to its model component before matching, not scanned whole. The
644
+ * Vertex replay key is built in `src/adapters/google.ts` as
645
+ * `vertex:<project>:<location>:<modelId>`, and the project id is operator-chosen: a project
646
+ * named `gemini-prod` made a whole-string scan return true for
647
+ * `vertex:gemini-prod:global:gpt-oss-120b`, arming the Gemini-only sentinel for a non-Gemini
648
+ * model — the exact class of defect this predicate exists to prevent, reintroduced one layer up.
649
+ *
650
+ * So: take the last `:` segment for a Vertex identity, then the last `/` segment for a
651
+ * namespaced id (`google/gemini-3-pro`), and match only that. The trailing `[-.\d]` keeps
652
+ * `geminibot` and `my-gemini-clone` out. A model outside this set that genuinely needs the
653
+ * sentinel must arrive with a captured accepted CCA contract, not by widening this predicate.
654
+ */
655
+ export function antigravitySupportsThoughtSignatureSentinel(model: string): boolean {
656
+ const afterTransport = model.slice(model.lastIndexOf(":") + 1);
657
+ const wireModel = afterTransport.slice(afterTransport.lastIndexOf("/") + 1);
658
+ return /^gemini[-.\d]/i.test(wireModel);
659
+ }
660
+
661
+ /**
662
+ * Ensure every model turn's FIRST functionCall carries a thought signature, injecting the
663
+ * validator-bypass sentinel only where one is genuinely absent.
664
+ *
665
+ * Split out of `applyAntigravityReplay` on purpose. Replay answers "what did upstream already
666
+ * tell us about this call", and its absence of a signature is meaningful — 18 assertions in the
667
+ * suite read `thoughtSignature === undefined` as "the cache did not match", covering eviction,
668
+ * TTL expiry, oversize refusal and clear-on-invalid. Folding a fabricated token into that
669
+ * function would overwrite the very signal those tests read. Keeping the sentinel as its own
670
+ * pass means a cache miss still looks like a cache miss.
671
+ *
672
+ * Three properties this must hold, each of which a naive presence-check gets wrong:
673
+ * - it decides from `extractSignature`, so a valid NESTED
674
+ * `extra_content.google.thought_signature` counts as signed (no competing sentinel) and a
675
+ * present-but-too-short value does not (the fallback still fires);
676
+ * - it looks at the FIRST functionCall only, so a later sibling receiving a cached signature
677
+ * cannot vote away the sentinel the first call requires;
678
+ * - it is gated on the Gemini wire dialect, not on replay-cache participation.
679
+ */
680
+ export function applyAntigravityThoughtSignatureFallback(model: string, contents: unknown[]): unknown[] {
681
+ if (!antigravitySupportsThoughtSignatureSentinel(model) || !Array.isArray(contents)) return contents;
682
+ for (const rawContent of contents as { role?: string; parts?: unknown[] }[]) {
683
+ if (!rawContent || typeof rawContent !== "object" || rawContent.role !== "model") continue;
684
+ if (!Array.isArray(rawContent.parts)) continue;
685
+ for (const rawPart of rawContent.parts) {
686
+ if (!rawPart || typeof rawPart !== "object") continue;
687
+ const part = rawPart as Record<string, unknown>;
688
+ if (!part.functionCall) continue;
689
+ if (!extractSignature(part)) part.thoughtSignature = THOUGHT_SIGNATURE_BYPASS;
690
+ break;
691
+ }
692
+ }
693
+ return contents;
694
+ }
695
+
172
696
  /**
173
697
  * Observe a parsed CCA chunk's `candidates[0].content.parts` and record thought signatures keyed by
174
698
  * the functionCall identity (name + args). Accumulates across the whole session so a sequential
175
699
  * multi-step tool loop keeps EVERY prior call's signature, not just the latest part-index slot.
700
+ * A signature on a standalone thought part applies to the functionCall parts that follow it in
701
+ * the same array AND to later arrays of the same turn: streaming splits a thought part and its
702
+ * calls across SSE chunks, so `carriedThoughtSig` threads the still-unpaired signature from the
703
+ * previous chunk and the return value hands the remainder to the next one (#897, #2125). A call's
704
+ * own signature always takes precedence over a carried one.
176
705
  * `parts` is the already-unwrapped `response.candidates[0].content.parts`.
177
706
  */
178
- export function observeAntigravityReplay(model: string, sessionId: string, parts: unknown[]): void {
179
- if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return;
707
+ export function observeAntigravityReplay(
708
+ model: string,
709
+ sessionId: string,
710
+ parts: unknown[],
711
+ carriedThoughtSig?: string,
712
+ ): string | undefined {
713
+ if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return carriedThoughtSig;
714
+ ensureReplaySnapshotLoaded();
180
715
  const now = Date.now();
181
- deleteExpiredReplaySessions(now);
716
+ deleteExpiredReplaySessionsThrottled(now);
182
717
  const key = replayKey(model, sessionId);
183
- const entry = replayCache.get(key) ?? {
718
+ const existing = replayCache.get(key);
719
+ const entry = existing ?? {
184
720
  byCall: new Map<string, ReplayCall>(),
185
- bytes: 0,
721
+ bytes: REPLAY_SESSION_KEY_BYTES,
186
722
  expiresAtMs: 0,
187
723
  oldestAtMs: null,
724
+ lastActiveAtMs: 0,
188
725
  };
189
726
  let inserted = false;
727
+ let pendingThoughtSig: string | undefined = carriedThoughtSig;
190
728
  for (const raw of parts) {
191
729
  if (!raw || typeof raw !== "object") continue;
192
730
  const part = raw as Record<string, unknown>;
193
731
  const sig = extractSignature(part);
194
- if (!sig) continue;
195
732
  const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
196
- const ck = fc ? functionCallKey(fc.name, fc.args) : undefined;
733
+ if (!fc) {
734
+ // A signature on a standalone thought part pairs with the NEXT functionCall in this
735
+ // array: thought parts are stripped from replayed history, so the call part is the only
736
+ // carrier that survives (#897). Non-thought parts keep their own signature in history.
737
+ if (sig && part.thought === true) pendingThoughtSig = sig;
738
+ continue;
739
+ }
740
+ const callSig = sig ?? pendingThoughtSig; // a signature on the call part itself wins
741
+ if (!callSig) continue;
742
+ const ck = functionCallKey(fc.name, fc.args);
197
743
  if (!ck) continue; // only function-call signatures are replayable by identity
198
- const signatureBytes = utf8.encode(sig).byteLength;
199
- const sizeBytes = utf8.encode(ck).byteLength + signatureBytes;
200
- if (signatureBytes > replayLimits.maxSignatureBytes || sizeBytes > replayLimits.maxBytesPerSession) continue;
744
+ const signatureBytes = utf8.encode(callSig).byteLength;
745
+ if (signatureBytes > replayLimits.maxSignatureBytes) continue;
746
+
747
+ const existingCall = entry.byCall.get(ck);
748
+ let sigs: string[];
749
+ if (existingCall) {
750
+ sigs = existingCall.signatures && existingCall.signatures.length > 0
751
+ ? [...existingCall.signatures]
752
+ : [existingCall.signature];
753
+ if (!sigs.includes(callSig)) {
754
+ if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
755
+ sigs.shift();
756
+ }
757
+ sigs.push(callSig);
758
+ }
759
+ } else {
760
+ sigs = [callSig];
761
+ }
762
+ let totalSigBytes = 0;
763
+ for (const s of sigs) {
764
+ totalSigBytes += utf8.encode(s).byteLength;
765
+ }
766
+ const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
767
+ if (sizeBytes > replayLimits.maxBytesPerSession) continue;
768
+
201
769
  deleteReplayCall(entry, ck);
202
- entry.byCall.set(ck, { signature: sig, sizeBytes, touchedAtMs: now });
770
+ entry.byCall.set(ck, {
771
+ signature: callSig,
772
+ signatures: sigs,
773
+ sizeBytes,
774
+ touchedAtMs: now,
775
+ });
203
776
  entry.bytes += sizeBytes;
204
777
  replayBytes += sizeBytes;
205
778
  inserted = true;
206
779
  }
207
- if (!inserted) return;
780
+ if (!inserted) return pendingThoughtSig;
781
+ // Charge the fixed outer key only when the session is actually stored.
782
+ if (!existing) replayBytes += REPLAY_SESSION_KEY_BYTES;
208
783
  evictInnerCalls(entry);
784
+ if (entry.byCall.size === 0) {
785
+ // The fixed session overhead can exceed the per-session cap on its own
786
+ // (test-sized limits): an entry holding zero calls is unusable — drop it
787
+ // instead of retaining an unevictable shell.
788
+ if (existing) {
789
+ deleteReplaySession(key);
790
+ markReplayDirty();
791
+ } else {
792
+ replayBytes -= REPLAY_SESSION_KEY_BYTES;
793
+ }
794
+ return pendingThoughtSig;
795
+ }
209
796
  entry.expiresAtMs = now + REPLAY_TTL_MS;
797
+ entry.lastActiveAtMs = now;
210
798
  replayCache.set(key, entry);
211
799
  refreshReplaySessionCandidate(key, entry);
212
800
  evictIfNeeded();
213
801
  enforceAppOwnedMemoryBudget();
802
+ markReplayDirty();
803
+ return pendingThoughtSig;
214
804
  }
215
805
 
216
806
  /**
@@ -220,38 +810,94 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
220
810
  */
221
811
  export function applyAntigravityReplay(model: string, sessionId: string, contents: unknown[]): unknown[] {
222
812
  if (!antigravityUsesReplayCache(model) || !Array.isArray(contents)) return contents;
813
+ ensureReplaySnapshotLoaded();
223
814
  const now = Date.now();
224
- deleteExpiredReplaySessions(now);
815
+ deleteExpiredReplaySessionsThrottled(now);
225
816
  const entry = replayCache.get(replayKey(model, sessionId));
226
817
  if (!entry) {
227
818
  return contents;
228
819
  }
820
+
229
821
  let touched = false;
230
- for (const c of contents as { role?: string; parts?: unknown[] }[]) {
822
+ // Align from the END of the recorded signature list. History may have been truncated
823
+ // (compaction / previous_response_id): the last remaining occurrence is the most recent call,
824
+ // so it must receive the newest signature. Iterate backwards and count every occurrence
825
+ // (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
826
+ const reverseOccurrence = new Map<string, number>();
827
+ for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
828
+ const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
231
829
  if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
232
- for (const raw of c.parts) {
830
+ for (let pi = c.parts.length - 1; pi >= 0; pi--) {
831
+ const raw = c.parts[pi];
233
832
  if (!raw || typeof raw !== "object") continue;
234
833
  const part = raw as Record<string, unknown>;
235
834
  const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
236
835
  if (!fc) continue;
237
- if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
238
836
  const ck = functionCallKey(fc.name, fc.args);
239
- const call = ck ? entry.byCall.get(ck) : undefined;
240
- if (call && ck) {
837
+ let call = ck ? entry.byCall.get(ck) : undefined;
838
+ let matchedKey = ck;
839
+ if (!call && typeof fc.name === "string" && typeof fc.args === "object" && fc.args !== null) {
840
+ // Freeform / custom tool replay unwrap:
841
+ // The client replays custom_tool_call with arguments: { input: "..." }.
842
+ // Upstream was invoked with args: { input: "..." } or raw string or parsed JSON.
843
+ const argsObj = fc.args as Record<string, unknown>;
844
+ if (typeof argsObj.input === "string") {
845
+ const trimmedInput = argsObj.input.trim();
846
+ if (
847
+ trimmedInput.length <= REPLAY_MAX_CANONICAL_ARGS_BYTES
848
+ && utf8.encode(trimmedInput).byteLength <= REPLAY_MAX_CANONICAL_ARGS_BYTES
849
+ ) {
850
+ try {
851
+ const parsedInput = JSON.parse(trimmedInput);
852
+ if (parsedInput && typeof parsedInput === "object") {
853
+ const altKey = functionCallKey(fc.name, parsedInput);
854
+ if (altKey && entry.byCall.has(altKey)) {
855
+ call = entry.byCall.get(altKey);
856
+ matchedKey = altKey;
857
+ }
858
+ }
859
+ } catch {
860
+ // not JSON, keep default
861
+ }
862
+ }
863
+ }
864
+ }
865
+ if (matchedKey) {
866
+ const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
867
+ reverseOccurrence.set(matchedKey, revIdx + 1);
868
+ if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
869
+ // Already signed (e.g. Mechanism ① or upstream response), preserve it.
870
+ continue;
871
+ }
872
+ if (call) {
873
+ const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
874
+ const chosenSig = revIdx < sigs.length
875
+ ? sigs[sigs.length - 1 - revIdx]
876
+ : sigs[sigs.length - 1] ?? call.signature;
877
+ part.thoughtSignature = chosenSig;
878
+ entry.byCall.delete(matchedKey);
879
+ entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
880
+ touched = true;
881
+ }
882
+ } else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
241
883
  part.thoughtSignature = call.signature;
242
- entry.byCall.delete(ck);
243
- entry.byCall.set(ck, { ...call, touchedAtMs: now });
244
884
  touched = true;
245
885
  }
246
886
  }
247
887
  }
248
- if (touched) refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
888
+
889
+ if (touched) {
890
+ entry.lastActiveAtMs = now;
891
+ refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
892
+ markReplayDirty();
893
+ }
249
894
  return contents;
250
895
  }
251
896
 
252
897
  /** Drop the cache entry when upstream rejects a signature (clear-on-invalid). */
253
898
  export function clearAntigravityReplay(model: string, sessionId: string): void {
254
- deleteReplaySession(replayKey(model, sessionId));
899
+ ensureReplaySnapshotLoaded();
900
+ if (deleteReplaySession(replayKey(model, sessionId)) > 0) markReplayDirty();
255
901
  }
256
902
 
257
903
  export function antigravityReplayMetrics(): {
@@ -260,6 +906,7 @@ export function antigravityReplayMetrics(): {
260
906
  totalBytes: number;
261
907
  largestSessionBytes: number;
262
908
  } {
909
+ ensureReplaySnapshotLoaded();
263
910
  let calls = 0;
264
911
  let largestSessionBytes = 0;
265
912
  for (const entry of replayCache.values()) {
@@ -276,6 +923,7 @@ export function antigravityReplayRetainedStoreSnapshot(): {
276
923
  pinnedBytes: number;
277
924
  oldestAt: number | null;
278
925
  } {
926
+ ensureReplaySnapshotLoaded();
279
927
  return {
280
928
  count: replayCache.size,
281
929
  bytes: replayBytes,
@@ -286,7 +934,10 @@ export function antigravityReplayRetainedStoreSnapshot(): {
286
934
  }
287
935
 
288
936
  export function evictOldestAntigravityReplayForBudget(): number {
289
- return replayOldestSessionKey === undefined ? 0 : deleteReplaySession(replayOldestSessionKey);
937
+ ensureReplaySnapshotLoaded();
938
+ const removed = replayOldestSessionKey === undefined ? 0 : deleteReplaySession(replayOldestSessionKey);
939
+ if (removed > 0) markReplayDirty();
940
+ return removed;
290
941
  }
291
942
 
292
943
  export function setAntigravityReplayLimitsForTests(limits?: Partial<ReplayLimits>): void {
@@ -294,10 +945,31 @@ export function setAntigravityReplayLimitsForTests(limits?: Partial<ReplayLimits
294
945
  replayLimits = limits ? { ...DEFAULT_REPLAY_LIMITS, ...limits } : { ...DEFAULT_REPLAY_LIMITS };
295
946
  }
296
947
 
948
+ /** Test-only write seam (mirrors AtomicWriteAsyncTestSeam usage elsewhere). */
949
+ export function setAntigravityReplayWriteSeamForTests(seam: AtomicWriteAsyncTestSeam | undefined): void {
950
+ replaySnapshotWriteSeam = seam;
951
+ }
952
+
953
+ /** Test-only snapshot byte cap (mirrors the limits seam). */
954
+ export function setAntigravityReplaySnapshotMaxBytesForTests(maxBytes: number): void {
955
+ replaySnapshotMaxBytes = maxBytes;
956
+ }
957
+
297
958
  /** Test seam. */
298
959
  export function __resetAntigravityReplayCache(): void {
960
+ if (replaySnapshotPersistTimer) {
961
+ clearTimeout(replaySnapshotPersistTimer);
962
+ replaySnapshotPersistTimer = null;
963
+ }
964
+ replaySnapshotLoaded = false;
965
+ lastLazySweepAt = Number.NEGATIVE_INFINITY;
299
966
  replayCache.clear();
300
967
  replayBytes = 0;
301
968
  replayOldestSessionKey = undefined;
302
969
  replayOldestAt = null;
970
+ replayMutationGeneration = 0;
971
+ replayWrittenGeneration = 0;
972
+ replaySnapshotWriteSeam = undefined;
973
+ replaySnapshotLoadDiscarded = false;
974
+ replaySnapshotMaxBytes = REPLAY_SNAPSHOT_MAX_BYTES;
303
975
  }