@iislee/opencodex 2.11.0 → 2.31.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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -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 +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -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 +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -1,6 +1,7 @@
1
- import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
1
+ import { chmodSync, existsSync, lstatSync, mkdirSync, opendirSync, readFileSync, rmSync, statSync, unlinkSync } from "node:fs";
2
+ import { uptime } from "node:os";
2
3
  import { dirname, join } from "node:path";
3
- import { atomicWriteFileAsync, getConfigDir } from "../config";
4
+ import { atomicWriteFileAsync, getConfigDir, resolveWriteTarget } from "../config";
4
5
  import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
5
6
  import type { OcxProviderContinuationState } from "../types";
6
7
  import {
@@ -9,6 +10,7 @@ import {
9
10
  readResponseSpill,
10
11
  recoverOrphanedResponseSpills,
11
12
  responseSpillDirectory,
13
+ responseSpillPayloadCap,
12
14
  type ResponseSpillRef,
13
15
  writeResponseSpillDurably,
14
16
  } from "./spill-store";
@@ -23,16 +25,34 @@ export const MAX_STORED_RESPONSE_BYTES = 64 * 1024 * 1024;
23
25
  /** Legacy snapshot selection only. Spill demotion is governed solely by the RAM cap above. */
24
26
  const SNAPSHOT_ENTRY_MAX_BYTES = 2 * 1024 * 1024;
25
27
  const SNAPSHOT_TOTAL_MAX_BYTES = 24 * 1024 * 1024;
28
+ /** Refuse-to-parse ceiling for an existing snapshot file (above the 24 MiB write
29
+ * bound, so anything we wrote ourselves always loads; guards against externally
30
+ * planted or pre-cap unbounded files being parsed whole). */
31
+ const SNAPSHOT_FILE_MAX_BYTES = 32 * 1024 * 1024;
26
32
  const STALE_TEMP_GRACE_MS = 15 * 60 * 1_000;
27
33
  const STALE_TEMP_MAX_ENTRIES = 4_096;
28
34
  const STALE_TEMP_MAX_CLEANUPS = 512;
35
+ /** Absorbs `os.uptime()` granularity only. It is deliberately NOT the safety margin:
36
+ * the unconditional 15-minute grace above is (see the boot floor in the scan loop). */
37
+ const BOOT_FLOOR_SKEW_MS = 60 * 1_000;
38
+ /** Per-tick budget for the periodic reclaim. Smaller than the startup budget because the
39
+ * periodic pass runs synchronously on the serving process's event loop every 60 s. */
40
+ const PERIODIC_TEMP_MAX_ENTRIES = 512;
41
+ const PERIODIC_TEMP_MAX_CLEANUPS = 64;
42
+ /** Wall-clock ceiling for one periodic scan. An entry cap bounds syscalls, not time: on a
43
+ * network-mounted config dir each `lstat` can cost 10-20 ms, which would stall in-flight
44
+ * streams. Reclaim is idempotent, so a truncated tick simply resumes on the next one. */
45
+ const PERIODIC_TEMP_SCAN_DEADLINE_MS = 25;
29
46
  const RESPONSE_STATE_TEMP_NAME = /^responses-state\.json\.ocx\.(\d+)\.(\d+)\.tmp$/;
30
47
  const MAX_SNAPSHOT_REWRITE_ATTEMPTS = 4;
31
48
 
32
49
  interface ResidentResponseState {
33
50
  kind: "resident";
34
51
  createdAt: number;
52
+ clientThreadId?: string;
35
53
  items: unknown[];
54
+ /** Index in `items` where provider output begins; see clientCarriedPrefixLength. */
55
+ providerOutputStart?: number;
36
56
  providers?: OcxProviderContinuationState;
37
57
  sizeBytes: number;
38
58
  }
@@ -40,6 +60,9 @@ interface ResidentResponseState {
40
60
  interface SpilledResponseState {
41
61
  kind: "spill";
42
62
  createdAt: number;
63
+ clientThreadId?: string;
64
+ /** Mirrors the spilled payload boundary so a spilled entry keeps its anchor. */
65
+ providerOutputStart?: number;
43
66
  providers?: OcxProviderContinuationState;
44
67
  spill: ResponseSpillRef;
45
68
  sizeBytes: number;
@@ -56,10 +79,11 @@ type ResidentInput = Omit<ResidentResponseState, "kind" | "sizeBytes">;
56
79
 
57
80
  export type PreviousResponseReplayFailure = {
58
81
  code: "previous_response_not_found";
59
- reason: "spill_missing" | "spill_corrupt" | "spill_failed";
82
+ reason: "spill_missing" | "spill_corrupt" | "spill_failed" | "spill_too_large";
60
83
  };
61
84
 
62
85
  const states = new Map<string, StoredResponseState>();
86
+ const replayScopeMismatches = new WeakSet<object>();
63
87
  let storedResponseBytes = 0;
64
88
  let residentResponseBytes = 0;
65
89
  let oldestResidentId: string | undefined;
@@ -67,6 +91,20 @@ let oldestResidentAt: number | null = null;
67
91
  let byteCapOverride: number | null = null;
68
92
  let stateRevision = 0;
69
93
  const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
94
+ /**
95
+ * Admission-boundary observability (test-visible). directSpills: oversized
96
+ * candidates routed straight to durable spill without a resident stay or
97
+ * unrelated demotion. oversizedDrops: candidates above the single-spill
98
+ * payload ceiling, tombstoned instead of retained. snapshotOversizedRefusals:
99
+ * snapshot files refused before parse.
100
+ */
101
+ const admissionCounters = { directSpills: 0, oversizedDrops: 0, snapshotOversizedRefusals: 0 };
102
+ let replayScopeMismatchDrops = 0;
103
+
104
+ /** Test-only: admission-boundary counters (proves the new paths fire). */
105
+ export function responseAdmissionCountersForTests(): Readonly<typeof admissionCounters> {
106
+ return admissionCounters;
107
+ }
70
108
  // Superseded spill generations awaiting a durable snapshot before unlink
71
109
  // (review C1-1: unlinking at swap time races a crash against the debounced
72
110
  // snapshot — the reloaded OLD stub would point at a deleted file).
@@ -106,7 +144,9 @@ function measureResidentEntry(id: string, entry: ResidentInput): ResidentRespons
106
144
  const sizeBytes = serializedBytes({
107
145
  responseId: id,
108
146
  createdAt: entry.createdAt,
147
+ ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
109
148
  items: entry.items,
149
+ ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
110
150
  ...(entry.providers ? { providers: entry.providers } : {}),
111
151
  });
112
152
  return sizeBytes === null ? null : { kind: "resident", ...entry, sizeBytes };
@@ -177,12 +217,28 @@ function deleteEntry(id: string, options: { deleteSpill?: boolean } = {}): void
177
217
  if (options.deleteSpill !== false) deleteOwnedSpills(existing);
178
218
  }
179
219
 
180
- function replaceWithSpillFailure(id: string, expected?: StoredResponseState): void {
220
+ function replaceWithSpillFailure(
221
+ id: string,
222
+ expected?: StoredResponseState,
223
+ options: { deferSpillUnlink?: boolean } = {},
224
+ ): void {
181
225
  const existing = states.get(id);
182
226
  if (expected && existing !== expected) return;
183
227
  const failed = tombstone(id, expected?.createdAt ?? existing?.createdAt ?? now());
184
228
  if (replaceMapEntry(id, failed, expected)) {
185
- if (existing) deleteOwnedSpills(existing);
229
+ if (existing) {
230
+ if (options.deferSpillUnlink && existing.kind === "spill") {
231
+ // Crash consistency (same rule as replaceSpillEntryAtomically): the old
232
+ // durable snapshot still references this generation until the tombstone
233
+ // itself is durable — queue the unlink for the next stable persist.
234
+ pendingSpillUnlinks.push(existing.spill);
235
+ while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
236
+ deleteResponseSpill(pendingSpillUnlinks.shift()!);
237
+ }
238
+ } else {
239
+ deleteOwnedSpills(existing);
240
+ }
241
+ }
186
242
  }
187
243
  }
188
244
 
@@ -190,6 +246,7 @@ function swapResidentForSpill(id: string, expected: ResidentResponseState, ref:
190
246
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
191
247
  kind: "spill",
192
248
  createdAt: expected.createdAt,
249
+ ...(expected.clientThreadId ? { clientThreadId: expected.clientThreadId } : {}),
193
250
  ...(expected.providers ? { providers: expected.providers } : {}),
194
251
  spill: ref,
195
252
  };
@@ -210,12 +267,16 @@ function replaceSpillEntryAtomically(
210
267
  try {
211
268
  const ref = writeResponseSpillDurably(id, {
212
269
  createdAt: candidate.createdAt,
270
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
213
271
  items: candidate.items,
272
+ ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
214
273
  ...(candidate.providers ? { providers: candidate.providers } : {}),
215
274
  });
216
275
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
217
276
  kind: "spill",
218
277
  createdAt: candidate.createdAt,
278
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
279
+ ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
219
280
  ...(candidate.providers ? { providers: candidate.providers } : {}),
220
281
  spill: ref,
221
282
  };
@@ -236,7 +297,9 @@ function replaceSpillEntryAtomically(
236
297
  }
237
298
  } catch {
238
299
  spillCounters.writeFailures += 1;
239
- replaceWithSpillFailure(id, expected);
300
+ // deferSpillUnlink: the durable snapshot may still reference the old
301
+ // generation; deleting it now would strand the old stub after a crash.
302
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
240
303
  }
241
304
  }
242
305
 
@@ -251,6 +314,11 @@ function setResidentEntry(id: string, entry: ResidentInput): void {
251
314
  pruneResponses();
252
315
  return;
253
316
  }
317
+ if (candidate.sizeBytes > byteCap()) {
318
+ admitOversizedCandidate(id, candidate, expected);
319
+ pruneResponses();
320
+ return;
321
+ }
254
322
  if (expected?.kind === "spill") {
255
323
  replaceSpillEntryAtomically(id, expected, candidate);
256
324
  pruneResponses();
@@ -260,9 +328,76 @@ function setResidentEntry(id: string, entry: ResidentInput): void {
260
328
  pruneResponses();
261
329
  }
262
330
 
263
- // Expansion provenance must stay proxy-private: a WeakMap distinguishes replayed history from the
331
+ /**
332
+ * Admission boundary for candidates that can never fit as resident (larger
333
+ * than the whole resident-map cap). Writes them DIRECTLY to durable spill and
334
+ * installs only the stub — the oversized candidate never becomes resident and
335
+ * no unrelated resident is demoted to make room for it. Candidates above the
336
+ * single-spill payload ceiling are tombstoned instead: retaining a spill the
337
+ * replay ceiling would refuse to read is write-only waste.
338
+ */
339
+ function admitOversizedCandidate(
340
+ id: string,
341
+ candidate: ResidentResponseState,
342
+ expected?: StoredResponseState,
343
+ ): void {
344
+ if (candidate.sizeBytes > responseSpillPayloadCap()) {
345
+ admissionCounters.oversizedDrops += 1;
346
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
347
+ return;
348
+ }
349
+ try {
350
+ const ref = writeResponseSpillDurably(id, {
351
+ createdAt: candidate.createdAt,
352
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
353
+ items: candidate.items,
354
+ ...(candidate.providerOutputStart !== undefined ? { providerOutputStart: candidate.providerOutputStart } : {}),
355
+ ...(candidate.providers ? { providers: candidate.providers } : {}),
356
+ });
357
+ // Enforce the ceiling against the REAL envelope: the spill payload adds
358
+ // the {version, responseId, ...} wrapper, so a candidate within the
359
+ // wrapper's size of the cap would otherwise be retained unreadably.
360
+ if (ref.payloadBytes > responseSpillPayloadCap()) {
361
+ deleteResponseSpill(ref);
362
+ admissionCounters.oversizedDrops += 1;
363
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
364
+ return;
365
+ }
366
+ const base: Omit<SpilledResponseState, "sizeBytes"> = {
367
+ kind: "spill",
368
+ createdAt: candidate.createdAt,
369
+ ...(candidate.clientThreadId ? { clientThreadId: candidate.clientThreadId } : {}),
370
+ ...(candidate.providers ? { providers: candidate.providers } : {}),
371
+ spill: ref,
372
+ };
373
+ const next: SpilledResponseState = { ...base, sizeBytes: stubSize(id, base) };
374
+ if (!replaceMapEntry(id, next, expected)) {
375
+ deleteResponseSpill(ref);
376
+ return;
377
+ }
378
+ spillCounters.writes += 1;
379
+ admissionCounters.directSpills += 1;
380
+ noteStubSwapForTest();
381
+ if (expected?.kind === "spill") {
382
+ // Same deferred-unlink rule as replaceSpillEntryAtomically: the new stub
383
+ // is durable only after the debounced snapshot, so the old generation
384
+ // stays until a stable persist drains the queue.
385
+ pendingSpillUnlinks.push(expected.spill);
386
+ while (pendingSpillUnlinks.length > PENDING_SPILL_UNLINKS_MAX) {
387
+ deleteResponseSpill(pendingSpillUnlinks.shift()!);
388
+ }
389
+ }
390
+ } catch {
391
+ spillCounters.writeFailures += 1;
392
+ replaceWithSpillFailure(id, expected, { deferSpillUnlink: true });
393
+ }
394
+ }
395
+
396
+ // Replay provenance must stay proxy-private: a WeakMap distinguishes replayed history from the
264
397
  // newly appended input suffix without adding an unknown field that native passthrough could send
265
- // upstream. The parser uses this boundary to acknowledge historical compaction markers exactly once.
398
+ // upstream. The parser uses this boundary to acknowledge historical compaction markers exactly
399
+ // once. It records the boundary whether the proxy prepended the history or the client already
400
+ // carried it — the boundary is the same either way, and only its provenance differs.
266
401
  const replayedInputPrefixLengths = new WeakMap<object, number>();
267
402
  const replayFailures = new WeakMap<object, PreviousResponseReplayFailure>();
268
403
  let loaded = false;
@@ -282,6 +417,7 @@ function snapshotPath(): string {
282
417
 
283
418
  interface LegacySnapshotState {
284
419
  createdAt?: unknown;
420
+ clientThreadId?: unknown;
285
421
  items?: unknown;
286
422
  providers?: OcxProviderContinuationState;
287
423
  conversationId?: unknown;
@@ -302,11 +438,26 @@ function loadSnapshotEntry(id: string, value: unknown): void {
302
438
  if (!value || typeof value !== "object" || Array.isArray(value)) return;
303
439
  const rec = value as LegacySnapshotState & { kind?: unknown; spill?: unknown };
304
440
  if (typeof rec.createdAt !== "number" || !Number.isFinite(rec.createdAt)) return;
441
+ const clientThreadId = typeof rec.clientThreadId === "string" && rec.clientThreadId.trim().length > 0
442
+ ? rec.clientThreadId.trim()
443
+ : undefined;
444
+ // A malformed boundary degrades to "never skip" rather than to a bad index: an untrusted
445
+ // snapshot must not be able to authorize dropping conversation history.
446
+ const anchorFor = (itemCount: number): number | undefined => {
447
+ const raw = (rec as { providerOutputStart?: unknown }).providerOutputStart;
448
+ return Number.isSafeInteger(raw) && (raw as number) >= 0 && (raw as number) <= itemCount
449
+ ? raw as number
450
+ : undefined;
451
+ };
305
452
  if (rec.kind === "spill") {
306
453
  if (!isSpillRef(rec.spill)) return;
307
454
  const base: Omit<SpilledResponseState, "sizeBytes"> = {
308
455
  kind: "spill",
309
456
  createdAt: rec.createdAt,
457
+ ...(clientThreadId ? { clientThreadId } : {}),
458
+ // Item count is unknown until materialization, so accept any non-negative integer
459
+ // here; the spill payload validator re-checks it against the real array.
460
+ ...(anchorFor(Number.MAX_SAFE_INTEGER) !== undefined ? { providerOutputStart: anchorFor(Number.MAX_SAFE_INTEGER) } : {}),
310
461
  ...(rec.providers ? { providers: rec.providers } : {}),
311
462
  spill: rec.spill,
312
463
  };
@@ -331,11 +482,23 @@ function loadSnapshotEntry(id: string, value: unknown): void {
331
482
  : undefined);
332
483
  const resident = measureResidentEntry(id, {
333
484
  createdAt: rec.createdAt,
485
+ ...(clientThreadId ? { clientThreadId } : {}),
334
486
  items: rec.items,
487
+ ...(anchorFor(rec.items.length) !== undefined ? { providerOutputStart: anchorFor(rec.items.length) } : {}),
335
488
  ...(providers ? { providers } : {}),
336
489
  });
337
- if (resident) replaceMapEntry(id, resident);
338
- else replaceMapEntry(id, tombstone(id, rec.createdAt));
490
+ if (!resident) {
491
+ replaceMapEntry(id, tombstone(id, rec.createdAt));
492
+ return;
493
+ }
494
+ // Same admission boundary as live writes: an oversized snapshot row goes
495
+ // straight to spill (or tombstone above the payload ceiling) instead of
496
+ // entering the resident map and demoting unrelated rows on the first prune.
497
+ if (resident.sizeBytes > byteCap()) {
498
+ admitOversizedCandidate(id, resident, undefined);
499
+ return;
500
+ }
501
+ replaceMapEntry(id, resident);
339
502
  }
340
503
 
341
504
  export interface ResponseStateTempRecoveryResult {
@@ -343,19 +506,37 @@ export interface ResponseStateTempRecoveryResult {
343
506
  removed: number;
344
507
  failed: number;
345
508
  bytesRemoved: number;
509
+ /** Entries that passed EVERY gate and would be reclaimed. In a dry run nothing is
510
+ * unlinked, so this is the only honest count to show an operator: `matched` is
511
+ * incremented before the file-type, age, boot-floor, and liveness gates. */
512
+ eligible: number;
513
+ /** Total size of the `eligible` entries. */
514
+ eligibleBytes: number;
515
+ /** The scan stopped on a budget (entry cap, cleanup cap, or deadline) rather than reaching
516
+ * the end of the directory, so the counts below describe a prefix of the backlog and not
517
+ * the backlog. `eligible > removed + failed` cannot express this: outside a dry run every
518
+ * eligible entry is unlinked or failed on the same iteration, so the two are always equal
519
+ * and a comparison between them is dead code. */
520
+ truncated: boolean;
346
521
  }
347
522
 
348
523
  interface ResponseStateTempRecoveryIO {
349
524
  now: () => number;
525
+ /** Approximate epoch ms of the current boot; see the boot floor in the scan loop. */
526
+ bootTime: () => number;
350
527
  list: (dir: string) => Iterable<string>;
351
528
  inspect: (path: string) => { isFile: boolean; mtimeMs: number; size: number };
352
529
  isProcessAlive: (pid: number) => boolean;
353
530
  unlink: (path: string) => void;
354
531
  }
355
532
 
356
- type ResponseStateTempRecoveryOptions = Partial<ResponseStateTempRecoveryIO> & {
533
+ export type ResponseStateTempRecoveryOptions = Partial<ResponseStateTempRecoveryIO> & {
357
534
  maxEntries?: number;
358
535
  maxCleanups?: number;
536
+ /** Wall-clock ceiling for the scan, or null/undefined for no deadline (startup path). */
537
+ deadlineMs?: number | null;
538
+ /** Report only: apply every gate, count what would be reclaimed, unlink nothing. */
539
+ dryRun?: boolean;
359
540
  };
360
541
 
361
542
  function processIsAlive(pid: number): boolean {
@@ -372,6 +553,7 @@ function processIsAlive(pid: number): boolean {
372
553
 
373
554
  const responseStateTempRecoveryIO: ResponseStateTempRecoveryIO = {
374
555
  now: Date.now,
556
+ bootTime: () => Date.now() - uptime() * 1_000,
375
557
  list: function* list(dir) {
376
558
  const handle = opendirSync(dir);
377
559
  try {
@@ -398,26 +580,56 @@ export function recoverStaleResponseStateTemps(
398
580
  dir = getConfigDir(),
399
581
  options: ResponseStateTempRecoveryOptions = {},
400
582
  ): ResponseStateTempRecoveryResult {
401
- const { maxEntries = STALE_TEMP_MAX_ENTRIES, maxCleanups = STALE_TEMP_MAX_CLEANUPS, ...overrides } = options;
583
+ const {
584
+ maxEntries = STALE_TEMP_MAX_ENTRIES,
585
+ maxCleanups = STALE_TEMP_MAX_CLEANUPS,
586
+ deadlineMs = null,
587
+ dryRun = false,
588
+ ...overrides
589
+ } = options;
402
590
  const io = { ...responseStateTempRecoveryIO, ...overrides };
403
591
  const result: ResponseStateTempRecoveryResult = {
404
592
  matched: 0,
405
593
  removed: 0,
406
594
  failed: 0,
407
595
  bytesRemoved: 0,
596
+ eligible: 0,
597
+ eligibleBytes: 0,
598
+ truncated: false,
408
599
  };
600
+ const startedAt = io.now();
601
+ // One probe per scan, not one per entry. A non-finite or future-dated boot is anomalous, and
602
+ // clamping it to "now" would be the WORST response: the floor would then retire the liveness
603
+ // probe for every file older than the skew, which is every file past the grace. Disable it
604
+ // instead -- an absent floor only costs a missed reclaim, never a wrong one.
605
+ const rawBoot = io.bootTime();
606
+ const bootMs = Number.isFinite(rawBoot) && rawBoot <= startedAt ? rawBoot : Number.NEGATIVE_INFINITY;
409
607
  let names: Iterable<string>;
410
608
  try { names = io.list(dir); } catch { return result; }
411
609
  let iterator: Iterator<string>;
412
610
  try { iterator = names[Symbol.iterator](); } catch { return result; }
413
611
  let scanned = 0;
612
+ // Every early exit runs through this. The production `list` is a generator that closes its
613
+ // directory handle in a `finally`, and a `finally` does NOT run when the consumer simply
614
+ // stops calling `next()` -- only `return()` resumes the generator to completion. Breaking
615
+ // out of the loop directly therefore leaked one directory handle per truncated scan, and the
616
+ // periodic reclaim truncates on purpose (entry cap, cleanup cap, deadline), so on a slow
617
+ // filesystem that is a leak per tick, forever.
618
+ const stopScan = (): ResponseStateTempRecoveryResult => {
619
+ try { iterator.return?.(); } catch { /* closing is best-effort; never fail a reclaim on it */ }
620
+ return result;
621
+ };
414
622
  for (;;) {
415
623
  let next: IteratorResult<string>;
416
624
  try { next = iterator.next(); } catch { return result; }
417
625
  if (next.done) break;
418
626
  const name = next.value;
419
627
  scanned += 1;
420
- if (scanned > maxEntries || result.removed + result.failed >= maxCleanups) break;
628
+ // A dry run performs no cleanups, so bounding it by the cleanup budget would truncate
629
+ // the very report an operator uses to size the problem.
630
+ if (scanned > maxEntries) { result.truncated = true; return stopScan(); }
631
+ if (!dryRun && result.removed + result.failed >= maxCleanups) { result.truncated = true; return stopScan(); }
632
+ if (deadlineMs !== null && io.now() - startedAt > deadlineMs) { result.truncated = true; return stopScan(); }
421
633
  const match = RESPONSE_STATE_TEMP_NAME.exec(name);
422
634
  if (!match) continue;
423
635
  result.matched += 1;
@@ -428,13 +640,34 @@ export function recoverStaleResponseStateTemps(
428
640
  let file: ReturnType<ResponseStateTempRecoveryIO["inspect"]>;
429
641
  try { file = io.inspect(path); } catch { continue; }
430
642
  if (!file.isFile || io.now() - file.mtimeMs < STALE_TEMP_GRACE_MS) continue;
431
- if (pid === process.pid || io.isProcessAlive(pid)) continue;
643
+ // Boot floor. After a reboot the original writer's pid is routinely reused, which makes
644
+ // the liveness skip PERMANENT: the 15-minute grace above is a lower bound and never
645
+ // expires it, so the file is skipped on every future pass forever. A temp older than
646
+ // this boot cannot be owned by the pid we would probe, so the probe is vacuous and we
647
+ // retire it. This does NOT claim the file is provably dead: under a shared-volume
648
+ // container, suspend-excluding uptime, or a network config dir the computed boot can
649
+ // land after the real one. The unconditional 15-minute grace above remains the safety
650
+ // floor, and this process's own temps are never touched.
651
+ const predatesBoot = file.mtimeMs < bootMs - BOOT_FLOOR_SKEW_MS;
652
+ if (pid === process.pid) continue;
653
+ if (!predatesBoot && io.isProcessAlive(pid)) continue;
654
+
655
+ result.eligible += 1;
656
+ result.eligibleBytes += file.size;
657
+ if (dryRun) continue;
432
658
 
433
659
  try {
434
660
  io.unlink(path);
435
661
  result.removed += 1;
436
662
  result.bytesRemoved += file.size;
437
- } catch {
663
+ } catch (error) {
664
+ // Another proxy sharing this config dir may have won the race. A file that is already
665
+ // gone is reclaimed, not a failure -- reporting it as one would surface "in use or
666
+ // locked" to an operator for a file nobody holds.
667
+ if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
668
+ result.removed += 1;
669
+ continue;
670
+ }
438
671
  // Locked files remain for a later startup. Do not truncate by path: a same-user
439
672
  // replacement could turn that fallback into an arbitrary symlink-target write.
440
673
  result.failed += 1;
@@ -443,6 +676,23 @@ export function recoverStaleResponseStateTemps(
443
676
  return result;
444
677
  }
445
678
 
679
+ /**
680
+ * Literal config dir plus the snapshot's resolved dir. Atomic writes place their temp beside
681
+ * the RESOLVED target, so a symlinked snapshot (dotfiles-managed config dir) strands temps in
682
+ * the link's real directory where a scan of the literal dir would never see them. The two
683
+ * collapse to one when nothing is symlinked.
684
+ */
685
+ function responseStateSweepDirectories(): Set<string> {
686
+ const path = snapshotPath();
687
+ let resolvedDir = dirname(path);
688
+ try {
689
+ resolvedDir = dirname(resolveWriteTarget(path));
690
+ } catch {
691
+ /* unresolvable link: sweep the literal dir only */
692
+ }
693
+ return new Set([dirname(path), resolvedDir]);
694
+ }
695
+
446
696
  /**
447
697
  * Best-effort disk snapshot so previous_response_id chains survive a proxy restart (the
448
698
  * dominant expansion-miss cause: an in-memory-only store dies with the process, and the next
@@ -454,18 +704,43 @@ function ensureLoaded(): void {
454
704
  if (loaded) return;
455
705
  loaded = true;
456
706
  const path = snapshotPath();
707
+ // Atomic writes place their temp beside the RESOLVED target, so a symlinked
708
+ // snapshot (dotfiles-managed config dir) strands temps in the link's real
709
+ // directory where a scan of the literal config dir would never see them.
710
+ // Both locations are swept; they collapse to one when nothing is symlinked.
711
+ // resolveWriteTarget refuses a dangling link; snapshot loading stays independent.
712
+ let resolvedDir = dirname(path);
457
713
  try {
458
- recoverStaleResponseStateTemps(dirname(path));
714
+ resolvedDir = dirname(resolveWriteTarget(path));
459
715
  } catch {
460
- /* best-effort cleanup only; snapshot loading must remain independent */
716
+ /* unresolvable link: sweep the literal dir only */
717
+ }
718
+ for (const dir of new Set([dirname(path), resolvedDir])) {
719
+ try {
720
+ recoverStaleResponseStateTemps(dir);
721
+ } catch {
722
+ /* best-effort cleanup only; snapshot loading must remain independent */
723
+ }
461
724
  }
462
725
  try {
463
726
  if (existsSync(path)) {
464
- const raw = JSON.parse(readFileSync(path, "utf-8")) as { version?: unknown; states?: unknown };
465
- if ((raw.version === 1 || raw.version === 2) && Array.isArray(raw.states)) {
466
- for (const entry of raw.states) {
467
- if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") continue;
468
- loadSnapshotEntry(entry[0], entry[1]);
727
+ // Bound the read BEFORE parse: the 24 MiB write cap constrains snapshots
728
+ // this process wrote, not a pre-existing oversized file. statSync follows
729
+ // symlinks deliberately readFileSync below follows them too, so the
730
+ // size gate must measure the same target the read would.
731
+ const stat = statSync(path);
732
+ if (!stat.isFile()) {
733
+ // Symlink to a FIFO/device (e.g. /dev/zero): reading would block or
734
+ // return unbounded input. Only regular files are ever parsed.
735
+ } else if (stat.size > SNAPSHOT_FILE_MAX_BYTES) {
736
+ admissionCounters.snapshotOversizedRefusals += 1;
737
+ } else {
738
+ const raw = JSON.parse(readFileSync(path, "utf-8")) as { version?: unknown; states?: unknown };
739
+ if ((raw.version === 1 || raw.version === 2) && Array.isArray(raw.states)) {
740
+ for (const entry of raw.states) {
741
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") continue;
742
+ loadSnapshotEntry(entry[0], entry[1]);
743
+ }
469
744
  }
470
745
  }
471
746
  }
@@ -504,7 +779,9 @@ async function writeBoundedSnapshot(path: string): Promise<SnapshotWriteOutcome>
504
779
  persistable = smallState;
505
780
  }
506
781
  const persistEntry: [string, unknown] = [id, persistable];
507
- const size = JSON.stringify(persistEntry).length;
782
+ // UTF-8 bytes, not UTF-16 code units: multibyte items otherwise slip
783
+ // past both snapshot caps at up to 2x the intended size.
784
+ const size = Buffer.byteLength(JSON.stringify(persistEntry), "utf8");
508
785
  if (state.kind === "resident" && size > SNAPSHOT_ENTRY_MAX_BYTES) continue;
509
786
  if (total + size > SNAPSHOT_TOTAL_MAX_BYTES) break;
510
787
  total += size;
@@ -584,6 +861,94 @@ function inputItems(input: unknown): unknown[] {
584
861
  return [input];
585
862
  }
586
863
 
864
+ /** Hard cap for canonicalizing ANY item. Past it, the item is not comparable. */
865
+ const REPLAY_FINGERPRINT_MAX_BYTES = 8 * 1024;
866
+ /** Depth ceiling so a pathologically nested item cannot blow the canonicalizer. */
867
+ const REPLAY_FINGERPRINT_MAX_DEPTH = 64;
868
+
869
+ let replayOverlapSkips = 0;
870
+
871
+ /**
872
+ * Canonical, order-stable fingerprint for one input item, or null when the item cannot be
873
+ * compared safely.
874
+ *
875
+ * Byte-counted DURING the walk rather than serialize-then-measure: a tool result can be
876
+ * megabytes and this runs on the request path, so the point of the cap is to stop early,
877
+ * not to discover afterwards that we should have. Object keys are sorted so two
878
+ * semantically identical items cannot differ by key order alone.
879
+ *
880
+ * The cap applies to EVERY item. An `id`/`call_id` is additional occurrence evidence, never
881
+ * a substitute for content equality, so an over-cap identified tool item is non-comparable
882
+ * exactly like an over-cap message.
883
+ */
884
+ function replayItemFingerprint(item: unknown): string | null {
885
+ const out: string[] = [];
886
+ let bytes = 0;
887
+ const push = (text: string): boolean => {
888
+ bytes += Buffer.byteLength(text, "utf8");
889
+ if (bytes > REPLAY_FINGERPRINT_MAX_BYTES) return false;
890
+ out.push(text);
891
+ return true;
892
+ };
893
+ const walk = (value: unknown, depth: number): boolean => {
894
+ if (depth > REPLAY_FINGERPRINT_MAX_DEPTH) return false;
895
+ if (value === null || typeof value !== "object") return push(JSON.stringify(value) ?? "null");
896
+ if (Array.isArray(value)) {
897
+ if (!push("[")) return false;
898
+ for (const element of value) {
899
+ if (!walk(element, depth + 1)) return false;
900
+ if (!push(",")) return false;
901
+ }
902
+ return push("]");
903
+ }
904
+ if (!push("{")) return false;
905
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
906
+ if (!push(JSON.stringify(key))) return false;
907
+ if (!walk((value as Record<string, unknown>)[key], depth + 1)) return false;
908
+ if (!push(",")) return false;
909
+ }
910
+ return push("}");
911
+ };
912
+ return walk(item, 0) ? out.join("") : null;
913
+ }
914
+
915
+ /** Non-empty provider-issued `id`/`call_id` on an item, else null. */
916
+ function providerIssuedIdentity(item: unknown): string | null {
917
+ if (!item || typeof item !== "object" || Array.isArray(item)) return null;
918
+ const record = item as { id?: unknown; call_id?: unknown };
919
+ for (const candidate of [record.id, record.call_id]) {
920
+ if (typeof candidate === "string" && candidate.trim().length > 0) return candidate;
921
+ }
922
+ return null;
923
+ }
924
+
925
+ /**
926
+ * Number of leading stored items the client already carries verbatim, or 0.
927
+ *
928
+ * Requires an exact ordered run: every stored item must match the client input item at the
929
+ * same index. Any not-comparable item aborts to 0 — skipping just that item could align two
930
+ * different occurrences and manufacture a false positive, and a false positive here deletes
931
+ * real conversation history.
932
+ *
933
+ * Known gap (FU-2): stored input can contain proxy-injected guidance the client never saw,
934
+ * and ids repaired after recording. Those sessions do not match here and expand as before.
935
+ */
936
+ function clientCarriedPrefixLength(stored: readonly unknown[], clientInput: readonly unknown[]): number {
937
+ if (stored.length === 0 || clientInput.length < stored.length) return 0;
938
+ for (let index = 0; index < stored.length; index += 1) {
939
+ const storedPrint = replayItemFingerprint(stored[index]);
940
+ if (storedPrint === null) return 0;
941
+ const clientPrint = replayItemFingerprint(clientInput[index]);
942
+ if (clientPrint === null || storedPrint !== clientPrint) return 0;
943
+ }
944
+ return stored.length;
945
+ }
946
+
947
+ /** Test-only: replay prepends skipped because the client already carried the history. */
948
+ export function replayOverlapSkipsForTests(): number {
949
+ return replayOverlapSkips;
950
+ }
951
+
587
952
  function pruneResponses(at = now()): void {
588
953
  for (const [id, state] of states) {
589
954
  if (at - state.createdAt > RESPONSE_TTL_MS) deleteEntry(id);
@@ -607,7 +972,9 @@ function pruneResponses(at = now()): void {
607
972
  try {
608
973
  const ref = writeResponseSpillDurably(oldestId, {
609
974
  createdAt: entry.createdAt,
975
+ ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
610
976
  items: entry.items,
977
+ ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
611
978
  ...(entry.providers ? { providers: entry.providers } : {}),
612
979
  });
613
980
  if (swapResidentForSpill(oldestId, entry, ref)) spillCounters.writes += 1;
@@ -630,6 +997,64 @@ export function sweepExpiredResponseStates(at = now()): number {
630
997
  return removed;
631
998
  }
632
999
 
1000
+ /**
1001
+ * Periodic disk reclaim for abandoned atomic-write temps.
1002
+ *
1003
+ * `ensureLoaded` sweeps once per process, at load, BEFORE that process writes anything:
1004
+ * every `schedulePersist` site is downstream of it. So a process that abandons a temp has
1005
+ * already had its only look, the 15-minute grace hides the temp its predecessor's crash
1006
+ * just produced, and `maxCleanups` caps a single pass below a large backlog. A restart
1007
+ * loop therefore accumulates monotonically. Repeating the reclaim on a timer fixes all
1008
+ * three: the grace expires into a later tick and the per-pass cap becomes a per-tick rate.
1009
+ *
1010
+ * Registered on the sweeper's LIVENESS tick, not the TTL tick: `sweepExpiredOnWrite` puts
1011
+ * `sweepExpired` on hot write paths, and a directory scan does not belong there.
1012
+ */
1013
+ export function reclaimAbandonedResponseStateTemps(
1014
+ options: ResponseStateTempRecoveryOptions = {},
1015
+ ): ResponseStateTempRecoveryResult {
1016
+ const total: ResponseStateTempRecoveryResult = {
1017
+ matched: 0, removed: 0, failed: 0, bytesRemoved: 0, eligible: 0, eligibleBytes: 0, truncated: false,
1018
+ };
1019
+ // The try encloses responseStateSweepDirectories() deliberately: recoverStaleResponseStateTemps
1020
+ // already swallows its own enumeration failures, so a catch around only that call would be
1021
+ // unreachable. snapshotPath()/getConfigDir() are the paths that can genuinely throw.
1022
+ try {
1023
+ for (const dir of responseStateSweepDirectories()) {
1024
+ const result = recoverStaleResponseStateTemps(dir, options);
1025
+ total.matched += result.matched;
1026
+ total.removed += result.removed;
1027
+ total.failed += result.failed;
1028
+ total.bytesRemoved += result.bytesRemoved;
1029
+ total.eligible += result.eligible;
1030
+ total.eligibleBytes += result.eligibleBytes;
1031
+ // Truncation anywhere makes the whole total a prefix.
1032
+ total.truncated ||= result.truncated;
1033
+ }
1034
+ } catch {
1035
+ /* best-effort: disk reclaim must never destabilize the caller */
1036
+ }
1037
+ return total;
1038
+ }
1039
+
1040
+ /**
1041
+ * Report-only counterpart for `ocx doctor`: applies every selection gate and unlinks
1042
+ * nothing. It runs the SAME predicate as the reclaim, so the report and the subsequent
1043
+ * removal cannot disagree about which files are reclaimable.
1044
+ */
1045
+ export function inspectAbandonedResponseStateTemps(): ResponseStateTempRecoveryResult {
1046
+ return reclaimAbandonedResponseStateTemps({ dryRun: true });
1047
+ }
1048
+
1049
+ /** Sweeper adapter: narrows the reclaim to the `() => number` the liveness tick expects. */
1050
+ export function sweepAbandonedResponseStateTemps(): number {
1051
+ return reclaimAbandonedResponseStateTemps({
1052
+ maxEntries: PERIODIC_TEMP_MAX_ENTRIES,
1053
+ maxCleanups: PERIODIC_TEMP_MAX_CLEANUPS,
1054
+ deadlineMs: PERIODIC_TEMP_SCAN_DEADLINE_MS,
1055
+ }).removed;
1056
+ }
1057
+
633
1058
  export function responseContinuationRetainedStoreSnapshot(): RetainedStoreSnapshot {
634
1059
  return {
635
1060
  count: states.size,
@@ -648,7 +1073,9 @@ export function evictOldestResponseContinuationForBudget(): number {
648
1073
  try {
649
1074
  const ref = writeResponseSpillDurably(id, {
650
1075
  createdAt: entry.createdAt,
1076
+ ...(entry.clientThreadId ? { clientThreadId: entry.clientThreadId } : {}),
651
1077
  items: entry.items,
1078
+ ...(entry.providerOutputStart !== undefined ? { providerOutputStart: entry.providerOutputStart } : {}),
652
1079
  ...(entry.providers ? { providers: entry.providers } : {}),
653
1080
  });
654
1081
  if (swapResidentForSpill(id, entry, ref)) spillCounters.writes += 1;
@@ -676,7 +1103,11 @@ function materializeEntry(
676
1103
  spillCounters.readFailures += 1;
677
1104
  const failure: PreviousResponseReplayFailure = {
678
1105
  code: "previous_response_not_found",
679
- reason: result.reason === "missing" ? "spill_missing" : "spill_corrupt",
1106
+ reason: result.reason === "missing"
1107
+ ? "spill_missing"
1108
+ : result.reason === "too_large"
1109
+ ? "spill_too_large"
1110
+ : "spill_corrupt",
680
1111
  };
681
1112
  replaceWithSpillFailure(id, entry);
682
1113
  schedulePersist();
@@ -684,7 +1115,11 @@ function materializeEntry(
684
1115
  }
685
1116
  const state = measureResidentEntry(id, {
686
1117
  createdAt: result.payload.createdAt,
1118
+ ...(result.payload.clientThreadId ? { clientThreadId: result.payload.clientThreadId } : {}),
687
1119
  items: result.payload.items,
1120
+ ...(result.payload.providerOutputStart !== undefined
1121
+ ? { providerOutputStart: result.payload.providerOutputStart }
1122
+ : {}),
688
1123
  ...(result.payload.providers ? { providers: result.payload.providers } : {}),
689
1124
  });
690
1125
  if (!state) {
@@ -696,7 +1131,16 @@ function materializeEntry(
696
1131
  return { ok: true, state };
697
1132
  }
698
1133
 
699
- export function expandPreviousResponseInput(body: unknown): unknown {
1134
+ function normalizedClientThreadId(value: unknown): string | undefined {
1135
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
1136
+ }
1137
+
1138
+ function withoutPreviousResponseId(request: Record<string, unknown>): Record<string, unknown> {
1139
+ const { previous_response_id: _previousResponseId, ...freshRequest } = request;
1140
+ return freshRequest;
1141
+ }
1142
+
1143
+ export function expandPreviousResponseInput(body: unknown, clientThreadId?: string): unknown {
700
1144
  if (!body || typeof body !== "object" || Array.isArray(body)) return body;
701
1145
  const request = body as Record<string, unknown>;
702
1146
  const previousId = typeof request.previous_response_id === "string" ? request.previous_response_id : undefined;
@@ -710,6 +1154,49 @@ export function expandPreviousResponseInput(body: unknown): unknown {
710
1154
  replayFailures.set(request, materialized.failure);
711
1155
  return body;
712
1156
  }
1157
+ const requestThreadId = normalizedClientThreadId(clientThreadId);
1158
+ const storedThreadId = normalizedClientThreadId(materialized.state.clientThreadId);
1159
+ // A Codex task must never inherit another task's continuation, nor a legacy unscoped entry.
1160
+ // Unscoped callers retain backward-compatible replay only with other unscoped entries.
1161
+ if (requestThreadId !== storedThreadId) {
1162
+ const freshRequest = withoutPreviousResponseId(request);
1163
+ replayScopeMismatches.add(freshRequest);
1164
+ replayScopeMismatchDrops += 1;
1165
+ return freshRequest;
1166
+ }
1167
+ // The client already replayed this history verbatim. Prepending the stored copy would
1168
+ // double it, and the doubled turn is stored again, so the next turn triples (#1412 saw
1169
+ // 127k of real context reach 1.3M tokens this way).
1170
+ //
1171
+ // Three conditions, all required. The run must cover the whole stored entry; it must reach
1172
+ // the provider-output region; and some matched item in that region must carry a
1173
+ // provider-issued id. The last one is the load-bearing part: content equality alone proves
1174
+ // two items look alike, not that they are the same occurrence, so a client that merely
1175
+ // repeats its own message would otherwise authorize a skip that deletes real history.
1176
+ // There is no invariant that provider output always carries ids, so an entry whose output
1177
+ // has none simply never skips.
1178
+ {
1179
+ const clientInput = inputItems(request.input);
1180
+ const stored = materialized.state.items;
1181
+ const anchor = materialized.state.providerOutputStart;
1182
+ const carried = clientCarriedPrefixLength(stored, clientInput);
1183
+ if (
1184
+ carried === stored.length
1185
+ && anchor !== undefined
1186
+ && carried > anchor
1187
+ && stored.slice(anchor, carried).some(item => providerIssuedIdentity(item) !== null)
1188
+ ) {
1189
+ replayOverlapSkips += 1;
1190
+ // Keep previous_response_id: Kiro and Cursor recover their conversation ids from it
1191
+ // (kiro-wire.ts, cursor/request-builder.ts). Only the concatenation is skipped.
1192
+ const unchanged = { ...request };
1193
+ // Same provenance boundary a real expansion would record, so the replayed prefix does
1194
+ // not re-acknowledge historical compaction markers (parser.ts) and stays visible to
1195
+ // guidance de-duplication (collaboration.ts).
1196
+ replayedInputPrefixLengths.set(unchanged, carried);
1197
+ return unchanged;
1198
+ }
1199
+ }
713
1200
  const expanded = {
714
1201
  ...request,
715
1202
  input: [...materialized.state.items, ...inputItems(request.input)],
@@ -729,6 +1216,22 @@ export function previousResponseReplayPrefixLength(body: unknown): number {
729
1216
  return replayedInputPrefixLengths.get(body) ?? 0;
730
1217
  }
731
1218
 
1219
+ /** Copy proxy-private replay provenance to an internal clone with the same materialized input. */
1220
+ export function copyPreviousResponseReplayProvenance(source: unknown, target: unknown): void {
1221
+ if (!source || typeof source !== "object" || Array.isArray(source)) return;
1222
+ if (!target || typeof target !== "object" || Array.isArray(target)) return;
1223
+ const prefixLength = replayedInputPrefixLengths.get(source);
1224
+ if (!prefixLength) return;
1225
+ const input = (target as { input?: unknown }).input;
1226
+ if (!Array.isArray(input) || prefixLength > input.length) return;
1227
+ replayedInputPrefixLengths.set(target, prefixLength);
1228
+ }
1229
+
1230
+ /** True when a stale or foreign previous_response_id was removed from this exact request body. */
1231
+ export function previousResponseScopeMismatch(body: unknown): boolean {
1232
+ return !!body && typeof body === "object" && replayScopeMismatches.has(body as object);
1233
+ }
1234
+
732
1235
  export function previousResponseConversationId(responseId: string | undefined): string | undefined {
733
1236
  return previousResponseProviderState(responseId)?.cursor?.conversationId;
734
1237
  }
@@ -754,6 +1257,7 @@ export interface ResponseStateMetrics {
754
1257
  spillWrites: number;
755
1258
  spillWriteFailures: number;
756
1259
  spillReadFailures: number;
1260
+ replayScopeMismatchDrops: number;
757
1261
  }
758
1262
 
759
1263
  /**
@@ -796,6 +1300,7 @@ export function responseStateMetrics(): ResponseStateMetrics {
796
1300
  spillWrites: spillCounters.writes,
797
1301
  spillWriteFailures: spillCounters.writeFailures,
798
1302
  spillReadFailures: spillCounters.readFailures,
1303
+ replayScopeMismatchDrops,
799
1304
  };
800
1305
  }
801
1306
 
@@ -803,14 +1308,36 @@ export function responseStateMetrics(): ResponseStateMetrics {
803
1308
  * Cache completed output and max_output_tokens partial output for previous_response_id replay.
804
1309
  * Content-filtered incomplete and failed output are not authoritative replay history.
805
1310
  */
1311
+ /**
1312
+ * Request bodies that must never enter the continuation cache.
1313
+ *
1314
+ * The cache is persisted to `responses-state.json`, so anything recorded here reaches disk.
1315
+ * Encrypted-agent-task recovery decrypts task text into the request body and promises
1316
+ * in-memory, TTL-bounded retention; recording that body would put the plaintext on disk with
1317
+ * no TTL and break the promise.
1318
+ *
1319
+ * A WeakSet rather than a body field on purpose: `_rawBody` is serialized verbatim by the
1320
+ * native passthrough, so any marker written into the body itself would be sent upstream.
1321
+ * Marking is enforced once here rather than at each call site, because every recording path
1322
+ * (streaming, non-streaming, passthrough, forced) funnels through `rememberResponseState` —
1323
+ * a new call site cannot reintroduce the leak by forgetting a guard.
1324
+ */
1325
+ const nonPersistableBodies = new WeakSet<object>();
1326
+
1327
+ /** Bar this exact request body from the continuation cache, and therefore from disk. */
1328
+ export function markBodyNonPersistable(body: unknown): void {
1329
+ if (body && typeof body === "object") nonPersistableBodies.add(body as object);
1330
+ }
1331
+
806
1332
  export function rememberResponseState(
807
1333
  requestBody: unknown,
808
1334
  response: { id?: unknown; output?: unknown; status?: unknown; incomplete_details?: unknown },
809
1335
  providerState?: OcxProviderContinuationState | string,
810
- opts?: { force?: boolean },
1336
+ opts?: { force?: boolean; clientThreadId?: string },
811
1337
  ): void {
812
1338
  if (!requestBody || typeof requestBody !== "object" || Array.isArray(requestBody)) return;
813
1339
  const request = requestBody as Record<string, unknown>;
1340
+ if (nonPersistableBodies.has(request)) return;
814
1341
  // `force` bypasses only the store:false skip: Codex sends `store:false` on every non-Azure
815
1342
  // HTTP request (and WS inherits it), yet its WS turns still chain with previous_response_id.
816
1343
  // The passthrough branch records with force so those chains can be expanded locally; the
@@ -832,9 +1359,18 @@ export function rememberResponseState(
832
1359
  return !!item && typeof item === "object" && (item as { type?: unknown }).type === "function_call";
833
1360
  });
834
1361
  }
1362
+ const clientThreadId = normalizedClientThreadId(opts?.clientThreadId);
1363
+ // Compute the normalized array once and reuse it for both fields, so the recorded
1364
+ // boundary can never disagree with the items it indexes.
1365
+ const requestItems = inputItems(request.input);
835
1366
  setResidentEntry(response.id, {
836
1367
  createdAt: now(),
837
- items: [...inputItems(request.input), ...response.output],
1368
+ ...(clientThreadId ? { clientThreadId } : {}),
1369
+ items: [...requestItems, ...response.output],
1370
+ // Where response.output begins. A replay skip requires a matched item at or past this
1371
+ // index that also carries a provider-issued id — position alone proves only that an item
1372
+ // sits on the provider side, not that the provider authored it.
1373
+ providerOutputStart: requestItems.length,
838
1374
  // Always preserve the Cursor conversation id so the next tool-result turn can continue the SAME
839
1375
  // Cursor conversation (multi-turn continuation). Separately track whether Cursor's own
840
1376
  // checkpoint/cache is safe to reuse: a turn that ended with a pending client tool call produced an
@@ -879,6 +1415,8 @@ export function clearResponseStateMemoryForTests(): void {
879
1415
  spillCounters.writes = 0;
880
1416
  spillCounters.writeFailures = 0;
881
1417
  spillCounters.readFailures = 0;
1418
+ replayScopeMismatchDrops = 0;
1419
+ replayOverlapSkips = 0;
882
1420
  persistAttemptHookForTests = null;
883
1421
  loaded = false;
884
1422
  }