@remodex/rmx 1.0.2

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 (588) hide show
  1. package/AGENTS_INSTALL.md +91 -0
  2. package/LICENSE +21 -0
  3. package/README.md +242 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +584 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-CZqebSPQ.css +1 -0
  11. package/gui/dist/assets/index-CkETtt7P.js +71 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
  14. package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
  15. package/gui/dist/icons.svg +24 -0
  16. package/gui/dist/index.html +25 -0
  17. package/gui/dist/logo.png +0 -0
  18. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  19. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  20. package/gui/dist/provider-icons/claude-color.svg +1 -0
  21. package/gui/dist/provider-icons/cline-color.svg +16 -0
  22. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  23. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  24. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  25. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  26. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  27. package/gui/dist/provider-icons/discord.svg +1 -0
  28. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  29. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  30. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  31. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  32. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  33. package/gui/dist/provider-icons/grok.svg +1 -0
  34. package/gui/dist/provider-icons/groq-color.svg +1 -0
  35. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  36. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  37. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  38. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  39. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  40. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  41. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  42. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  43. package/gui/dist/provider-icons/openai.svg +1 -0
  44. package/gui/dist/provider-icons/opencode.svg +2 -0
  45. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  46. package/gui/dist/provider-icons/pi.svg +21 -0
  47. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  48. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  49. package/gui/dist/provider-icons/telegram.svg +1 -0
  50. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  51. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  52. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  53. package/package.json +118 -0
  54. package/src/AGENTS.md +28 -0
  55. package/src/adapters/anthropic-image-guard.ts +251 -0
  56. package/src/adapters/anthropic-image-normalize.ts +518 -0
  57. package/src/adapters/anthropic.ts +1205 -0
  58. package/src/adapters/azure.ts +36 -0
  59. package/src/adapters/base.ts +83 -0
  60. package/src/adapters/client-fingerprint.ts +59 -0
  61. package/src/adapters/command-code.ts +453 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/cursor-errors.ts +165 -0
  65. package/src/adapters/cursor/discovery.ts +276 -0
  66. package/src/adapters/cursor/effort-map.ts +139 -0
  67. package/src/adapters/cursor/exec-policy.ts +88 -0
  68. package/src/adapters/cursor/framing.ts +250 -0
  69. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  70. package/src/adapters/cursor/kv-store.ts +52 -0
  71. package/src/adapters/cursor/live-models.ts +153 -0
  72. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  73. package/src/adapters/cursor/live-transport.ts +1235 -0
  74. package/src/adapters/cursor/mcp-config.ts +42 -0
  75. package/src/adapters/cursor/mcp-manager.ts +333 -0
  76. package/src/adapters/cursor/message-mapper.ts +49 -0
  77. package/src/adapters/cursor/native-exec-common.ts +59 -0
  78. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  79. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  80. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  81. package/src/adapters/cursor/native-exec-network.ts +43 -0
  82. package/src/adapters/cursor/native-exec-shell.ts +548 -0
  83. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  84. package/src/adapters/cursor/native-exec.ts +604 -0
  85. package/src/adapters/cursor/protobuf-events.ts +735 -0
  86. package/src/adapters/cursor/protobuf-request.ts +719 -0
  87. package/src/adapters/cursor/request-builder.ts +280 -0
  88. package/src/adapters/cursor/thread-continuity.ts +67 -0
  89. package/src/adapters/cursor/tool-definitions.ts +621 -0
  90. package/src/adapters/cursor/transport-retry.ts +132 -0
  91. package/src/adapters/cursor/transport.ts +57 -0
  92. package/src/adapters/cursor/types.ts +59 -0
  93. package/src/adapters/cursor.ts +196 -0
  94. package/src/adapters/google-antigravity-replay.ts +520 -0
  95. package/src/adapters/google-antigravity-wire.ts +140 -0
  96. package/src/adapters/google-errors.ts +85 -0
  97. package/src/adapters/google-http.ts +100 -0
  98. package/src/adapters/google-tool-schema.ts +173 -0
  99. package/src/adapters/google-truncation.ts +24 -0
  100. package/src/adapters/google-wire-compiler.ts +232 -0
  101. package/src/adapters/google.ts +859 -0
  102. package/src/adapters/identity.ts +77 -0
  103. package/src/adapters/image.ts +23 -0
  104. package/src/adapters/kiro-constants.ts +16 -0
  105. package/src/adapters/kiro-errors.ts +208 -0
  106. package/src/adapters/kiro-events.ts +197 -0
  107. package/src/adapters/kiro-images.ts +129 -0
  108. package/src/adapters/kiro-retry.ts +312 -0
  109. package/src/adapters/kiro-thinking.ts +104 -0
  110. package/src/adapters/kiro-tool-fallback.ts +36 -0
  111. package/src/adapters/kiro-tools.ts +224 -0
  112. package/src/adapters/kiro-truncation.ts +33 -0
  113. package/src/adapters/kiro-wire.ts +129 -0
  114. package/src/adapters/kiro.ts +1924 -0
  115. package/src/adapters/mimo-free.ts +263 -0
  116. package/src/adapters/openai-chat.ts +1265 -0
  117. package/src/adapters/openai-responses.ts +1309 -0
  118. package/src/adapters/run-turn-queue.ts +114 -0
  119. package/src/adapters/tool-catalog-nudge.ts +71 -0
  120. package/src/adapters/upstream-http-error.ts +48 -0
  121. package/src/android-remote/assets.ts +218 -0
  122. package/src/android-remote/attachments.ts +168 -0
  123. package/src/android-remote/auth.ts +237 -0
  124. package/src/android-remote/cloudflare-provisioning.ts +409 -0
  125. package/src/android-remote/cloudflare-secret.ts +106 -0
  126. package/src/android-remote/cloudflare-tunnel.ts +488 -0
  127. package/src/android-remote/cloudflared.ts +286 -0
  128. package/src/android-remote/codex-app-server.ts +565 -0
  129. package/src/android-remote/desktop-history-page.ts +936 -0
  130. package/src/android-remote/desktop-ipc.ts +3643 -0
  131. package/src/android-remote/desktop-ownership-store.ts +98 -0
  132. package/src/android-remote/desktop-project-registration.ts +129 -0
  133. package/src/android-remote/desktop-session-stream.ts +1110 -0
  134. package/src/android-remote/desktop-workspace-state.ts +443 -0
  135. package/src/android-remote/file-change-parser.ts +112 -0
  136. package/src/android-remote/gateway.ts +9108 -0
  137. package/src/android-remote/mutation-store.ts +299 -0
  138. package/src/android-remote/projection.ts +1780 -0
  139. package/src/android-remote/queued-turn-store.ts +248 -0
  140. package/src/android-remote/session-command-recovery.ts +1384 -0
  141. package/src/android-remote/store.ts +466 -0
  142. package/src/android-remote/thread-reconciliation.ts +133 -0
  143. package/src/android-remote/thread-source-paths.ts +307 -0
  144. package/src/android-remote/thread-stream.ts +546 -0
  145. package/src/android-remote/turn-activity.ts +235 -0
  146. package/src/android-remote/user-message-identity.ts +94 -0
  147. package/src/bridge.ts +1793 -0
  148. package/src/chat/inbound.ts +295 -0
  149. package/src/chat/outbound.ts +821 -0
  150. package/src/claude/agents-inject.ts +267 -0
  151. package/src/claude/alias.ts +149 -0
  152. package/src/claude/auth-detect.ts +229 -0
  153. package/src/claude/auth-mode-migration.ts +32 -0
  154. package/src/claude/auth-mode.ts +62 -0
  155. package/src/claude/context-windows.ts +189 -0
  156. package/src/claude/desktop-3p-guard.ts +35 -0
  157. package/src/claude/desktop-3p-paths.ts +84 -0
  158. package/src/claude/desktop-3p.ts +601 -0
  159. package/src/claude/desktop-health.ts +26 -0
  160. package/src/claude/desktop-profile.ts +263 -0
  161. package/src/claude/gateway-cache.ts +70 -0
  162. package/src/claude/inbound-debug.ts +163 -0
  163. package/src/claude/inbound.ts +519 -0
  164. package/src/claude/model-info.ts +154 -0
  165. package/src/claude/outbound.ts +898 -0
  166. package/src/cli/access.ts +108 -0
  167. package/src/cli/account-api.ts +296 -0
  168. package/src/cli/account-auth.ts +250 -0
  169. package/src/cli/account-catalog-refresh.ts +14 -0
  170. package/src/cli/account-extended.ts +476 -0
  171. package/src/cli/account-main.ts +317 -0
  172. package/src/cli/account.ts +297 -0
  173. package/src/cli/agent-driven.ts +70 -0
  174. package/src/cli/agent.ts +184 -0
  175. package/src/cli/catalog-prewarm.ts +27 -0
  176. package/src/cli/claude-desktop.ts +211 -0
  177. package/src/cli/claude.ts +302 -0
  178. package/src/cli/codex-shim-autorestore.ts +45 -0
  179. package/src/cli/codex-shim-readiness.ts +69 -0
  180. package/src/cli/combo.ts +124 -0
  181. package/src/cli/config-command.ts +183 -0
  182. package/src/cli/debug.ts +228 -0
  183. package/src/cli/desktop-first-run.ts +25 -0
  184. package/src/cli/doctor.ts +1022 -0
  185. package/src/cli/export-command.ts +201 -0
  186. package/src/cli/help.ts +370 -0
  187. package/src/cli/index.ts +1565 -0
  188. package/src/cli/init.ts +224 -0
  189. package/src/cli/integrations.ts +225 -0
  190. package/src/cli/interactive-confirm.ts +133 -0
  191. package/src/cli/internal-dispatch.ts +35 -0
  192. package/src/cli/launcher-context.ts +77 -0
  193. package/src/cli/models-runtime.ts +224 -0
  194. package/src/cli/models.ts +340 -0
  195. package/src/cli/observe.ts +170 -0
  196. package/src/cli/opencode.ts +587 -0
  197. package/src/cli/provider-runtime.ts +179 -0
  198. package/src/cli/provider.ts +476 -0
  199. package/src/cli/ready.ts +301 -0
  200. package/src/cli/route-policy.ts +92 -0
  201. package/src/cli/runtime-api.ts +328 -0
  202. package/src/cli/star-prompt.ts +211 -0
  203. package/src/cli/status-oauth.ts +78 -0
  204. package/src/cli/status.ts +321 -0
  205. package/src/cli/system-command.ts +196 -0
  206. package/src/cli/system-restart-client.ts +146 -0
  207. package/src/cli/tray-proxy.ts +205 -0
  208. package/src/cli/v2.ts +200 -0
  209. package/src/cli.ts +10 -0
  210. package/src/clients/config-export.ts +1109 -0
  211. package/src/codex/account-id.ts +34 -0
  212. package/src/codex/account-label.ts +34 -0
  213. package/src/codex/account-lifecycle.ts +172 -0
  214. package/src/codex/account-namespace-match.ts +63 -0
  215. package/src/codex/account-namespaces.ts +195 -0
  216. package/src/codex/account-pause.ts +20 -0
  217. package/src/codex/account-priority.ts +83 -0
  218. package/src/codex/account-runtime-state.ts +31 -0
  219. package/src/codex/account-store.ts +517 -0
  220. package/src/codex/account-usability.ts +40 -0
  221. package/src/codex/admission.ts +263 -0
  222. package/src/codex/app-server-processes.ts +799 -0
  223. package/src/codex/auth-api.ts +2098 -0
  224. package/src/codex/auth-collision.ts +107 -0
  225. package/src/codex/auth-context.ts +480 -0
  226. package/src/codex/autostart-health.ts +156 -0
  227. package/src/codex/catalog/account-models.ts +67 -0
  228. package/src/codex/catalog/aggregation.ts +471 -0
  229. package/src/codex/catalog/bundled.ts +533 -0
  230. package/src/codex/catalog/effort.ts +432 -0
  231. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  232. package/src/codex/catalog/kinds.ts +2 -0
  233. package/src/codex/catalog/metadata.ts +287 -0
  234. package/src/codex/catalog/native-models.ts +7 -0
  235. package/src/codex/catalog/parsing.ts +503 -0
  236. package/src/codex/catalog/provider-fetch.ts +2267 -0
  237. package/src/codex/catalog/sync.ts +1606 -0
  238. package/src/codex/catalog-admission.ts +197 -0
  239. package/src/codex/catalog-refresh-status.ts +87 -0
  240. package/src/codex/catalog-write-serialization.ts +242 -0
  241. package/src/codex/catalog.ts +15 -0
  242. package/src/codex/codex-write-lock.ts +384 -0
  243. package/src/codex/convergence-types.ts +593 -0
  244. package/src/codex/convergence.ts +580 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/desktop-client-processes.ts +521 -0
  249. package/src/codex/exec-invocation.ts +22 -0
  250. package/src/codex/features.ts +1091 -0
  251. package/src/codex/generation.ts +202 -0
  252. package/src/codex/history-job.ts +347 -0
  253. package/src/codex/history-lock.ts +242 -0
  254. package/src/codex/history-migration-guardian.ts +115 -0
  255. package/src/codex/history-provider.ts +1075 -0
  256. package/src/codex/history-transition.ts +105 -0
  257. package/src/codex/history-worker.ts +204 -0
  258. package/src/codex/home.ts +206 -0
  259. package/src/codex/inject-coordination.ts +257 -0
  260. package/src/codex/inject.ts +1857 -0
  261. package/src/codex/injected-marker.ts +79 -0
  262. package/src/codex/integration-record.ts +266 -0
  263. package/src/codex/internal/catalog-writer.ts +203 -0
  264. package/src/codex/internal/history-writer.ts +105 -0
  265. package/src/codex/journal.ts +172 -0
  266. package/src/codex/main-account-cache.ts +56 -0
  267. package/src/codex/main-account.ts +40 -0
  268. package/src/codex/management-convergence.ts +114 -0
  269. package/src/codex/model-cache.ts +267 -0
  270. package/src/codex/native-main-admission.ts +47 -0
  271. package/src/codex/native-main-auth-temp.ts +187 -0
  272. package/src/codex/native-main-claim.ts +167 -0
  273. package/src/codex/native-main-lock-file.ts +162 -0
  274. package/src/codex/native-main-owner.ts +329 -0
  275. package/src/codex/native-profile-api.ts +247 -0
  276. package/src/codex/native-profile-manager.ts +1531 -0
  277. package/src/codex/native-profile-processes.ts +121 -0
  278. package/src/codex/native-profile-recovery.ts +99 -0
  279. package/src/codex/native-profile-stage-store.ts +387 -0
  280. package/src/codex/native-profile-startup.ts +348 -0
  281. package/src/codex/native-profile-store.ts +855 -0
  282. package/src/codex/native-profile-types.ts +120 -0
  283. package/src/codex/native-residue.ts +691 -0
  284. package/src/codex/paths.ts +78 -0
  285. package/src/codex/plugins-doctor.ts +242 -0
  286. package/src/codex/pool-rotation.ts +295 -0
  287. package/src/codex/project-config-warnings.ts +426 -0
  288. package/src/codex/prompt-journal.ts +311 -0
  289. package/src/codex/prompt-layers.ts +967 -0
  290. package/src/codex/prompt-lock.ts +143 -0
  291. package/src/codex/provider-adoption.ts +242 -0
  292. package/src/codex/quota-rejection.ts +224 -0
  293. package/src/codex/quota.ts +494 -0
  294. package/src/codex/refresh.ts +60 -0
  295. package/src/codex/routing.ts +1855 -0
  296. package/src/codex/runtime.ts +659 -0
  297. package/src/codex/shim.ts +1215 -0
  298. package/src/codex/subagent-defaults.ts +557 -0
  299. package/src/codex/subagent-model-fallback.ts +560 -0
  300. package/src/codex/sync.ts +238 -0
  301. package/src/codex/transition-state.ts +612 -0
  302. package/src/codex/upstream-host-health.ts +368 -0
  303. package/src/codex/user-identity.ts +374 -0
  304. package/src/codex/warmup.ts +192 -0
  305. package/src/codex/websocket-registry.ts +100 -0
  306. package/src/codex/write-coordination.ts +114 -0
  307. package/src/combos/failover.ts +140 -0
  308. package/src/combos/index.ts +44 -0
  309. package/src/combos/request.ts +64 -0
  310. package/src/combos/resolve.ts +232 -0
  311. package/src/combos/types.ts +392 -0
  312. package/src/config.ts +3270 -0
  313. package/src/generated/model-metadata.ts +144 -0
  314. package/src/github/star-state.ts +203 -0
  315. package/src/grok/inject.ts +540 -0
  316. package/src/grok/inspect.ts +45 -0
  317. package/src/grok/status.ts +127 -0
  318. package/src/grok/sync.ts +66 -0
  319. package/src/images/artifacts.ts +516 -0
  320. package/src/images/fulfill-video.ts +163 -0
  321. package/src/images/fulfill.ts +149 -0
  322. package/src/images/index.ts +4 -0
  323. package/src/images/loop.ts +922 -0
  324. package/src/images/plan.ts +133 -0
  325. package/src/images/synthetic-tool.ts +133 -0
  326. package/src/images/types.ts +41 -0
  327. package/src/images/xai-client.ts +141 -0
  328. package/src/images/xai-video-client.ts +163 -0
  329. package/src/index.ts +22 -0
  330. package/src/integrations/config-io.ts +151 -0
  331. package/src/integrations/journal.ts +315 -0
  332. package/src/integrations/merge.ts +135 -0
  333. package/src/integrations/native/ownership-preflight.ts +202 -0
  334. package/src/integrations/ownership.ts +111 -0
  335. package/src/integrations/registry.ts +108 -0
  336. package/src/integrations/serialize.ts +235 -0
  337. package/src/integrations/state.ts +290 -0
  338. package/src/integrations/store.ts +103 -0
  339. package/src/integrations/writer.ts +492 -0
  340. package/src/lib/abort.ts +146 -0
  341. package/src/lib/admin-secrets.ts +25 -0
  342. package/src/lib/admission.ts +83 -0
  343. package/src/lib/app-owned-memory-stores.ts +173 -0
  344. package/src/lib/app-owned-memory.ts +265 -0
  345. package/src/lib/bounded-body.ts +242 -0
  346. package/src/lib/bun-binary-validator.d.mts +3 -0
  347. package/src/lib/bun-binary-validator.mjs +18 -0
  348. package/src/lib/bun-runtime.ts +184 -0
  349. package/src/lib/bun-stream-caps.ts +127 -0
  350. package/src/lib/config-ownership.ts +438 -0
  351. package/src/lib/crash-guard.ts +344 -0
  352. package/src/lib/debug-log-buffer.ts +83 -0
  353. package/src/lib/debug-settings.ts +108 -0
  354. package/src/lib/debug.ts +31 -0
  355. package/src/lib/destination-policy.ts +316 -0
  356. package/src/lib/errors.ts +364 -0
  357. package/src/lib/eventstream-decoder.ts +253 -0
  358. package/src/lib/gcp-adc.ts +341 -0
  359. package/src/lib/injection-debug-log.ts +58 -0
  360. package/src/lib/local-management-attestation.ts +51 -0
  361. package/src/lib/open-url.ts +25 -0
  362. package/src/lib/pinned-http.ts +182 -0
  363. package/src/lib/privacy.ts +20 -0
  364. package/src/lib/process-control.ts +168 -0
  365. package/src/lib/provider-environment.ts +470 -0
  366. package/src/lib/provider-outbound.ts +203 -0
  367. package/src/lib/provider-url.ts +14 -0
  368. package/src/lib/proxy-env.ts +18 -0
  369. package/src/lib/redact.ts +510 -0
  370. package/src/lib/remodex-home.ts +616 -0
  371. package/src/lib/retry-after.ts +55 -0
  372. package/src/lib/service-secrets.ts +178 -0
  373. package/src/lib/shadow-call.ts +54 -0
  374. package/src/lib/sidecar-tracker.ts +52 -0
  375. package/src/lib/sse-decoder.ts +364 -0
  376. package/src/lib/state-store-registrations.ts +109 -0
  377. package/src/lib/state-store-sweeper.ts +184 -0
  378. package/src/lib/system-restart-contract.ts +73 -0
  379. package/src/lib/test-home-guard.ts +98 -0
  380. package/src/lib/token-estimate.ts +69 -0
  381. package/src/lib/translator-budget.ts +366 -0
  382. package/src/lib/upstream-reachability.ts +91 -0
  383. package/src/lib/upstream-retry.ts +508 -0
  384. package/src/lib/win-exec.ts +115 -0
  385. package/src/lib/win-paths.ts +68 -0
  386. package/src/lib/windows-elevation.ts +705 -0
  387. package/src/lib/windows-secret-acl.ts +817 -0
  388. package/src/lib/windows-user-principal.ts +283 -0
  389. package/src/lib/winsw.ts +402 -0
  390. package/src/model-sources.ts +73 -0
  391. package/src/oauth/anthropic-routing.ts +594 -0
  392. package/src/oauth/anthropic.ts +177 -0
  393. package/src/oauth/callback-server.ts +294 -0
  394. package/src/oauth/chatgpt.ts +150 -0
  395. package/src/oauth/command-code.ts +239 -0
  396. package/src/oauth/cursor.ts +231 -0
  397. package/src/oauth/github-copilot.ts +428 -0
  398. package/src/oauth/google-antigravity.ts +230 -0
  399. package/src/oauth/health.ts +443 -0
  400. package/src/oauth/index.ts +1280 -0
  401. package/src/oauth/key-providers.ts +128 -0
  402. package/src/oauth/kimi.ts +213 -0
  403. package/src/oauth/kiro-credentials.ts +726 -0
  404. package/src/oauth/kiro.ts +621 -0
  405. package/src/oauth/local-token-detect.ts +121 -0
  406. package/src/oauth/log.ts +48 -0
  407. package/src/oauth/login-cli.ts +163 -0
  408. package/src/oauth/pkce.ts +15 -0
  409. package/src/oauth/store.ts +655 -0
  410. package/src/oauth/token-guardian.ts +309 -0
  411. package/src/oauth/types.ts +62 -0
  412. package/src/oauth/xai.ts +241 -0
  413. package/src/providers/alibaba-region-backup.ts +75 -0
  414. package/src/providers/alibaba-region-migration.ts +156 -0
  415. package/src/providers/alibaba-region-startup.ts +36 -0
  416. package/src/providers/antigravity-models.ts +317 -0
  417. package/src/providers/api-keys.ts +140 -0
  418. package/src/providers/base-url-choices.ts +64 -0
  419. package/src/providers/codex-capacity.ts +288 -0
  420. package/src/providers/command-code-efforts.ts +85 -0
  421. package/src/providers/context-cap.ts +73 -0
  422. package/src/providers/derive.ts +451 -0
  423. package/src/providers/free-directory.ts +187 -0
  424. package/src/providers/github-copilot-transport.ts +56 -0
  425. package/src/providers/google-vertex-location.ts +14 -0
  426. package/src/providers/key-failover.ts +271 -0
  427. package/src/providers/kiro-models.ts +67 -0
  428. package/src/providers/label.ts +19 -0
  429. package/src/providers/model-discovery-limits.ts +16 -0
  430. package/src/providers/model-discovery.ts +361 -0
  431. package/src/providers/openai-sidecar.ts +235 -0
  432. package/src/providers/openai-tier-startup.ts +27 -0
  433. package/src/providers/openai-tiers.ts +301 -0
  434. package/src/providers/openai-virtual-models.ts +83 -0
  435. package/src/providers/openrouter-routing.ts +102 -0
  436. package/src/providers/provider-id-rewrite.ts +179 -0
  437. package/src/providers/quota.ts +1942 -0
  438. package/src/providers/reasoning-capabilities.ts +336 -0
  439. package/src/providers/registry.ts +2375 -0
  440. package/src/providers/slug-codec.ts +74 -0
  441. package/src/providers/xai-transport.ts +149 -0
  442. package/src/reasoning-effort.ts +243 -0
  443. package/src/responses/compaction.ts +124 -0
  444. package/src/responses/hosted-tool-policy.ts +9 -0
  445. package/src/responses/parser.ts +714 -0
  446. package/src/responses/reasoning-envelope.ts +60 -0
  447. package/src/responses/reasoning-replay-cache.ts +106 -0
  448. package/src/responses/schema.ts +159 -0
  449. package/src/responses/spill-store.ts +431 -0
  450. package/src/responses/state.ts +1039 -0
  451. package/src/responses/tool-groups.ts +19 -0
  452. package/src/router.ts +802 -0
  453. package/src/routing/analytics.ts +377 -0
  454. package/src/routing/capability.ts +205 -0
  455. package/src/routing/cost.ts +77 -0
  456. package/src/routing/evaluator.ts +444 -0
  457. package/src/routing/health.ts +401 -0
  458. package/src/routing/history/cursor.ts +43 -0
  459. package/src/routing/history/indexer.ts +605 -0
  460. package/src/routing/history/schema.ts +72 -0
  461. package/src/routing/profile-namespace.ts +15 -0
  462. package/src/routing/profile.ts +424 -0
  463. package/src/routing/quota.ts +145 -0
  464. package/src/routing/request-evidence.ts +45 -0
  465. package/src/routing/trace.ts +686 -0
  466. package/src/server/adapter-resolve.ts +83 -0
  467. package/src/server/auth-cors.ts +606 -0
  468. package/src/server/chat-completions.ts +379 -0
  469. package/src/server/claude-messages.ts +980 -0
  470. package/src/server/effort-policy.ts +251 -0
  471. package/src/server/github-copilot-responses-repair.ts +338 -0
  472. package/src/server/gui-static.ts +152 -0
  473. package/src/server/image-retry.ts +42 -0
  474. package/src/server/images.ts +485 -0
  475. package/src/server/index.ts +1633 -0
  476. package/src/server/lifecycle.ts +482 -0
  477. package/src/server/live.ts +609 -0
  478. package/src/server/management/agent-settings-routes.ts +1180 -0
  479. package/src/server/management/android-remote-routes.ts +390 -0
  480. package/src/server/management/api-access.ts +141 -0
  481. package/src/server/management/api-key-usage.ts +167 -0
  482. package/src/server/management/body.ts +35 -0
  483. package/src/server/management/combo-routes.ts +244 -0
  484. package/src/server/management/config-routes.ts +602 -0
  485. package/src/server/management/context.ts +88 -0
  486. package/src/server/management/integration-routes.ts +538 -0
  487. package/src/server/management/logs-usage-routes.ts +516 -0
  488. package/src/server/management/model-routes.ts +519 -0
  489. package/src/server/management/model-rows.ts +143 -0
  490. package/src/server/management/native-integration-routes.ts +781 -0
  491. package/src/server/management/oauth-account-routes.ts +573 -0
  492. package/src/server/management/provider-routes.ts +781 -0
  493. package/src/server/management/request-history-routes.ts +191 -0
  494. package/src/server/management/routing-analytics-routes.ts +74 -0
  495. package/src/server/management/routing-profile-routes.ts +384 -0
  496. package/src/server/management/shared.ts +277 -0
  497. package/src/server/management/sidebar-routes.ts +106 -0
  498. package/src/server/management/sync-response.ts +69 -0
  499. package/src/server/management/system-restart.ts +433 -0
  500. package/src/server/management/system-routes.ts +141 -0
  501. package/src/server/management/usage-summary-cache.ts +86 -0
  502. package/src/server/management-api.ts +269 -0
  503. package/src/server/management-auth.ts +353 -0
  504. package/src/server/memory-watchdog.ts +156 -0
  505. package/src/server/port-reclaim.ts +307 -0
  506. package/src/server/ports.ts +156 -0
  507. package/src/server/proxy-liveness.ts +326 -0
  508. package/src/server/proxy-stop.ts +92 -0
  509. package/src/server/readiness.ts +99 -0
  510. package/src/server/relay-eager.ts +353 -0
  511. package/src/server/relay.ts +1179 -0
  512. package/src/server/request-decompress.ts +132 -0
  513. package/src/server/request-log-conversation.ts +168 -0
  514. package/src/server/request-log.ts +1072 -0
  515. package/src/server/responses/collaboration.ts +409 -0
  516. package/src/server/responses/compact.ts +710 -0
  517. package/src/server/responses/core.ts +3561 -0
  518. package/src/server/responses/encrypted-payload.ts +308 -0
  519. package/src/server/responses/fetch-helpers.ts +171 -0
  520. package/src/server/responses/passthrough-error.ts +78 -0
  521. package/src/server/responses/policy-fallback.ts +152 -0
  522. package/src/server/responses/terminal-guard.ts +230 -0
  523. package/src/server/responses/upstream-error.ts +48 -0
  524. package/src/server/responses-image-gen-repair.ts +132 -0
  525. package/src/server/responses-item-id-repair.ts +272 -0
  526. package/src/server/responses-json-events.ts +52 -0
  527. package/src/server/responses-model-rewrite.ts +29 -0
  528. package/src/server/responses-snapshot-repair.ts +621 -0
  529. package/src/server/responses.ts +10 -0
  530. package/src/server/search.ts +181 -0
  531. package/src/server/sse-frame-buffer.ts +292 -0
  532. package/src/server/sse-payload-rewrite.ts +263 -0
  533. package/src/server/startup-action-control.ts +308 -0
  534. package/src/server/startup-health-cache.ts +119 -0
  535. package/src/server/system-env.ts +418 -0
  536. package/src/server/windows-tcp-drop.ts +184 -0
  537. package/src/server/windows-tray-control.ts +41 -0
  538. package/src/server/ws-bridge.ts +470 -0
  539. package/src/service-manager-probe.ts +824 -0
  540. package/src/service.ts +3011 -0
  541. package/src/stall-timeout.ts +20 -0
  542. package/src/storage/cleanup-job.ts +57 -0
  543. package/src/storage/cleanup.ts +3085 -0
  544. package/src/storage/policy-job.ts +457 -0
  545. package/src/storage/policy-scheduler.ts +40 -0
  546. package/src/storage/policy-worker.ts +59 -0
  547. package/src/storage/policy.ts +527 -0
  548. package/src/storage/restore-job.ts +299 -0
  549. package/src/storage/restore-worker.ts +58 -0
  550. package/src/storage/scanner.ts +238 -0
  551. package/src/storage/storage-mutation-coordinator.ts +139 -0
  552. package/src/storage/worker-lifecycle.ts +215 -0
  553. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  554. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  555. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  556. package/src/tray/assets/opencodex-tray.png +0 -0
  557. package/src/tray/windows-tray.ps1 +364 -0
  558. package/src/tray/windows.ts +738 -0
  559. package/src/types.ts +1531 -0
  560. package/src/update/badge.ts +72 -0
  561. package/src/update/desktop-release.ts +1620 -0
  562. package/src/update/index.ts +402 -0
  563. package/src/update/job.ts +1906 -0
  564. package/src/update/notify.ts +261 -0
  565. package/src/update/npm-cache-preflight.d.mts +47 -0
  566. package/src/update/npm-cache-preflight.mjs +201 -0
  567. package/src/update/npm-invocation.d.mts +23 -0
  568. package/src/update/npm-invocation.mjs +94 -0
  569. package/src/update/tray-update-plan.d.mts +18 -0
  570. package/src/update/tray-update-plan.mjs +38 -0
  571. package/src/usage/cost.ts +0 -0
  572. package/src/usage/debug.ts +97 -0
  573. package/src/usage/expected-prices.ts +283 -0
  574. package/src/usage/log.ts +695 -0
  575. package/src/usage/summary.ts +585 -0
  576. package/src/usage/totals.ts +14 -0
  577. package/src/vision/anthropic-describe.ts +185 -0
  578. package/src/vision/describe.ts +127 -0
  579. package/src/vision/index.ts +558 -0
  580. package/src/vision/reasoning.ts +55 -0
  581. package/src/web-search/anthropic-executor.ts +189 -0
  582. package/src/web-search/executor.ts +105 -0
  583. package/src/web-search/format-result.ts +89 -0
  584. package/src/web-search/index.ts +196 -0
  585. package/src/web-search/loop.ts +791 -0
  586. package/src/web-search/parse.ts +235 -0
  587. package/src/web-search/progress-stream.ts +342 -0
  588. package/src/web-search/synthetic-tool.ts +47 -0
