@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,936 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+
3
+ /**
4
+ * The Desktop IPC frame is 64 MiB, but history pages intentionally stay far
5
+ * below that ceiling. A small fixed page leaves room for the envelope,
6
+ * patches, retries, and future protocol fields and makes the bound useful on
7
+ * every transport (including a compressed transport).
8
+ */
9
+ export const DESKTOP_HISTORY_PAGE_MAX_TURNS = 10;
10
+ export const DESKTOP_HISTORY_PAGE_MAX_ITEMS = 500;
11
+ export const DESKTOP_HISTORY_PAGE_MAX_SERIALIZED_BYTES = 2 * 1024 * 1024;
12
+ export const DESKTOP_HISTORY_PAGE_MAX_INLINE_FIELD_BYTES = 256 * 1024;
13
+ export const DESKTOP_CONTENT_CHUNK_MAX_BYTES = 1024 * 1024;
14
+
15
+ const DEFAULT_CONTENT_PREVIEW_BYTES = 8 * 1024;
16
+ const DEFAULT_PAGE_TOKEN_TTL_MS = 15 * 60 * 1000;
17
+ const DEFAULT_CONTENT_TTL_MS = 15 * 60 * 1000;
18
+ const DEFAULT_MAX_PAGE_TOKENS = 128;
19
+ const DEFAULT_MAX_CONTENT_ENTRIES = 128;
20
+ const DEFAULT_MAX_RETAINED_CONTENT_BYTES = 64 * 1024 * 1024;
21
+ const MAX_COMPACT_COLLECTION_ENTRIES = 1_000;
22
+ // Small protocol records (items/turns) should keep their shape when one field
23
+ // is large; larger containers are safer as one opaque JSON handle.
24
+ const MAX_INLINE_COLLECTION_ENTRIES = 64;
25
+ const MAX_COMPACT_METADATA_KEYS = 128;
26
+ const MAX_COMPACT_KEY_BYTES = 512;
27
+ const MAX_COMPACT_DEPTH = 64;
28
+
29
+ export type DesktopJsonRecord = Record<string, unknown>;
30
+ export type DesktopHistoryPageDirection = "recent" | "older" | "newer";
31
+
32
+ export type DesktopContentHandleReference = {
33
+ kind: "handle";
34
+ handle: string;
35
+ preview: string;
36
+ byteLength: number;
37
+ mediaType: string;
38
+ truncatedInline: true;
39
+ };
40
+
41
+ export type DesktopMissingContent = {
42
+ kind: "missing";
43
+ reason: "deferred" | "not-retained" | "source-missing";
44
+ };
45
+
46
+ export type DesktopInlineContent = {
47
+ kind: "inline";
48
+ text: string;
49
+ mediaType: string;
50
+ };
51
+
52
+ export type DesktopBoundedContent =
53
+ | DesktopInlineContent
54
+ | DesktopContentHandleReference
55
+ | DesktopMissingContent;
56
+
57
+ export type DesktopHistoryPageInfo = {
58
+ kind: "desktop-history-page";
59
+ sourceRevision: string;
60
+ order: "oldest-to-newest";
61
+ hasOlder: boolean;
62
+ hasNewer: boolean;
63
+ olderPageToken: string | null;
64
+ newerPageToken: string | null;
65
+ itemsOmitted: number;
66
+ inlineFieldsMovedToHandles: number;
67
+ completeOrExplicitlyPartial: boolean;
68
+ };
69
+
70
+ export type DesktopBoundedHistoryPage = {
71
+ state: DesktopJsonRecord;
72
+ pageInfo: DesktopHistoryPageInfo;
73
+ };
74
+
75
+ export type DesktopContentChunk = {
76
+ kind: "chunk";
77
+ handle: string;
78
+ offset: number;
79
+ nextOffset: number;
80
+ chunkByteLength: number;
81
+ totalByteLength: number;
82
+ mediaType: string;
83
+ encoding: "base64";
84
+ data: string;
85
+ complete: boolean;
86
+ } | DesktopMissingContent;
87
+
88
+ export type DesktopHistoryPageStoreOptions = {
89
+ now?: () => number;
90
+ pageTokenTtlMs?: number;
91
+ contentTtlMs?: number;
92
+ maxPageTokens?: number;
93
+ maxContentEntries?: number;
94
+ maxRetainedContentBytes?: number;
95
+ maxSerializedBytes?: number;
96
+ maxTurns?: number;
97
+ maxItems?: number;
98
+ };
99
+
100
+ type PageTokenState = {
101
+ threadId: string;
102
+ sourceRevision: string;
103
+ direction: Exclude<DesktopHistoryPageDirection, "recent">;
104
+ /** For older pages this is an exclusive end turn index. */
105
+ boundary: number;
106
+ issuedAt: number;
107
+ expiresAt: number;
108
+ };
109
+
110
+ type ContentEntry = {
111
+ threadId: string;
112
+ sourceRevision: string;
113
+ value: Buffer;
114
+ mediaType: string;
115
+ expiresAt: number;
116
+ lastUsedAt: number;
117
+ };
118
+
119
+ type BuildContext = {
120
+ handlesCreated: number;
121
+ itemsOmitted: number;
122
+ itemsIncluded: number;
123
+ seen: WeakSet<object>;
124
+ contentHandles: string[];
125
+ pageTokens: string[];
126
+ };
127
+
128
+ function isRecord(value: unknown): value is DesktopJsonRecord {
129
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
130
+ }
131
+
132
+ function stringValue(value: unknown): string {
133
+ return typeof value === "string" ? value : "";
134
+ }
135
+
136
+ function numberValue(value: unknown): number | null {
137
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
138
+ }
139
+
140
+ function byteLength(value: string): number {
141
+ return Buffer.byteLength(value, "utf8");
142
+ }
143
+
144
+ function jsonByteLength(value: unknown, maximumBytes: number): number {
145
+ const estimate = estimateJsonBytes(value, maximumBytes);
146
+ if (estimate === null) return Number.POSITIVE_INFINITY;
147
+ try {
148
+ const encoded = JSON.stringify(value);
149
+ return encoded === undefined ? 0 : byteLength(encoded);
150
+ } catch {
151
+ return Number.POSITIVE_INFINITY;
152
+ }
153
+ }
154
+
155
+ function truncateUtf8(value: string, maximumBytes: number): string {
156
+ if (byteLength(value) <= maximumBytes) return value;
157
+ let output = "";
158
+ let used = 0;
159
+ for (const character of value) {
160
+ const next = byteLength(character);
161
+ if (used + next > maximumBytes) break;
162
+ output += character;
163
+ used += next;
164
+ }
165
+ return output;
166
+ }
167
+
168
+ function mediaTypeForKey(key: string): string {
169
+ const normalized = key.toLowerCase();
170
+ if (normalized.includes("diff") || normalized.includes("patch")) return "text/x-diff";
171
+ if (normalized.includes("output") || normalized.includes("text") || normalized.includes("message")) {
172
+ return "text/plain";
173
+ }
174
+ return "application/json";
175
+ }
176
+
177
+ function opaqueToken(prefix: string): string {
178
+ // UUID bytes are random and the token contains no source cursor, path, or
179
+ // native thread identity. Keep it URL/JSON friendly for mobile clients.
180
+ return `${prefix}${randomUUID().replaceAll("-", "")}`;
181
+ }
182
+
183
+ function sourceRevision(threadId: string, state: DesktopJsonRecord): string {
184
+ // Hash a bounded sample of each scalar directly into the digest instead of
185
+ // first constructing a second array shaped like the complete transcript.
186
+ // The state can contain hundreds of megabytes of turns; revision tracking
187
+ // must not have a proportional temporary allocation.
188
+ const hash = createHash("sha256");
189
+ const update = (value: unknown): void => {
190
+ hash.update(String(value));
191
+ hash.update("\0");
192
+ };
193
+ const sample = (value: unknown): void => {
194
+ if (typeof value !== "string") {
195
+ update(value ?? "");
196
+ return;
197
+ }
198
+ update(value.length);
199
+ update(value.slice(0, 64));
200
+ update(value.slice(-64));
201
+ };
202
+ update("desktop-history-v1");
203
+ sample(threadId);
204
+ sample(state.updatedAt);
205
+ sample(state.title || state.name);
206
+ const turns = safeArray(state.turns);
207
+ update(turns.length);
208
+ for (const turn of turns) {
209
+ const row = isRecord(turn) ? turn : null;
210
+ sample(row?.id || row?.turnId || row?.turn_id);
211
+ sample(row?.status);
212
+ const items = safeArray(row?.items);
213
+ update(items.length);
214
+ for (const item of items) {
215
+ const candidate = isRecord(item) ? item : null;
216
+ sample(candidate?.id);
217
+ sample(candidate?.type);
218
+ sample(candidate?.status);
219
+ for (const key of ["text", "message"]) sample(candidate?.[key]);
220
+ }
221
+ }
222
+ return `desktop_source_v1_${hash.digest("hex").slice(0, 48)}`;
223
+ }
224
+
225
+ function tokenStateToString(_state: PageTokenState): string {
226
+ // The state itself remains server-side; the client receives only this
227
+ // random key. In particular, do not base64-encode the native cursor into
228
+ // the token.
229
+ return opaqueToken("desktop_page_v1_");
230
+ }
231
+
232
+ function safeArray(value: unknown): unknown[] {
233
+ return Array.isArray(value) ? value : [];
234
+ }
235
+
236
+ /**
237
+ * A bounded, connection-local history/content store. Tokens and handles are
238
+ * intentionally opaque and expire; their maps are capped so a client cannot
239
+ * turn pagination into an unbounded memory allocation.
240
+ */
241
+ export class DesktopHistoryPageStore {
242
+ private readonly now: () => number;
243
+ private readonly pageTokenTtlMs: number;
244
+ private readonly contentTtlMs: number;
245
+ private readonly maxPageTokens: number;
246
+ private readonly maxContentEntries: number;
247
+ private readonly maxRetainedContentBytes: number;
248
+ private readonly maxSerializedBytes: number;
249
+ private readonly maxTurns: number;
250
+ private readonly maxItems: number;
251
+ private readonly pageTokens = new Map<string, PageTokenState>();
252
+ private readonly content = new Map<string, ContentEntry>();
253
+ private retainedContentBytes = 0;
254
+
255
+ constructor(options: DesktopHistoryPageStoreOptions = {}) {
256
+ this.now = options.now ?? Date.now;
257
+ this.pageTokenTtlMs = Math.max(1, options.pageTokenTtlMs ?? DEFAULT_PAGE_TOKEN_TTL_MS);
258
+ this.contentTtlMs = Math.max(1, options.contentTtlMs ?? DEFAULT_CONTENT_TTL_MS);
259
+ this.maxPageTokens = Math.max(1, Math.floor(options.maxPageTokens ?? DEFAULT_MAX_PAGE_TOKENS));
260
+ this.maxContentEntries = Math.max(1, Math.floor(options.maxContentEntries ?? DEFAULT_MAX_CONTENT_ENTRIES));
261
+ this.maxRetainedContentBytes = Math.max(0, Math.floor(options.maxRetainedContentBytes ?? DEFAULT_MAX_RETAINED_CONTENT_BYTES));
262
+ this.maxSerializedBytes = Math.max(1, Math.floor(options.maxSerializedBytes ?? DESKTOP_HISTORY_PAGE_MAX_SERIALIZED_BYTES));
263
+ this.maxTurns = Math.max(1, Math.floor(options.maxTurns ?? DESKTOP_HISTORY_PAGE_MAX_TURNS));
264
+ this.maxItems = Math.max(1, Math.floor(options.maxItems ?? DESKTOP_HISTORY_PAGE_MAX_ITEMS));
265
+ }
266
+
267
+ recentPage(threadId: string, state: DesktopJsonRecord): DesktopBoundedHistoryPage {
268
+ const rows = safeArray(state.turns);
269
+ const revision = sourceRevision(threadId, state);
270
+ const start = Math.max(0, rows.length - this.maxTurns);
271
+ return this.buildPage(threadId, state, revision, start, rows.length, "recent");
272
+ }
273
+
274
+ page(
275
+ threadId: string,
276
+ state: DesktopJsonRecord,
277
+ direction: Exclude<DesktopHistoryPageDirection, "recent">,
278
+ pageToken: string,
279
+ ): DesktopBoundedHistoryPage {
280
+ this.prune();
281
+ const token = this.pageTokens.get(pageToken);
282
+ if (!token) throw new Error("Desktop history page token is invalid or expired");
283
+ if (token.threadId !== threadId) throw new Error("Desktop history page token belongs to another task");
284
+ const revision = sourceRevision(threadId, state);
285
+ if (token.sourceRevision !== revision) throw new Error("Desktop history source changed; reload the task");
286
+ if (token.direction !== direction) throw new Error("Desktop history page token direction mismatch");
287
+ this.pageTokens.delete(pageToken);
288
+ const rows = safeArray(state.turns);
289
+ if (direction === "older") {
290
+ const end = Math.min(rows.length, Math.max(0, token.boundary));
291
+ const start = this.selectStart(end);
292
+ return this.buildPage(threadId, state, revision, start, end, direction);
293
+ }
294
+ const start = Math.min(rows.length, Math.max(0, token.boundary));
295
+ const end = Math.min(rows.length, start + this.maxTurns);
296
+ return this.buildPage(threadId, state, revision, start, end, direction);
297
+ }
298
+
299
+ readContentChunk(input: {
300
+ threadId: string;
301
+ sourceRevision: string;
302
+ handle: string;
303
+ offset: number;
304
+ }): DesktopContentChunk {
305
+ this.prune();
306
+ const entry = this.content.get(input.handle);
307
+ if (!entry) return { kind: "missing", reason: "source-missing" };
308
+ if (entry.threadId !== input.threadId) return { kind: "missing", reason: "source-missing" };
309
+ if (entry.sourceRevision !== input.sourceRevision) return { kind: "missing", reason: "source-missing" };
310
+ const offset = Number.isSafeInteger(input.offset) ? input.offset : -1;
311
+ if (offset < 0 || offset > entry.value.byteLength) return { kind: "missing", reason: "source-missing" };
312
+ entry.lastUsedAt = this.now();
313
+ entry.expiresAt = entry.lastUsedAt + this.contentTtlMs;
314
+ const end = Math.min(entry.value.byteLength, offset + DESKTOP_CONTENT_CHUNK_MAX_BYTES);
315
+ const data = entry.value.subarray(offset, end).toString("base64");
316
+ return {
317
+ kind: "chunk",
318
+ handle: input.handle,
319
+ offset,
320
+ nextOffset: end,
321
+ chunkByteLength: end - offset,
322
+ totalByteLength: entry.value.byteLength,
323
+ mediaType: entry.mediaType,
324
+ encoding: "base64",
325
+ data,
326
+ complete: end >= entry.value.byteLength,
327
+ };
328
+ }
329
+
330
+ clearThread(threadId: string): void {
331
+ for (const [token, state] of this.pageTokens) {
332
+ if (state.threadId === threadId) this.pageTokens.delete(token);
333
+ }
334
+ for (const [handle, entry] of this.content) {
335
+ if (entry.threadId !== threadId) continue;
336
+ this.retainedContentBytes -= entry.value.byteLength;
337
+ this.content.delete(handle);
338
+ }
339
+ this.retainedContentBytes = Math.max(0, this.retainedContentBytes);
340
+ }
341
+
342
+ get pageTokenCount(): number {
343
+ return this.pageTokens.size;
344
+ }
345
+
346
+ get contentEntryCount(): number {
347
+ return this.content.size;
348
+ }
349
+
350
+ private selectStart(end: number): number {
351
+ return Math.max(0, Math.max(0, end) - this.maxTurns);
352
+ }
353
+
354
+ private newBuildContext(itemsOmitted = 0): BuildContext {
355
+ return {
356
+ handlesCreated: 0,
357
+ itemsOmitted,
358
+ itemsIncluded: 0,
359
+ seen: new WeakSet(),
360
+ contentHandles: [],
361
+ pageTokens: [],
362
+ };
363
+ }
364
+
365
+ /** Remove side effects from a candidate that was rejected by the byte guard. */
366
+ private discardBuildContext(context: BuildContext): void {
367
+ for (const handle of context.contentHandles) {
368
+ const entry = this.content.get(handle);
369
+ if (!entry) continue;
370
+ this.retainedContentBytes -= entry.value.byteLength;
371
+ this.content.delete(handle);
372
+ }
373
+ for (const token of context.pageTokens) this.pageTokens.delete(token);
374
+ this.retainedContentBytes = Math.max(0, this.retainedContentBytes);
375
+ }
376
+
377
+ private buildPage(
378
+ threadId: string,
379
+ state: DesktopJsonRecord,
380
+ revision: string,
381
+ start: number,
382
+ end: number,
383
+ direction: DesktopHistoryPageDirection,
384
+ ): DesktopBoundedHistoryPage {
385
+ const rows = safeArray(state.turns);
386
+ // Recompute the range against the actual state. This also avoids relying
387
+ // on any untrusted client-provided index in a token.
388
+ let selectedStart = Math.max(0, Math.min(start, rows.length));
389
+ let selectedEnd = Math.max(selectedStart, Math.min(end, rows.length));
390
+ let selectedRows = rows.slice(selectedStart, selectedEnd);
391
+ let forcedItemsOmitted = 0;
392
+ let context = this.newBuildContext(forcedItemsOmitted);
393
+ let candidate = this.makeState(
394
+ threadId,
395
+ state,
396
+ selectedRows,
397
+ rows.length,
398
+ selectedStart,
399
+ selectedEnd,
400
+ revision,
401
+ context,
402
+ direction,
403
+ );
404
+
405
+ while (jsonByteLength(candidate, this.maxSerializedBytes) > this.maxSerializedBytes) {
406
+ this.discardBuildContext(context);
407
+ if (selectedEnd - selectedStart > 1) {
408
+ selectedStart += 1;
409
+ selectedRows = selectedRows.slice(1);
410
+ } else {
411
+ const turnCandidate = selectedRows[0];
412
+ const turn: DesktopJsonRecord | null = isRecord(turnCandidate) ? turnCandidate : null;
413
+ const items = safeArray(turn?.items);
414
+ if (items.length > 1) {
415
+ // Keep the newest work in a single oversized turn and expose the
416
+ // omitted count in pageInfo. Individual fields are already handles.
417
+ const shortened = { ...turn } as DesktopJsonRecord;
418
+ // Halve the currently selected set, rather than resetting to a
419
+ // fixed max-item count. This guarantees progress even when one
420
+ // turn remains larger than the serialized page budget.
421
+ const nextLength = Math.max(1, Math.floor(items.length / 2));
422
+ shortened.items = items.slice(Math.max(0, items.length - nextLength));
423
+ selectedRows = [shortened];
424
+ forcedItemsOmitted += items.length - nextLength;
425
+ } else {
426
+ context = this.newBuildContext(forcedItemsOmitted);
427
+ candidate = this.minimalState(
428
+ threadId,
429
+ state,
430
+ selectedRows,
431
+ rows.length,
432
+ selectedStart,
433
+ selectedEnd,
434
+ revision,
435
+ context,
436
+ direction,
437
+ );
438
+ break;
439
+ }
440
+ }
441
+ context = this.newBuildContext(forcedItemsOmitted);
442
+ candidate = this.makeState(
443
+ threadId,
444
+ state,
445
+ selectedRows,
446
+ rows.length,
447
+ selectedStart,
448
+ selectedEnd,
449
+ revision,
450
+ context,
451
+ direction,
452
+ );
453
+ }
454
+
455
+ // A pathological metadata object can still exceed the page budget after
456
+ // all turns are removed. The final minimal state is deterministic and
457
+ // always serializable under the same byte guard.
458
+ if (jsonByteLength(candidate, this.maxSerializedBytes) > this.maxSerializedBytes) {
459
+ this.discardBuildContext(context);
460
+ candidate = {
461
+ id: threadId,
462
+ hostId: stringValue(state.hostId) || "local",
463
+ updatedAt: numberValue(state.updatedAt) ?? this.now(),
464
+ turns: [],
465
+ requests: [],
466
+ historyPage: {
467
+ kind: "desktop-history-page",
468
+ sourceRevision: revision,
469
+ order: "oldest-to-newest",
470
+ hasOlder: selectedStart > 0,
471
+ hasNewer: selectedEnd < rows.length,
472
+ olderPageToken: null,
473
+ newerPageToken: null,
474
+ itemsOmitted: forcedItemsOmitted,
475
+ inlineFieldsMovedToHandles: 0,
476
+ completeOrExplicitlyPartial: false,
477
+ },
478
+ } satisfies DesktopJsonRecord;
479
+ }
480
+
481
+ const pageInfo = recordPageInfo(candidate.historyPage)!;
482
+ return { state: candidate, pageInfo };
483
+ }
484
+
485
+ private makeState(
486
+ threadId: string,
487
+ source: DesktopJsonRecord,
488
+ rows: unknown[],
489
+ totalRows: number,
490
+ start: number,
491
+ end: number,
492
+ revision: string,
493
+ context: BuildContext,
494
+ direction: DesktopHistoryPageDirection,
495
+ ): DesktopJsonRecord {
496
+ const output: DesktopJsonRecord = {};
497
+ const omittedKeys = new Set(["turns", "historyPage"]);
498
+ let metadataKeys = 0;
499
+ for (const key in source) {
500
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
501
+ if (omittedKeys.has(key)) continue;
502
+ metadataKeys += 1;
503
+ if (metadataKeys > MAX_COMPACT_METADATA_KEYS) {
504
+ output._desktopHistoryMetadataTruncated = true;
505
+ break;
506
+ }
507
+ if (byteLength(key) > MAX_COMPACT_KEY_BYTES) {
508
+ output._desktopHistoryMetadataTruncated = true;
509
+ continue;
510
+ }
511
+ const value = source[key];
512
+ if (key === "requests") {
513
+ const requestRows = safeArray(value).slice(-64);
514
+ output[key] = requestRows.map(row => this.compactValue(row, threadId, revision, key, context));
515
+ continue;
516
+ }
517
+ output[key] = this.compactValue(value, threadId, revision, key, context);
518
+ }
519
+ output.id = stringValue(source.id) || threadId;
520
+ output.hostId = stringValue(source.hostId) || "local";
521
+ const selectedRows = rows;
522
+ const compactedTurns = new Array<DesktopJsonRecord>(selectedRows.length);
523
+ // Reserve the global item budget from newest to oldest, while retaining
524
+ // the wire order expected by the Desktop renderer.
525
+ for (let index = selectedRows.length - 1; index >= 0; index -= 1) {
526
+ compactedTurns[index] = this.compactTurn(
527
+ selectedRows[index],
528
+ threadId,
529
+ revision,
530
+ context,
531
+ index === selectedRows.length - 1,
532
+ );
533
+ }
534
+ output.turns = compactedTurns;
535
+ const olderToken = start > 0
536
+ ? this.issuePageToken({ threadId, sourceRevision: revision, direction: "older", boundary: start }, context)
537
+ : null;
538
+ const newerToken = end < totalRows
539
+ ? this.issuePageToken({ threadId, sourceRevision: revision, direction: "newer", boundary: end }, context)
540
+ : null;
541
+ output.historyPage = {
542
+ kind: "desktop-history-page",
543
+ sourceRevision: revision,
544
+ order: "oldest-to-newest",
545
+ hasOlder: start > 0,
546
+ hasNewer: end < totalRows,
547
+ olderPageToken: olderToken,
548
+ newerPageToken: newerToken,
549
+ itemsOmitted: context.itemsOmitted,
550
+ inlineFieldsMovedToHandles: context.handlesCreated,
551
+ // A recent page is explicitly partial whenever older turns exist. A
552
+ // page with no adjacent turns is complete even if a field uses a handle.
553
+ completeOrExplicitlyPartial: start === 0 && end === totalRows,
554
+ };
555
+ return output;
556
+ }
557
+
558
+ private minimalState(
559
+ threadId: string,
560
+ source: DesktopJsonRecord,
561
+ rows: unknown[],
562
+ totalRows: number,
563
+ start: number,
564
+ end: number,
565
+ revision: string,
566
+ context: BuildContext,
567
+ direction: DesktopHistoryPageDirection,
568
+ ): DesktopJsonRecord {
569
+ const latestCandidate = rows.at(-1);
570
+ const latest: DesktopJsonRecord | null = isRecord(latestCandidate) ? latestCandidate : null;
571
+ const turn = latest
572
+ ? {
573
+ id: stringValue(latest.id || latest.turnId || latest.turn_id),
574
+ turnId: stringValue(latest.turnId || latest.id || latest.turn_id),
575
+ status: this.compactValue(latest.status ?? "completed", threadId, revision, "status", context),
576
+ items: [],
577
+ }
578
+ : null;
579
+ const output: DesktopJsonRecord = {
580
+ id: threadId,
581
+ hostId: stringValue(source.hostId) || "local",
582
+ title: truncateUtf8(stringValue(source.title || source.name), 2048),
583
+ updatedAt: numberValue(source.updatedAt) ?? this.now(),
584
+ turns: turn ? [turn] : [],
585
+ requests: [],
586
+ };
587
+ output.historyPage = {
588
+ kind: "desktop-history-page",
589
+ sourceRevision: revision,
590
+ order: "oldest-to-newest",
591
+ hasOlder: start > 0,
592
+ hasNewer: end < totalRows,
593
+ olderPageToken: start > 0
594
+ ? this.issuePageToken({ threadId, sourceRevision: revision, direction: "older", boundary: start }, context)
595
+ : null,
596
+ newerPageToken: end < totalRows
597
+ ? this.issuePageToken({ threadId, sourceRevision: revision, direction: "newer", boundary: end }, context)
598
+ : null,
599
+ itemsOmitted: context.itemsOmitted + Math.max(0, safeArray(latest?.items).length),
600
+ inlineFieldsMovedToHandles: context.handlesCreated,
601
+ completeOrExplicitlyPartial: false,
602
+ };
603
+ return output;
604
+ }
605
+
606
+ private compactTurn(
607
+ value: unknown,
608
+ threadId: string,
609
+ revision: string,
610
+ context: BuildContext,
611
+ newest: boolean,
612
+ ): DesktopJsonRecord {
613
+ if (!isRecord(value)) return { id: "unknown", status: "completed", items: [] };
614
+ const output: DesktopJsonRecord = {};
615
+ let metadataKeys = 0;
616
+ for (const key in value) {
617
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
618
+ if (key === "items") continue;
619
+ metadataKeys += 1;
620
+ if (metadataKeys > MAX_COMPACT_METADATA_KEYS) {
621
+ output._desktopHistoryMetadataTruncated = true;
622
+ break;
623
+ }
624
+ if (byteLength(key) > MAX_COMPACT_KEY_BYTES) {
625
+ output._desktopHistoryMetadataTruncated = true;
626
+ continue;
627
+ }
628
+ output[key] = this.compactValue(value[key], threadId, revision, key, context);
629
+ }
630
+ const items = safeArray(value.items);
631
+ const remaining = Math.max(0, this.maxItems - context.itemsIncluded);
632
+ const limit = Math.min(remaining, items.length);
633
+ const selected = limit > 0 ? items.slice(Math.max(0, items.length - limit)) : [];
634
+ if (items.length > selected.length) context.itemsOmitted += items.length - selected.length;
635
+ context.itemsIncluded += selected.length;
636
+ const compactedItems = selected.map(item => this.compactValue(item, threadId, revision, "item", context));
637
+ output.items = compactedItems;
638
+ if (!newest && compactedItems.length === 0) output.items = [];
639
+ return output;
640
+ }
641
+
642
+ private compactValue(
643
+ value: unknown,
644
+ threadId: string,
645
+ revision: string,
646
+ key: string,
647
+ context: BuildContext,
648
+ depth = 0,
649
+ ): unknown {
650
+ if (typeof value === "string") {
651
+ // Measure the encoded JSON form as well as UTF-8 bytes. Control-heavy
652
+ // or surrogate-heavy strings can expand substantially when escaped;
653
+ // keeping those inline would make the later page serialization large.
654
+ if (
655
+ byteLength(value) <= DESKTOP_HISTORY_PAGE_MAX_INLINE_FIELD_BYTES
656
+ && estimateJsonBytes(value, DESKTOP_HISTORY_PAGE_MAX_INLINE_FIELD_BYTES) !== null
657
+ ) return value;
658
+ return this.contentReference(threadId, revision, value, mediaTypeForKey(key), context);
659
+ }
660
+ if (value === null || typeof value === "boolean" || typeof value === "number") return value;
661
+ if (typeof value !== "object") return String(value);
662
+ if (depth >= MAX_COMPACT_DEPTH) {
663
+ return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
664
+ }
665
+ if (context.seen.has(value)) return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
666
+ context.seen.add(value);
667
+ try {
668
+ // Preflight the complete structure without serializing it. If it is
669
+ // larger than the inline budget, retain it as one opaque value only when
670
+ // its full JSON representation can fit in the bounded content store.
671
+ // This avoids JSON.stringify() on an arbitrarily large array/object.
672
+ const objectValue = value as DesktopJsonRecord;
673
+ const collectionSize = Array.isArray(value)
674
+ ? value.length
675
+ : boundedOwnEntryCount(objectValue, MAX_COMPACT_COLLECTION_ENTRIES + 1);
676
+ if (collectionSize > MAX_INLINE_COLLECTION_ENTRIES) {
677
+ const serialized = this.safeRetainedJson(value);
678
+ if (serialized) {
679
+ return this.contentReference(threadId, revision, serialized, "application/json", context);
680
+ }
681
+ return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
682
+ }
683
+ if (Array.isArray(value)) {
684
+ const output = value.map(entry => this.compactValue(entry, threadId, revision, key, context, depth + 1));
685
+ if (estimateJsonBytes(output, DESKTOP_HISTORY_PAGE_MAX_INLINE_FIELD_BYTES) === null) {
686
+ const serialized = this.safeRetainedJson(value);
687
+ if (serialized) return this.contentReference(threadId, revision, serialized, "application/json", context);
688
+ return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
689
+ }
690
+ return output;
691
+ }
692
+ const output: DesktopJsonRecord = {};
693
+ let keys = 0;
694
+ for (const entryKey in objectValue) {
695
+ if (!Object.prototype.hasOwnProperty.call(objectValue, entryKey)) continue;
696
+ keys += 1;
697
+ if (keys > MAX_COMPACT_COLLECTION_ENTRIES) {
698
+ return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
699
+ }
700
+ const entryValue = objectValue[entryKey];
701
+ if (byteLength(entryKey) > MAX_COMPACT_KEY_BYTES) {
702
+ return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
703
+ }
704
+ output[entryKey] = this.compactValue(entryValue, threadId, revision, entryKey, context, depth + 1);
705
+ }
706
+ if (estimateJsonBytes(output, DESKTOP_HISTORY_PAGE_MAX_INLINE_FIELD_BYTES) === null) {
707
+ const serialized = this.safeRetainedJson(value);
708
+ if (serialized) return this.contentReference(threadId, revision, serialized, "application/json", context);
709
+ return { kind: "missing", reason: "not-retained" } satisfies DesktopMissingContent;
710
+ }
711
+ return output;
712
+ } finally {
713
+ context.seen.delete(value);
714
+ }
715
+ }
716
+
717
+ private safeRetainedJson(value: unknown): string | null {
718
+ const available = Math.min(
719
+ this.maxRetainedContentBytes,
720
+ Math.max(0, this.maxRetainedContentBytes - this.retainedContentBytes),
721
+ );
722
+ if (available <= 0) return null;
723
+ const estimate = estimateJsonBytes(value, available);
724
+ if (estimate === null || estimate > available) return null;
725
+ return safeJsonStringWithin(value, available);
726
+ }
727
+
728
+ private contentReference(
729
+ threadId: string,
730
+ revision: string,
731
+ value: string,
732
+ mediaType: string,
733
+ context: BuildContext,
734
+ ): DesktopBoundedContent {
735
+ // Reject by byte length before allocating a Buffer. A single field can be
736
+ // larger than the entire retained-content budget (the incident that
737
+ // motivated this store was >170 MiB).
738
+ const length = byteLength(value);
739
+ if (
740
+ length === 0
741
+ || length > this.maxRetainedContentBytes
742
+ || this.retainedContentBytes + length > this.maxRetainedContentBytes
743
+ || context.contentHandles.length >= this.maxContentEntries
744
+ ) {
745
+ return { kind: "missing", reason: "not-retained" };
746
+ }
747
+ const handle = opaqueToken("desktop_content_v1_");
748
+ const bytes = Buffer.from(value, "utf8");
749
+ this.content.set(handle, {
750
+ threadId,
751
+ sourceRevision: revision,
752
+ value: bytes,
753
+ mediaType,
754
+ expiresAt: this.now() + this.contentTtlMs,
755
+ lastUsedAt: this.now(),
756
+ });
757
+ this.retainedContentBytes += bytes.byteLength;
758
+ context.handlesCreated += 1;
759
+ context.contentHandles.push(handle);
760
+ this.prune();
761
+ return {
762
+ kind: "handle",
763
+ handle,
764
+ preview: truncateUtf8(value, DEFAULT_CONTENT_PREVIEW_BYTES),
765
+ byteLength: bytes.byteLength,
766
+ mediaType,
767
+ truncatedInline: true,
768
+ };
769
+ }
770
+
771
+ private issuePageToken(
772
+ input: Omit<PageTokenState, "issuedAt" | "expiresAt">,
773
+ context?: BuildContext,
774
+ ): string {
775
+ this.prune();
776
+ const now = this.now();
777
+ const state: PageTokenState = {
778
+ ...input,
779
+ issuedAt: now,
780
+ expiresAt: now + this.pageTokenTtlMs,
781
+ };
782
+ const token = tokenStateToString(state);
783
+ this.pageTokens.set(token, state);
784
+ while (this.pageTokens.size > this.maxPageTokens) {
785
+ const oldest = this.pageTokens.keys().next().value as string | undefined;
786
+ if (!oldest) break;
787
+ this.pageTokens.delete(oldest);
788
+ }
789
+ context?.pageTokens.push(token);
790
+ return token;
791
+ }
792
+
793
+ private prune(): void {
794
+ const now = this.now();
795
+ for (const [token, state] of this.pageTokens) {
796
+ if (state.expiresAt <= now) this.pageTokens.delete(token);
797
+ }
798
+ for (const [handle, entry] of this.content) {
799
+ if (entry.expiresAt <= now) {
800
+ this.retainedContentBytes -= entry.value.byteLength;
801
+ this.content.delete(handle);
802
+ }
803
+ }
804
+ this.retainedContentBytes = Math.max(0, this.retainedContentBytes);
805
+ while (this.content.size > this.maxContentEntries) this.evictLeastRecentlyUsedContent();
806
+ while (this.retainedContentBytes > this.maxRetainedContentBytes) this.evictLeastRecentlyUsedContent();
807
+ }
808
+
809
+ private evictLeastRecentlyUsedContent(): void {
810
+ let candidate: [string, ContentEntry] | null = null;
811
+ for (const row of this.content) {
812
+ if (!candidate || row[1].lastUsedAt < candidate[1].lastUsedAt) candidate = row;
813
+ }
814
+ if (!candidate) return;
815
+ this.content.delete(candidate[0]);
816
+ this.retainedContentBytes -= candidate[1].value.byteLength;
817
+ }
818
+ }
819
+
820
+ function safeJsonStringWithin(value: unknown, maximumBytes: number): string | null {
821
+ if (maximumBytes <= 0 || (isRecord(value) && typeof value.toJSON === "function")) return null;
822
+ try {
823
+ const encoded = JSON.stringify(value);
824
+ if (typeof encoded !== "string" || byteLength(encoded) > maximumBytes) return null;
825
+ return encoded;
826
+ } catch {
827
+ return null;
828
+ }
829
+ }
830
+
831
+ function boundedOwnEntryCount(value: DesktopJsonRecord, limit: number): number {
832
+ let count = 0;
833
+ for (const key in value) {
834
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
835
+ count += 1;
836
+ if (count >= limit) return count;
837
+ }
838
+ return count;
839
+ }
840
+
841
+ /**
842
+ * Estimate JSON bytes without creating a serialized copy. Returning null
843
+ * means the value is cyclic, unsupported, or already beyond the supplied
844
+ * bound. The estimate is deliberately conservative for strings so the later
845
+ * JSON.stringify call can never be asked to build an unbounded value.
846
+ */
847
+ function estimateJsonBytes(
848
+ value: unknown,
849
+ maximumBytes: number,
850
+ seen = new WeakSet<object>(),
851
+ depth = 0,
852
+ ): number | null {
853
+ if (maximumBytes <= 0) return null;
854
+ if (depth >= MAX_COMPACT_DEPTH) return null;
855
+ const stringBytes = (input: string): number | null => {
856
+ let total = 2; // quotes
857
+ for (const character of input) {
858
+ const code = character.codePointAt(0) ?? 0;
859
+ const codeUnit = character.charCodeAt(0);
860
+ total += character === "\b"
861
+ || character === "\f"
862
+ || character === "\n"
863
+ || character === "\r"
864
+ || character === "\t"
865
+ || character === '"'
866
+ || character === "\\"
867
+ ? 2
868
+ : codeUnit >= 0xd800 && codeUnit <= 0xdfff
869
+ ? 6 // JSON.stringify escapes lone UTF-16 surrogates
870
+ : code < 0x20
871
+ ? 6
872
+ : Buffer.byteLength(character, "utf8");
873
+ if (total > maximumBytes) return null;
874
+ }
875
+ return total;
876
+ };
877
+ const add = (left: number, right: number): number | null => {
878
+ const next = left + right;
879
+ return next > maximumBytes ? null : next;
880
+ };
881
+
882
+ if (typeof value === "string") return stringBytes(value);
883
+ if (value === null || typeof value === "boolean") return value === null ? 4 : 5;
884
+ if (typeof value === "number") {
885
+ const encoded = JSON.stringify(value);
886
+ return typeof encoded === "string" && byteLength(encoded) <= maximumBytes
887
+ ? byteLength(encoded)
888
+ : null;
889
+ }
890
+ if (typeof value !== "object") return 4;
891
+ if (seen.has(value)) return null;
892
+ if (isRecord(value) && typeof value.toJSON === "function") return null;
893
+ seen.add(value);
894
+ try {
895
+ let total = 2; // [] or {}
896
+ if (Array.isArray(value)) {
897
+ if (value.length > MAX_COMPACT_COLLECTION_ENTRIES) return null;
898
+ for (let index = 0; index < value.length; index += 1) {
899
+ if (index > 0) total += 1;
900
+ const entry = estimateJsonBytes(value[index], maximumBytes - total, seen, depth + 1);
901
+ if (entry === null) return null;
902
+ const next = add(total, entry);
903
+ if (next === null) return null;
904
+ total = next;
905
+ }
906
+ return total;
907
+ }
908
+ let index = 0;
909
+ const objectValue = value as DesktopJsonRecord;
910
+ for (const key in objectValue) {
911
+ if (!Object.prototype.hasOwnProperty.call(objectValue, key)) continue;
912
+ if (index >= MAX_COMPACT_COLLECTION_ENTRIES) return null;
913
+ if (index > 0) total += 1;
914
+ const keySize = stringBytes(key);
915
+ if (keySize === null) return null;
916
+ const withKey = add(total, keySize);
917
+ if (withKey === null) return null;
918
+ total = withKey + 1; // colon
919
+ if (total > maximumBytes) return null;
920
+ const entry = estimateJsonBytes(objectValue[key], maximumBytes - total, seen, depth + 1);
921
+ if (entry === null) return null;
922
+ const next = add(total, entry);
923
+ if (next === null) return null;
924
+ total = next;
925
+ index += 1;
926
+ }
927
+ return total;
928
+ } finally {
929
+ seen.delete(value);
930
+ }
931
+ }
932
+
933
+ function recordPageInfo(value: unknown): DesktopHistoryPageInfo | null {
934
+ if (!isRecord(value) || value.kind !== "desktop-history-page") return null;
935
+ return value as unknown as DesktopHistoryPageInfo;
936
+ }