@@ -0,0 +1,1039 @@
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, rmSync, statSync, unlinkSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { atomicWriteFileAsync, getConfigDir, resolveWriteTarget } from "../config";
4
+ import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
5
+ import type { OcxProviderContinuationState } from "../types";
6
+ import {
7
+ deleteResponseSpill,
8
+ noteStubSwapForTest,
9
+ readResponseSpill,
10
+ recoverOrphanedResponseSpills,
11
+ responseSpillDirectory,
12
+ responseSpillPayloadCap,
13
+ type ResponseSpillRef,
14
+ writeResponseSpillDurably,
15
+ } from "./spill-store";
16
+
17
+ const MAX_STORED_RESPONSES = 1_000;
18
+ const RESPONSE_TTL_MS = 60 * 60 * 1_000;
19
+ const SNAPSHOT_DEBOUNCE_MS = 2_000;
20
+ /** In-memory high-water byte cap across all entries. Forced store:false retention (kiro/cursor
21
+ * continuation chains) stores the full expanded input each turn — ~quadratic bytes per chain —
22
+ * so a count cap alone cannot bound memory. Oldest-first eviction applies past this mark. */
23
+ export const MAX_STORED_RESPONSE_BYTES = 64 * 1024 * 1024;
24
+ /** Legacy snapshot selection only. Spill demotion is governed solely by the RAM cap above. */
25
+ const SNAPSHOT_ENTRY_MAX_BYTES = 2 * 1024 * 1024;
26
+ const SNAPSHOT_TOTAL_MAX_BYTES = 24 * 1024 * 1024;
27
+ /** Refuse-to-parse ceiling for an existing snapshot file (above the 24 MiB write
28
+ * bound, so anything we wrote ourselves always loads; guards against externally
29
+ * planted or pre-cap unbounded files being parsed whole). */
30
+ const SNAPSHOT_FILE_MAX_BYTES = 32 * 1024 * 1024;
31
+ const STALE_TEMP_GRACE_MS = 15 * 60 * 1_000;
32
+ const STALE_TEMP_MAX_ENTRIES = 4_096;
33
+ const STALE_TEMP_MAX_CLEANUPS = 512;
34
+ const RESPONSE_STATE_TEMP_NAME = /^responses-state\.json\.ocx\.(\d+)\.(\d+)\.tmp$/;
35
+ const MAX_SNAPSHOT_REWRITE_ATTEMPTS = 4;
36
+
37
+ interface ResidentResponseState {
38
+ kind: "resident";
39
+ createdAt: number;
40
+ items: unknown[];
41
+ providers?: OcxProviderContinuationState;
42
+ sizeBytes: number;
43
+ }
44
+
45
+ interface SpilledResponseState {
46
+ kind: "spill";
47
+ createdAt: number;
48
+ providers?: OcxProviderContinuationState;
49
+ spill: ResponseSpillRef;
50
+ sizeBytes: number;
51
+ }
52
+
53
+ interface SpillFailedResponseState {
54
+ kind: "spill-failed";
55
+ createdAt: number;
56
+ sizeBytes: number;
57
+ }
58
+
59
+ type StoredResponseState = ResidentResponseState | SpilledResponseState | SpillFailedResponseState;
60
+ type ResidentInput = Omit<ResidentResponseState, "kind" | "sizeBytes">;
61
+
62
+ export type PreviousResponseReplayFailure = {
63
+ code: "previous_response_not_found";
64
+ reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large";
65
+ };
66
+
67
+ const states = new Map<string, StoredResponseState>();
68
+ let storedResponseBytes = 0;
69
+ let residentResponseBytes = 0;
70
+ let oldestResidentId: string | undefined;
71
+ let oldestResidentAt: number | null = null;
72
+ let byteCapOverride: number | null = null;
73
+ let stateRevision = 0;
74
+ const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
75
+ /**
76
+ * Admission-boundary observability (test-visible). directSpills: oversized
77
+ * candidates routed straight to durable spill without a resident stay or
78
+ * unrelated demotion. oversizedDrops: candidates above the single-spill
79
+ * payload ceiling, tombstoned instead of retained. snapshotOversizedRefusals:
80
+ * snapshot files refused before parse.
81
+ */
82
+ const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
83
+
84
+ /** Test-only: admission-boundary counters (proves the new paths fire). */
85
+ export function responseAdmissionCountersForTests(): Readonly<typeof admissionCounters> {
86
+ return admissionCounters;
87
+ }
88
+ // Superseded spill generations awaiting a durable snapshot before unlink
89
+ // (review C1-1: unlinking at swap time races a crash against the debounced
90
+ // snapshot — the reloaded OLD stub would point at a deleted file).
91
+ const pendingSpillUnlinks: ResponseSpillRef[] = [];
92
+ // The queue itself must stay bounded (review C2-2: repeated replacements with
93
+ // a persistently failing snapshot write would otherwise grow it without
94
+ // limit). Beyond the cap the OLDEST superseded generation is unlinked
95
+ // immediately: the accepted worst case is that a crash inside that window
96
+ // reloads a stub whose file is gone, which fails replay with the explicit
97
+ // structured 400 — bounded-loss, never silent corruption or unbounded disk.
98
+ const PENDING_SPILL_UNLINKS_MAX = 128;
99
+
100
+ function byteCap(): number {
101
+ return byteCapOverride ?? MAX_STORED_RESPONSE_BYTES;
102
+ }
103
+
104
+ /** Test-only: lower/restore the in-memory byte cap (null restores the default). */
105
+ export function setResponseStateByteCapForTests(bytes: number | null): void {
106
+ byteCapOverride = bytes;
107
+ }
108
+
109
+ /** Test-only: current in-memory byte accounting (proves evictions release their bytes). */
110
+ export function getStoredResponseBytesForTests(): number {
111
+ return storedResponseBytes;
112
+ }
113
+
114
+ function serializedBytes(value: unknown): number | null {
115
+ try {
116
+ const serialized = JSON.stringify(value);
117
+ return serialized === undefined ? null : Buffer.byteLength(serialized, "utf8");
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ function measureResidentEntry(id: string, entry: ResidentInput): ResidentResponseState | null {
124
+ const sizeBytes = serializedBytes({
125
+ responseId: id,
126
+ createdAt: entry.createdAt,
127
+ items: entry.items,
128
+ ...(entry.providers ? { providers: entry.providers } : {}),
129
+ });
130
+ return sizeBytes === null ? null : { kind: "resident", ...entry, sizeBytes };
131
+ }
132
+
133
+ function recomputeOldestResident(): void {
134
+ oldestResidentId = undefined;
135
+ oldestResidentAt = null;
136
+ for (const [id, state] of states) {
137
+ if (state.kind !== "resident") continue;
138
+ if (oldestResidentAt !== null && state.createdAt >= oldestResidentAt) continue;
139
+ oldestResidentId = id;
140
+ oldestResidentAt = state.createdAt;
141
+ }
142
+ }
143
+
144
+ function replaceMapEntry(id: string, next: StoredResponseState, expected?: StoredResponseState): boolean {
145
+ const existing = states.get(id);
146
+ if (expected && existing !== expected) return false;
147
+ storedResponseBytes -= existing?.sizeBytes ?? 0;
148
+ storedResponseBytes += next.sizeBytes;
149
+ if (existing?.kind === "resident") {
150
+ residentResponseBytes -= existing.sizeBytes;
151
+ }
152
+ if (next.kind === "resident") {
153
+ residentResponseBytes += next.sizeBytes;
154
+ }
155
+ if (storedResponseBytes < 0) storedResponseBytes = 0;
156
+ if (residentResponseBytes < 0) residentResponseBytes = 0;
157
+ if (existing) states.delete(id);
158
+ states.set(id, next);
159
+ if (oldestResidentId === id) {
160
+ recomputeOldestResident();
161
+ } else if (next.kind === "resident" && (oldestResidentAt === null || next.createdAt < oldestResidentAt)) {
162
+ oldestResidentId = id;
163
+ oldestResidentAt = next.createdAt;
164
+ }
165
+ stateRevision += 1;
166
+ return true;
167
+ }
168
+
169
+ function stubSize(id: string, entry: Omit<SpilledResponseState, "sizeBytes">): number {
170
+ return serializedBytes({ responseId: id, ...entry }) ?? 0;
171
+ }
172
+
173
+ function tombstone(id: string, createdAt: number): SpillFailedResponseState {
174
+ const base = { kind: "spill-failed" as const, createdAt };
175
+ return { ...base, sizeBytes: serializedBytes({ responseId: id, ...base }) ?? 0 };
176
+ }
177
+
178
+ function deleteOwnedSpills(entry: StoredResponseState): void {
179
+ if (entry.kind === "spill") deleteResponseSpill(entry.spill);
180
+ }
181
+
182
+ /** The ONLY deletion point: TTL, count, byte, and explicit deletes all route here. */
183
+ function deleteEntry(id: string, options: { deleteSpill?: boolean } = {}): void {
184
+ const existing = states.get(id);
185
+ if (!existing) return;
186
+ storedResponseBytes -= existing.sizeBytes;
187
+ if (existing.kind === "resident") {
188
+ residentResponseBytes -= existing.sizeBytes;
189
+ }
190
+ if (storedResponseBytes < 0) storedResponseBytes = 0;
191
+ if (residentResponseBytes < 0) residentResponseBytes = 0;
192
+ states.delete(id);
193
+ if (oldestResidentId === id) recomputeOldestResident();
194
+ stateRevision += 1;
195
+ if (options.deleteSpill !== false) deleteOwnedSpills(existing);
196
+ }
197
+
198
+ function replaceWithSpillFailure(
199
+ id: string,
200
+ expected?: StoredResponseState,
201
+ options: { deferSpillUnlink?: boolean } = {},
202
+ ): void {
203
+ const existing = states.get(id);
204
+ if (expected && existing !== expected) return;
205
+ const failed = tombstone(id, expected?.createdAt ?? existing?.createdAt ?? now());
206
+ if (replaceMapEntry(id, failed, expected)) {
207
+ if (existing) {
208
+ if (options.deferSpillUnlink && existing.kind === "spill") {
209
+ // Crash consistency (same rule as replaceSpillEntryAtomically): the old
210
+ // durable snapshot still references this generation until the tombstone
211
+ // itself is durable — queue the unlink for the next stable persist.
212
+ pendingSpillUnlinks.push(existing.spill);
213
+ while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
214
+ deleteResponseSpill(pendingSpillUnlinks.shift()!);
215
+ }
216
+ } else {
217
+ deleteOwnedSpills(existing);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref: ResponseSpillRef): boolean {
224
+ const base: Omit<SpilledResponseState, "sizeBytes"> = {
225
+ kind: "spill",
226
+ createdAt: expected.createdAt,
227
+ ...(expected.providers ? { providers: expected.providers } : {}),
228
+ spill: ref,
229
+ };
230
+ const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
231
+ if (!replaceMapEntry(id, next, expected)) {
232
+ deleteResponseSpill(ref);
233
+ return false;
234
+ }
235
+ noteStubSwapForTest();
236
+ return true;
237
+ }
238
+
239
+ function replaceSpillEntryAtomically(
240
+ id: string,
241
+ expected: SpilledResponseState,
242
+ candidate: ResidentResponseState,
243
+ ): void {
244
+ try {
245
+ const ref = writeResponseSpillDurably(id, {
246
+ createdAt: candidate.createdAt,
247
+ items: candidate.items,
248
+ ...(candidate.providers ? { providers: candidate.providers } : {}),
249
+ });
250
+ const base: Omit<SpilledResponseState, "sizeBytes"> = {
251
+ kind: "spill",
252
+ createdAt: candidate.createdAt,
253
+ ...(candidate.providers ? { providers: candidate.providers } : {}),
254
+ spill: ref,
255
+ };
256
+ const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
257
+ if (!replaceMapEntry(id, next, expected)) {
258
+ deleteResponseSpill(ref);
259
+ return;
260
+ }
261
+ spillCounters.writes += 1;
262
+ noteStubSwapForTest();
263
+ // The old generation is NOT unlinked here (review C1-1): the new stub is
264
+ // only durable once the debounced snapshot flushes — a crash before that
265
+ // reloads the OLD stub, which must still find its file. Queue the unlink;
266
+ // persistNow() drains the queue only after the snapshot write succeeds.
267
+ pendingSpillUnlinks.push(expected.spill);
268
+ while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
269
+ deleteResponseSpill(pendingSpillUnlinks.shift()!);
270
+ }
271
+ } catch {
272
+ spillCounters.writeFailures += 1;
273
+ // deferSpillUnlink: the durable snapshot may still reference the old
274
+ // generation; deleting it now would strand the old stub after a crash.
275
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
276
+ }
277
+ }
278
+
279
+ function setResidentEntry(id: string, entry: ResidentInput): void {
280
+ const expected = states.get(id);
281
+ const candidate = measureResidentEntry(id, entry);
282
+ if (!candidate) {
283
+ replaceWithSpillFailure(id, expected);
284
+ // A tombstone is tiny but still resident state: the hard-cap invariant
285
+ // must hold on EVERY mutation path (review C2-1 — with a test cap below
286
+ // tombstone size, skipping the prune leaves the store over cap).
287
+ pruneResponses();
288
+ return;
289
+ }
290
+ if (candidate.sizeBytes > byteCap()) {
291
+ admitOversizedCandidate(id, candidate, expected);
292
+ pruneResponses();
293
+ return;
294
+ }
295
+ if (expected?.kind === "spill") {
296
+ replaceSpillEntryAtomically(id, expected, candidate);
297
+ pruneResponses();
298
+ return;
299
+ }
300
+ if (!replaceMapEntry(id, candidate, expected)) return;
301
+ pruneResponses();
302
+ }
303
+
304
+ /**
305
+ * Admission boundary for candidates that can never fit as resident (larger
306
+ * than the whole resident-map cap). Writes them DIRECTLY to durable spill and
307
+ * installs only the stub — the oversized candidate never becomes resident and
308
+ * no unrelated resident is demoted to make room for it. Candidates above the
309
+ * single-spill payload ceiling are tombstoned instead: retaining a spill the
310
+ * replay ceiling would refuse to read is write-only waste.
311
+ */
312
+ function admitOversizedCandidate(
313
+ id: string,
314
+ candidate: ResidentResponseState,
315
+ expected?: StoredResponseState,
316
+ ): void {
317
+ if (candidate.sizeBytes > responseSpillPayloadCap()) {
318
+ admissionCounters.oversizedDrops += 1;
319
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
320
+ return;
321
+ }
322
+ try {
323
+ const ref = writeResponseSpillDurably(id, {
324
+ createdAt: candidate.createdAt,
325
+ items: candidate.items,
326
+ ...(candidate.providers ? { providers: candidate.providers } : {}),
327
+ });
328
+ // Enforce the ceiling against the REAL envelope: the spill payload adds
329
+ // the {version, responseId, ...} wrapper, so a candidate within the
330
+ // wrapper's size of the cap would otherwise be retained unreadably.
331
+ if (ref.payloadBytes > responseSpillPayloadCap()) {
332
+ deleteResponseSpill(ref);
333
+ admissionCounters.oversizedDrops += 1;
334
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
335
+ return;
336
+ }
337
+ const base: Omit<SpilledResponseState, "sizeBytes"> = {
338
+ kind: "spill",
339
+ createdAt: candidate.createdAt,
340
+ ...(candidate.providers ? { providers: candidate.providers } : {}),
341
+ spill: ref,
342
+ };
343
+ const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
344
+ if (!replaceMapEntry(id, next, expected)) {
345
+ deleteResponseSpill(ref);
346
+ return;
347
+ }
348
+ spillCounters.writes += 1;
349
+ admissionCounters.directSpills += 1;
350
+ noteStubSwapForTest();
351
+ if (expected?.kind === "spill") {
352
+ // Same deferred-unlink rule as replaceSpillEntryAtomically: the new stub
353
+ // is durable only after the debounced snapshot, so the old generation
354
+ // stays until a stable persist drains the queue.
355
+ pendingSpillUnlinks.push(expected.spill);
356
+ while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
357
+ deleteResponseSpill(pendingSpillUnlinks.shift()!);
358
+ }
359
+ }
360
+ } catch {
361
+ spillCounters.writeFailures += 1;
362
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
363
+ }
364
+ }
365
+
366
+ // Expansion provenance must stay proxy-private: a WeakMap distinguishes replayed history from the
367
+ // newly appended input suffix without adding an unknown field that native passthrough could send
368
+ // upstream. The parser uses this boundary to acknowledge historical compaction markers exactly once.
369
+ const replayedInputPrefixLengths = new WeakMap<object, number>();
370
+ const replayFailures = new WeakMap<object, PreviousResponseReplayFailure>();
371
+ let loaded = false;
372
+ let persistTimer: ReturnType<typeof setTimeout> | null = null;
373
+ let pendingPersistPath: string | null = null;
374
+ /** Single-flight gate: overlapping response-state writes serialize (#612). */
375
+ let persistGate: Promise<void> = Promise.resolve();
376
+ let persistAttemptHookForTests: (() => void) | null = null;
377
+
378
+ function now(): number {
379
+ return Date.now();
380
+ }
381
+
382
+ function snapshotPath(): string {
383
+ return join(getConfigDir(), "responses-state.json");
384
+ }
385
+
386
+ interface LegacySnapshotState {
387
+ createdAt?: unknown;
388
+ items?: unknown;
389
+ providers?: OcxProviderContinuationState;
390
+ conversationId?: unknown;
391
+ cursorCheckpointUsable?: unknown;
392
+ }
393
+
394
+ function isSpillRef(value: unknown): value is ResponseSpillRef {
395
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
396
+ const ref = value as ResponseSpillRef;
397
+ return ref.version === 1
398
+ && typeof ref.fileName === "string"
399
+ && /^[0-9a-f]{64}$/.test(ref.digest)
400
+ && Number.isSafeInteger(ref.payloadBytes)
401
+ && ref.payloadBytes >= 0;
402
+ }
403
+
404
+ function loadSnapshotEntry(id: string, value: unknown): void {
405
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
406
+ const rec = value as LegacySnapshotState & { kind?: unknown; spill?: unknown };
407
+ if (typeof rec.createdAt !== "number" || !Number.isFinite(rec.createdAt)) return;
408
+ if (rec.kind === "spill") {
409
+ if (!isSpillRef(rec.spill)) return;
410
+ const base: Omit<SpilledResponseState, "sizeBytes"> = {
411
+ kind: "spill",
412
+ createdAt: rec.createdAt,
413
+ ...(rec.providers ? { providers: rec.providers } : {}),
414
+ spill: rec.spill,
415
+ };
416
+ replaceMapEntry(id, { ...base, sizeBytes: stubSize(id, base) });
417
+ return;
418
+ }
419
+ if (rec.kind === "spill-failed") {
420
+ replaceMapEntry(id, tombstone(id, rec.createdAt));
421
+ return;
422
+ }
423
+ if (rec.kind !== undefined && rec.kind !== "resident") return;
424
+ if (!Array.isArray(rec.items)) return;
425
+ const providers = rec.providers ?? (typeof rec.conversationId === "string"
426
+ ? {
427
+ cursor: {
428
+ conversationId: rec.conversationId,
429
+ ...(typeof rec.cursorCheckpointUsable === "boolean"
430
+ ? { checkpointUsable: rec.cursorCheckpointUsable }
431
+ : {}),
432
+ },
433
+ }
434
+ : undefined);
435
+ const resident = measureResidentEntry(id, {
436
+ createdAt: rec.createdAt,
437
+ items: rec.items,
438
+ ...(providers ? { providers } : {}),
439
+ });
440
+ if (!resident) {
441
+ replaceMapEntry(id, tombstone(id, rec.createdAt));
442
+ return;
443
+ }
444
+ // Same admission boundary as live writes: an oversized snapshot row goes
445
+ // straight to spill (or tombstone above the payload ceiling) instead of
446
+ // entering the resident map and demoting unrelated rows on the first prune.
447
+ if (resident.sizeBytes > byteCap()) {
448
+ admitOversizedCandidate(id, resident, undefined);
449
+ return;
450
+ }
451
+ replaceMapEntry(id, resident);
452
+ }
453
+
454
+ export interface ResponseStateTempRecoveryResult {
455
+ matched: number;
456
+ removed: number;
457
+ failed: number;
458
+ bytesRemoved: number;
459
+ }
460
+
461
+ interface ResponseStateTempRecoveryIO {
462
+ now: () => number;
463
+ list: (dir: string) => Iterable<string>;
464
+ inspect: (path: string) => { isFile: boolean; mtimeMs: number; size: number };
465
+ isProcessAlive: (pid: number) => boolean;
466
+ unlink: (path: string) => void;
467
+ }
468
+
469
+ type ResponseStateTempRecoveryOptions = Partial<ResponseStateTempRecoveryIO> & {
470
+ maxEntries?: number;
471
+ maxCleanups?: number;
472
+ };
473
+
474
+ function processIsAlive(pid: number): boolean {
475
+ if (pid === process.pid) return true;
476
+ try {
477
+ process.kill(pid, 0);
478
+ return true;
479
+ } catch (error) {
480
+ // EPERM means the process exists but cannot be signalled. Unknown platform errors
481
+ // are also protected; cleanup should prefer a false negative over touching a live writer.
482
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
483
+ }
484
+ }
485
+
486
+ const responseStateTempRecoveryIO: ResponseStateTempRecoveryIO = {
487
+ now: Date.now,
488
+ list: function* list(dir) {
489
+ const handle = opendirSync(dir);
490
+ try {
491
+ for (let entry = handle.readSync(); entry; entry = handle.readSync()) yield entry.name;
492
+ } finally {
493
+ handle.closeSync();
494
+ }
495
+ },
496
+ inspect: path => {
497
+ const stat = lstatSync(path);
498
+ return { isFile: stat.isFile() && !stat.isSymbolicLink(), mtimeMs: stat.mtimeMs, size: stat.size };
499
+ },
500
+ isProcessAlive: processIsAlive,
501
+ unlink: unlinkSync,
502
+ };
503
+
504
+ /**
505
+ * Recover only abandoned response-state atomic-write files. The exact basename,
506
+ * regular-file check, age gate, and PID liveness check protect unrelated/active files.
507
+ * Cleanup is capped and best-effort because continuation state is only a cache. Removal
508
+ * deliberately uses unlink only: path-based truncation could follow a replacement symlink.
509
+ */
510
+ export function recoverStaleResponseStateTemps(
511
+ dir = getConfigDir(),
512
+ options: ResponseStateTempRecoveryOptions = {},
513
+ ): ResponseStateTempRecoveryResult {
514
+ const { maxEntries = STALE_TEMP_MAX_ENTRIES, maxCleanups = STALE_TEMP_MAX_CLEANUPS, ...overrides } = options;
515
+ const io = { ...responseStateTempRecoveryIO, ...overrides };
516
+ const result: ResponseStateTempRecoveryResult = {
517
+ matched: 0,
518
+ removed: 0,
519
+ failed: 0,
520
+ bytesRemoved: 0,
521
+ };
522
+ let names: Iterable<string>;
523
+ try { names = io.list(dir); } catch { return result; }
524
+ let iterator: Iterator<string>;
525
+ try { iterator = names[Symbol.iterator](); } catch { return result; }
526
+ let scanned = 0;
527
+ for (;;) {
528
+ let next: IteratorResult<string>;
529
+ try { next = iterator.next(); } catch { return result; }
530
+ if (next.done) break;
531
+ const name = next.value;
532
+ scanned += 1;
533
+ if (scanned > maxEntries || result.removed + result.failed >= maxCleanups) break;
534
+ const match = RESPONSE_STATE_TEMP_NAME.exec(name);
535
+ if (!match) continue;
536
+ result.matched += 1;
537
+ const pid = Number(match[1]);
538
+ const sequence = Number(match[2]);
539
+ if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isSafeInteger(sequence) || sequence <= 0) continue;
540
+ const path = join(dir, name);
541
+ let file: ReturnType<ResponseStateTempRecoveryIO["inspect"]>;
542
+ try { file = io.inspect(path); } catch { continue; }
543
+ if (!file.isFile || io.now() - file.mtimeMs < STALE_TEMP_GRACE_MS) continue;
544
+ if (pid === process.pid || io.isProcessAlive(pid)) continue;
545
+
546
+ try {
547
+ io.unlink(path);
548
+ result.removed += 1;
549
+ result.bytesRemoved += file.size;
550
+ } catch {
551
+ // Locked files remain for a later startup. Do not truncate by path: a same-user
552
+ // replacement could turn that fallback into an arbitrary symlink-target write.
553
+ result.failed += 1;
554
+ }
555
+ }
556
+ return result;
557
+ }
558
+
559
+ /**
560
+ * Best-effort disk snapshot so previous_response_id chains survive a proxy restart (the
561
+ * dominant expansion-miss cause: an in-memory-only store dies with the process, and the next
562
+ * chained turn then reaches the upstream as a naked delta). Load is lazy on first store access;
563
+ * persistence is debounced + unref'd so the hot path never blocks and the process can exit.
564
+ * Every disk failure is swallowed — the snapshot is a cache, not a source of truth.
565
+ */
566
+ function ensureLoaded(): void {
567
+ if (loaded) return;
568
+ loaded = true;
569
+ const path = snapshotPath();
570
+ // Atomic writes place their temp beside the RESOLVED target, so a symlinked
571
+ // snapshot (dotfiles-managed config dir) strands temps in the link's real
572
+ // directory where a scan of the literal config dir would never see them.
573
+ // Both locations are swept; they collapse to one when nothing is symlinked.
574
+ // resolveWriteTarget refuses a dangling link; snapshot loading stays independent.
575
+ let resolvedDir = dirname(path);
576
+ try {
577
+ resolvedDir = dirname(resolveWriteTarget(path));
578
+ } catch {
579
+ /* unresolvable link: sweep the literal dir only */
580
+ }
581
+ for (const dir of new Set([dirname(path), resolvedDir])) {
582
+ try {
583
+ recoverStaleResponseStateTemps(dir);
584
+ } catch {
585
+ /* best-effort cleanup only; snapshot loading must remain independent */
586
+ }
587
+ }
588
+ try {
589
+ if (existsSync(path)) {
590
+ // Bound the read BEFORE parse: the 24 MiB write cap constrains snapshots
591
+ // this process wrote, not a pre-existing oversized file. statSync follows
592
+ // symlinks deliberately — readFileSync below follows them too, so the
593
+ // size gate must measure the same target the read would.
594
+ const stat = statSync(path);
595
+ if (!stat.isFile()) {
596
+ // Symlink to a FIFO/device (e.g. /dev/zero): reading would block or
597
+ // return unbounded input. Only regular files are ever parsed.
598
+ } else if (stat.size > SNAPSHOT_FILE_MAX_BYTES) {
599
+ admissionCounters.snapshotOversizedRefusals += 1;
600
+ } else {
601
+ const raw = JSON.parse(readFileSync(path, "utf-8")) as { version?: unknown; states?: unknown };
602
+ if ((raw.version === 1 || raw.version === 2) && Array.isArray(raw.states)) {
603
+ for (const entry of raw.states) {
604
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") continue;
605
+ loadSnapshotEntry(entry[0], entry[1]);
606
+ }
607
+ }
608
+ }
609
+ }
610
+ } catch {
611
+ /* missing/corrupt snapshot: start empty */
612
+ }
613
+ const referenced = new Set<string>();
614
+ for (const state of states.values()) {
615
+ if (state.kind === "spill") referenced.add(state.spill.fileName);
616
+ }
617
+ try { recoverOrphanedResponseSpills(referenced); } catch { /* best effort */ }
618
+ pruneResponses();
619
+ }
620
+
621
+ type SnapshotWriteOutcome = "stable" | "unstable" | "failed";
622
+
623
+ async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome> {
624
+ // Serialize writers so concurrent flush + debounce cannot race on temps / ACL (#612).
625
+ const previous = persistGate;
626
+ let release!: () => void;
627
+ persistGate = new Promise<void>(resolve => { release = resolve; });
628
+ await previous;
629
+ try {
630
+ for (let attempt = 0; attempt < MAX_SNAPSHOT_REWRITE_ATTEMPTS; attempt += 1) {
631
+ const revision = stateRevision;
632
+ const entries: Array<[string, unknown]> = [];
633
+ let total = 0;
634
+ // Newest-first so the most recent chains survive both legacy snapshot caps.
635
+ for (const [id, state] of [...states].reverse()) {
636
+ let persistable: unknown;
637
+ if (state.kind === "resident") {
638
+ const { sizeBytes: _sizeBytes, kind: _kind, ...resident } = state;
639
+ persistable = resident;
640
+ } else {
641
+ const { sizeBytes: _sizeBytes, ...smallState } = state;
642
+ persistable = smallState;
643
+ }
644
+ const persistEntry: [string, unknown] = [id, persistable];
645
+ // UTF-8 bytes, not UTF-16 code units: multibyte items otherwise slip
646
+ // past both snapshot caps at up to 2x the intended size.
647
+ const size = Buffer.byteLength(JSON.stringify(persistEntry), "utf8");
648
+ if (state.kind === "resident" && size > SNAPSHOT_ENTRY_MAX_BYTES) continue;
649
+ if (total + size > SNAPSHOT_TOTAL_MAX_BYTES) break;
650
+ total += size;
651
+ entries.push(persistEntry);
652
+ }
653
+ entries.reverse();
654
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
655
+ try { chmodSync(dirname(path), 0o700); } catch { /* best-effort (e.g. Windows) */ }
656
+ await atomicWriteFileAsync(path, JSON.stringify({ version: 2, states: entries }));
657
+ persistAttemptHookForTests?.();
658
+ if (revision === stateRevision) return "stable";
659
+ }
660
+ return "unstable";
661
+ } catch {
662
+ return "failed";
663
+ } finally {
664
+ release();
665
+ }
666
+ }
667
+
668
+ function drainPendingSpillUnlinks(): void {
669
+ while (pendingSpillUnlinks.length > 0) {
670
+ const ref = pendingSpillUnlinks.shift()!;
671
+ deleteResponseSpill(ref);
672
+ }
673
+ }
674
+
675
+ function schedulePersistAt(path: string, replace = false): void {
676
+ if (persistTimer && !replace) return;
677
+ if (persistTimer) clearTimeout(persistTimer);
678
+ pendingPersistPath = path;
679
+ persistTimer = setTimeout(() => { void persistNow(path); }, SNAPSHOT_DEBOUNCE_MS);
680
+ (persistTimer as { unref?: () => void }).unref?.();
681
+ }
682
+
683
+ async function persistNow(path: string, awaitFollowUp = false): Promise<void> {
684
+ if (persistTimer) {
685
+ clearTimeout(persistTimer);
686
+ persistTimer = null;
687
+ }
688
+ pendingPersistPath = null;
689
+ let outcome = await writeBoundedSnapshot(path);
690
+ if (outcome === "unstable" && awaitFollowUp) {
691
+ if (persistTimer) clearTimeout(persistTimer);
692
+ persistTimer = null;
693
+ pendingPersistPath = null;
694
+ outcome = await writeBoundedSnapshot(path);
695
+ }
696
+ if (outcome === "stable") drainPendingSpillUnlinks();
697
+ else if (outcome === "unstable" && !awaitFollowUp) schedulePersistAt(path, true);
698
+ }
699
+
700
+ function schedulePersist(): void {
701
+ // Resolve the target path NOW: tests (and anything else) may swap OPENCODEX_HOME before the
702
+ // debounce fires, and a late write must land in the home that owned the recorded state.
703
+ schedulePersistAt(snapshotPath());
704
+ }
705
+
706
+ /** Flush any pending debounced snapshot write (graceful shutdown / deterministic tests). */
707
+ export async function flushResponseState(): Promise<void> {
708
+ if (persistTimer) {
709
+ await persistNow(pendingPersistPath ?? snapshotPath(), true);
710
+ return;
711
+ }
712
+ // No pending timer: still await any in-flight write so shutdown does not race (#612).
713
+ await persistGate;
714
+ // A bounded background pass may have scheduled its same-path follow-up while
715
+ // this flush was waiting on the single-flight gate. Shutdown owns one awaited
716
+ // bounded follow-up rather than returning behind that unref'd timer.
717
+ if (persistTimer) await persistNow(pendingPersistPath ?? snapshotPath(), true);
718
+ }
719
+
720
+ function inputItems(input: unknown): unknown[] {
721
+ if (input === undefined) return [];
722
+ if (Array.isArray(input)) return input;
723
+ if (typeof input === "string") return [{ role: "user", content: input }];
724
+ return [input];
725
+ }
726
+
727
+ function pruneResponses(at = now()): void {
728
+ for (const [id, state] of states) {
729
+ if (at - state.createdAt > RESPONSE_TTL_MS) deleteEntry(id);
730
+ }
731
+ while (states.size > MAX_STORED_RESPONSES) {
732
+ const oldest = states.keys().next().value;
733
+ if (!oldest) break;
734
+ deleteEntry(oldest);
735
+ }
736
+ // Unconditional RAM cap. Resident payloads demote durably; stubs/tombstones are
737
+ // deleted only when even their bounded metadata cannot fit the override.
738
+ while (storedResponseBytes > byteCap() && states.size > 0) {
739
+ const oldestResident = [...states].find(([, entry]) => entry.kind === "resident");
740
+ const oldestId = oldestResident?.[0] ?? states.keys().next().value as string | undefined;
741
+ if (!oldestId) break;
742
+ const entry = states.get(oldestId)!;
743
+ if (entry.kind !== "resident") {
744
+ deleteEntry(oldestId);
745
+ continue;
746
+ }
747
+ try {
748
+ const ref = writeResponseSpillDurably(oldestId, {
749
+ createdAt: entry.createdAt,
750
+ items: entry.items,
751
+ ...(entry.providers ? { providers: entry.providers } : {}),
752
+ });
753
+ if (swapResidentForSpill(oldestId, entry, ref)) spillCounters.writes += 1;
754
+ } catch {
755
+ spillCounters.writeFailures += 1;
756
+ replaceWithSpillFailure(oldestId, entry);
757
+ }
758
+ }
759
+ }
760
+
761
+ /** Periodic TTL-only sweep; count/byte eviction remains owned by mutation paths. */
762
+ export function sweepExpiredResponseStates(at = now()): number {
763
+ let removed = 0;
764
+ for (const [id, state] of states) {
765
+ if (at - state.createdAt <= RESPONSE_TTL_MS) continue;
766
+ deleteEntry(id);
767
+ removed += 1;
768
+ }
769
+ if (removed > 0) schedulePersist();
770
+ return removed;
771
+ }
772
+
773
+ export function responseContinuationRetainedStoreSnapshot(): RetainedStoreSnapshot {
774
+ return {
775
+ count: states.size,
776
+ bytes: storedResponseBytes,
777
+ evictableBytes: residentResponseBytes,
778
+ pinnedBytes: Math.max(0, storedResponseBytes - residentResponseBytes),
779
+ oldestAt: oldestResidentAt,
780
+ };
781
+ }
782
+
783
+ export function evictOldestResponseContinuationForBudget(): number {
784
+ if (oldestResidentId === undefined) return 0;
785
+ const id = oldestResidentId;
786
+ const entry = states.get(id);
787
+ if (!entry || entry.kind !== "resident") return 0;
788
+ try {
789
+ const ref = writeResponseSpillDurably(id, {
790
+ createdAt: entry.createdAt,
791
+ items: entry.items,
792
+ ...(entry.providers ? { providers: entry.providers } : {}),
793
+ });
794
+ if (swapResidentForSpill(id, entry, ref)) spillCounters.writes += 1;
795
+ } catch {
796
+ spillCounters.writeFailures += 1;
797
+ replaceWithSpillFailure(id, entry);
798
+ }
799
+ schedulePersist();
800
+ const replacement = states.get(id);
801
+ return !replacement || replacement.kind === "resident"
802
+ ? 0
803
+ : Math.max(0, entry.sizeBytes - replacement.sizeBytes);
804
+ }
805
+
806
+ function materializeEntry(
807
+ id: string,
808
+ entry: StoredResponseState,
809
+ ): { ok: true; state: ResidentResponseState } | { ok: false; failure: PreviousResponseReplayFailure } {
810
+ if (entry.kind === "resident") return { ok: true, state: entry };
811
+ if (entry.kind === "spill-failed") {
812
+ return { ok: false, failure: { code: "previous_response_not_found", reason: "spill_failed" } };
813
+ }
814
+ const result = readResponseSpill(id, entry.spill);
815
+ if (!result.ok) {
816
+ spillCounters.readFailures += 1;
817
+ const failure: PreviousResponseReplayFailure = {
818
+ code: "previous_response_not_found",
819
+ reason: result.reason === "missing"
820
+ ? "spill_missing"
821
+ : result.reason === "too_large"
822
+ ? "spill_too_large"
823
+ : "spill_corrupt",
824
+ };
825
+ replaceWithSpillFailure(id, entry);
826
+ schedulePersist();
827
+ return { ok: false, failure };
828
+ }
829
+ const state = measureResidentEntry(id, {
830
+ createdAt: result.payload.createdAt,
831
+ items: result.payload.items,
832
+ ...(result.payload.providers ? { providers: result.payload.providers } : {}),
833
+ });
834
+ if (!state) {
835
+ spillCounters.readFailures += 1;
836
+ replaceWithSpillFailure(id, entry);
837
+ schedulePersist();
838
+ return { ok: false, failure: { code: "previous_response_not_found", reason: "spill_corrupt" } };
839
+ }
840
+ return { ok: true, state };
841
+ }
842
+
843
+ export function expandPreviousResponseInput(body: unknown): unknown {
844
+ if (!body || typeof body !== "object" || Array.isArray(body)) return body;
845
+ const request = body as Record<string, unknown>;
846
+ const previousId = typeof request.previous_response_id === "string" ? request.previous_response_id : undefined;
847
+ if (!previousId) return body;
848
+ ensureLoaded();
849
+ pruneResponses();
850
+ const previous = states.get(previousId);
851
+ if (!previous) return body;
852
+ const materialized = materializeEntry(previousId, previous);
853
+ if (!materialized.ok) {
854
+ replayFailures.set(request, materialized.failure);
855
+ return body;
856
+ }
857
+ const expanded = {
858
+ ...request,
859
+ input: [...materialized.state.items, ...inputItems(request.input)],
860
+ };
861
+ replayedInputPrefixLengths.set(expanded, materialized.state.items.length);
862
+ return expanded;
863
+ }
864
+
865
+ export function previousResponseReplayFailure(body: unknown): PreviousResponseReplayFailure | undefined {
866
+ if (!body || typeof body !== "object" || Array.isArray(body)) return undefined;
867
+ return replayFailures.get(body);
868
+ }
869
+
870
+ /** Number of leading input items restored from previous_response_id state for this exact body. */
871
+ export function previousResponseReplayPrefixLength(body: unknown): number {
872
+ if (!body || typeof body !== "object" || Array.isArray(body)) return 0;
873
+ return replayedInputPrefixLengths.get(body) ?? 0;
874
+ }
875
+
876
+ export function previousResponseConversationId(responseId: string | undefined): string | undefined {
877
+ return previousResponseProviderState(responseId)?.cursor?.conversationId;
878
+ }
879
+
880
+ export function previousResponseProviderState(responseId: string | undefined): OcxProviderContinuationState | undefined {
881
+ if (!responseId) return undefined;
882
+ ensureLoaded();
883
+ pruneResponses();
884
+ const state = states.get(responseId);
885
+ const providers = state?.kind === "spill-failed" ? undefined : state?.providers;
886
+ return providers ? structuredClone(providers) : undefined;
887
+ }
888
+
889
+ export interface ResponseStateMetrics {
890
+ count: number;
891
+ residentCount: number;
892
+ spillStubCount: number;
893
+ tombstoneCount: number;
894
+ totalBytes: number;
895
+ spillPayloadBytes: number;
896
+ largestBytes: number;
897
+ oldestAgeMs: number;
898
+ spillWrites: number;
899
+ spillWriteFailures: number;
900
+ spillReadFailures: number;
901
+ }
902
+
903
+ /**
904
+ * Observe-only snapshot of the in-RAM continuation store, surfaced via GET /api/system/memory.
905
+ * Additive and side-effect free — it does NOT lazy-load the disk snapshot, prune, or evict — so a
906
+ * diagnostics probe can sample it without perturbing request handling. `totalBytes` reads the
907
+ * running byte counter and `largestBytes` reads each entry's cached `sizeBytes`, so a probe never
908
+ * re-serializes the whole store (a large transient allocation that would fire exactly when memory
909
+ * is already under pressure). This is the seam for deciding whether RAM growth originates in this
910
+ * store (JS heap) or in the runtime allocator (native).
911
+ */
912
+ export function responseStateMetrics(): ResponseStateMetrics {
913
+ const at = now();
914
+ let largestBytes = 0;
915
+ let oldestCreatedAt = at;
916
+ let residentCount = 0;
917
+ let spillStubCount = 0;
918
+ let tombstoneCount = 0;
919
+ let spillPayloadBytes = 0;
920
+ for (const state of states.values()) {
921
+ const bytes = state.sizeBytes;
922
+ if (bytes > largestBytes) largestBytes = bytes;
923
+ if (state.createdAt < oldestCreatedAt) oldestCreatedAt = state.createdAt;
924
+ if (state.kind === "resident") {
925
+ residentCount += 1;
926
+ } else if (state.kind === "spill") {
927
+ spillStubCount += 1;
928
+ spillPayloadBytes += state.spill.payloadBytes;
929
+ } else tombstoneCount += 1;
930
+ }
931
+ return {
932
+ count: states.size,
933
+ residentCount,
934
+ spillStubCount,
935
+ tombstoneCount,
936
+ totalBytes: storedResponseBytes,
937
+ spillPayloadBytes,
938
+ largestBytes,
939
+ oldestAgeMs: states.size > 0 ? at - oldestCreatedAt : 0,
940
+ spillWrites: spillCounters.writes,
941
+ spillWriteFailures: spillCounters.writeFailures,
942
+ spillReadFailures: spillCounters.readFailures,
943
+ };
944
+ }
945
+
946
+ /**
947
+ * Cache completed output and max_output_tokens partial output for previous_response_id replay.
948
+ * Content-filtered incomplete and failed output are not authoritative replay history.
949
+ */
950
+ export function rememberResponseState(
951
+ requestBody: unknown,
952
+ response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
953
+ providerState?: OcxProviderContinuationState | string,
954
+ opts?: { force?: boolean },
955
+ ): void {
956
+ if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
957
+ const request = requestBody as Record<string, unknown>;
958
+ // `force` bypasses only the store:false skip: Codex sends `store:false` on every non-Azure
959
+ // HTTP request (and WS inherits it), yet its WS turns still chain with previous_response_id.
960
+ // The passthrough branch records with force so those chains can be expanded locally; the
961
+ // store stays in-memory with a 1h TTL, so this is a proxy-internal continuation cache, not
962
+ // real server-side response storage.
963
+ if (request.store === false && !opts?.force) return;
964
+ if (typeof response.id !== "string" || !Array.isArray(response.output)) return;
965
+ if (response.status === "incomplete") {
966
+ const details = response.incomplete_details;
967
+ if (!details || typeof details !== "object" || Array.isArray(details)
968
+ || (details as { reason?: unknown }).reason !== "max_output_tokens") return;
969
+ } else if (response.status !== undefined && response.status !== "completed") return;
970
+ ensureLoaded();
971
+ const normalizedProviderState: OcxProviderContinuationState = typeof providerState === "string"
972
+ ? { cursor: { conversationId: providerState } }
973
+ : structuredClone(providerState ?? {});
974
+ if (normalizedProviderState.cursor?.conversationId) {
975
+ normalizedProviderState.cursor.checkpointUsable = !response.output.some(item => {
976
+ return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
977
+ });
978
+ }
979
+ setResidentEntry(response.id, {
980
+ createdAt: now(),
981
+ items: [...inputItems(request.input), ...response.output],
982
+ // Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME
983
+ // Cursor conversation (multi-turn continuation). Separately track whether Cursor's own
984
+ // checkpoint/cache is safe to reuse: a turn that ended with a pending client tool call produced an
985
+ // incomplete agent turn on the Cursor side (we suspended without a real mcpResult), so its
986
+ // checkpoint must not be reused — but the conversation id string itself is still valid.
987
+ ...(Object.keys(normalizedProviderState).length > 0 ? { providers: normalizedProviderState } : {}),
988
+ });
989
+ enforceAppOwnedMemoryBudget();
990
+ schedulePersist();
991
+ }
992
+
993
+ /** Test-only persistence churn hook; invoked after each atomic snapshot rewrite. */
994
+ export function setResponseStatePersistAttemptHookForTests(hook: (() => void) | null): void {
995
+ persistAttemptHookForTests = hook;
996
+ }
997
+
998
+ /** Test-only: deterministically run the pending background debounce pass. */
999
+ export async function runPendingResponseStatePersistForTests(): Promise<void> {
1000
+ if (!persistTimer) return;
1001
+ await persistNow(pendingPersistPath ?? snapshotPath());
1002
+ }
1003
+
1004
+ /** Test-only: observe whether a debounce/follow-up pass is pending. */
1005
+ export function responseStatePersistPendingForTests(): boolean {
1006
+ return persistTimer !== null;
1007
+ }
1008
+
1009
+ /** Memory-only reset (simulates a process restart: the snapshot file survives). */
1010
+ export function clearResponseStateMemoryForTests(): void {
1011
+ if (persistTimer) {
1012
+ clearTimeout(persistTimer);
1013
+ persistTimer = null;
1014
+ }
1015
+ pendingPersistPath = null;
1016
+ states.clear();
1017
+ storedResponseBytes = 0;
1018
+ residentResponseBytes = 0;
1019
+ oldestResidentId = undefined;
1020
+ oldestResidentAt = null;
1021
+ stateRevision = 0;
1022
+ pendingSpillUnlinks.length = 0;
1023
+ spillCounters.writes = 0;
1024
+ spillCounters.writeFailures = 0;
1025
+ spillCounters.readFailures = 0;
1026
+ persistAttemptHookForTests = null;
1027
+ loaded = false;
1028
+ }
1029
+
1030
+ export function clearResponseStateForTests(): void {
1031
+ for (const entry of states.values()) deleteOwnedSpills(entry);
1032
+ clearResponseStateMemoryForTests();
1033
+ try {
1034
+ unlinkSync(snapshotPath());
1035
+ } catch {
1036
+ /* no snapshot on disk */
1037
+ }
1038
+ try { rmSync(responseSpillDirectory(), { recursive: true, force: true }); } catch { /* no spill directory */ }
1039
+ }