@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.
- package/AGENTS_INSTALL.md +91 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +584 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-CZqebSPQ.css +1 -0
- package/gui/dist/assets/index-CkETtt7P.js +71 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/fonts/google-sans-cyrillic.woff2 +0 -0
- package/gui/dist/fonts/google-sans-latin.woff2 +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +2 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +118 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1205 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +83 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/command-code.ts +453 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +139 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +250 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1235 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +59 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +332 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +604 -0
- package/src/adapters/cursor/protobuf-events.ts +735 -0
- package/src/adapters/cursor/protobuf-request.ts +719 -0
- package/src/adapters/cursor/request-builder.ts +280 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +621 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +59 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +520 -0
- package/src/adapters/google-antigravity-wire.ts +140 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +24 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +859 -0
- package/src/adapters/identity.ts +77 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +208 -0
- package/src/adapters/kiro-events.ts +197 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +104 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +224 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1924 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1265 -0
- package/src/adapters/openai-responses.ts +1309 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/android-remote/assets.ts +218 -0
- package/src/android-remote/attachments.ts +168 -0
- package/src/android-remote/auth.ts +237 -0
- package/src/android-remote/cloudflare-provisioning.ts +409 -0
- package/src/android-remote/cloudflare-secret.ts +106 -0
- package/src/android-remote/cloudflare-tunnel.ts +488 -0
- package/src/android-remote/cloudflared.ts +286 -0
- package/src/android-remote/codex-app-server.ts +565 -0
- package/src/android-remote/desktop-history-page.ts +936 -0
- package/src/android-remote/desktop-ipc.ts +3643 -0
- package/src/android-remote/desktop-ownership-store.ts +98 -0
- package/src/android-remote/desktop-project-registration.ts +129 -0
- package/src/android-remote/desktop-session-stream.ts +1110 -0
- package/src/android-remote/desktop-workspace-state.ts +443 -0
- package/src/android-remote/file-change-parser.ts +112 -0
- package/src/android-remote/gateway.ts +9108 -0
- package/src/android-remote/mutation-store.ts +299 -0
- package/src/android-remote/projection.ts +1780 -0
- package/src/android-remote/queued-turn-store.ts +248 -0
- package/src/android-remote/session-command-recovery.ts +1384 -0
- package/src/android-remote/store.ts +466 -0
- package/src/android-remote/thread-reconciliation.ts +133 -0
- package/src/android-remote/thread-source-paths.ts +307 -0
- package/src/android-remote/thread-stream.ts +546 -0
- package/src/android-remote/turn-activity.ts +235 -0
- package/src/android-remote/user-message-identity.ts +94 -0
- package/src/bridge.ts +1793 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +821 -0
- package/src/claude/agents-inject.ts +267 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +601 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +519 -0
- package/src/claude/model-info.ts +154 -0
- package/src/claude/outbound.ts +898 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +296 -0
- package/src/cli/account-auth.ts +250 -0
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +476 -0
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +297 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +211 -0
- package/src/cli/claude.ts +302 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/codex-shim-readiness.ts +69 -0
- package/src/cli/combo.ts +124 -0
- package/src/cli/config-command.ts +183 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/desktop-first-run.ts +25 -0
- package/src/cli/doctor.ts +1022 -0
- package/src/cli/export-command.ts +201 -0
- package/src/cli/help.ts +370 -0
- package/src/cli/index.ts +1565 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +225 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +35 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/models-runtime.ts +224 -0
- package/src/cli/models.ts +340 -0
- package/src/cli/observe.ts +170 -0
- package/src/cli/opencode.ts +587 -0
- package/src/cli/provider-runtime.ts +179 -0
- package/src/cli/provider.ts +476 -0
- package/src/cli/ready.ts +301 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +328 -0
- package/src/cli/star-prompt.ts +211 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/system-command.ts +196 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +205 -0
- package/src/cli/v2.ts +200 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +1109 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +172 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +195 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +40 -0
- package/src/codex/admission.ts +263 -0
- package/src/codex/app-server-processes.ts +799 -0
- package/src/codex/auth-api.ts +2098 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +480 -0
- package/src/codex/autostart-health.ts +156 -0
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +471 -0
- package/src/codex/catalog/bundled.ts +533 -0
- package/src/codex/catalog/effort.ts +432 -0
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +287 -0
- package/src/codex/catalog/native-models.ts +7 -0
- package/src/codex/catalog/parsing.ts +503 -0
- package/src/codex/catalog/provider-fetch.ts +2267 -0
- package/src/codex/catalog/sync.ts +1606 -0
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-refresh-status.ts +87 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +15 -0
- package/src/codex/codex-write-lock.ts +384 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +580 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-client-processes.ts +521 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +1091 -0
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +347 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +115 -0
- package/src/codex/history-provider.ts +1075 -0
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +204 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject-coordination.ts +257 -0
- package/src/codex/inject.ts +1857 -0
- package/src/codex/injected-marker.ts +79 -0
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +105 -0
- package/src/codex/journal.ts +172 -0
- package/src/codex/main-account-cache.ts +56 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/model-cache.ts +267 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +162 -0
- package/src/codex/native-main-owner.ts +329 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1531 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +348 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +691 -0
- package/src/codex/paths.ts +78 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +295 -0
- package/src/codex/project-config-warnings.ts +426 -0
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/provider-adoption.ts +242 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +494 -0
- package/src/codex/refresh.ts +60 -0
- package/src/codex/routing.ts +1855 -0
- package/src/codex/runtime.ts +659 -0
- package/src/codex/shim.ts +1215 -0
- package/src/codex/subagent-defaults.ts +557 -0
- package/src/codex/subagent-model-fallback.ts +560 -0
- package/src/codex/sync.ts +238 -0
- package/src/codex/transition-state.ts +612 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +374 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +44 -0
- package/src/combos/request.ts +64 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +392 -0
- package/src/config.ts +3270 -0
- package/src/generated/model-metadata.ts +144 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +540 -0
- package/src/grok/inspect.ts +45 -0
- package/src/grok/status.ts +127 -0
- package/src/grok/sync.ts +66 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +922 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +108 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +242 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +184 -0
- package/src/lib/bun-stream-caps.ts +127 -0
- package/src/lib/config-ownership.ts +438 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +182 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +168 -0
- package/src/lib/provider-environment.ts +470 -0
- package/src/lib/provider-outbound.ts +203 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +510 -0
- package/src/lib/remodex-home.ts +616 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +178 -0
- package/src/lib/shadow-call.ts +54 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +364 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/test-home-guard.ts +98 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +366 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +508 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +817 -0
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +402 -0
- package/src/model-sources.ts +73 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +231 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +443 -0
- package/src/oauth/index.ts +1280 -0
- package/src/oauth/key-providers.ts +128 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +621 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +655 -0
- package/src/oauth/token-guardian.ts +309 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +317 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/command-code-efforts.ts +85 -0
- package/src/providers/context-cap.ts +73 -0
- package/src/providers/derive.ts +451 -0
- package/src/providers/free-directory.ts +187 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +271 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +361 -0
- package/src/providers/openai-sidecar.ts +235 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +83 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +179 -0
- package/src/providers/quota.ts +1942 -0
- package/src/providers/reasoning-capabilities.ts +336 -0
- package/src/providers/registry.ts +2375 -0
- package/src/providers/slug-codec.ts +74 -0
- package/src/providers/xai-transport.ts +149 -0
- package/src/reasoning-effort.ts +243 -0
- package/src/responses/compaction.ts +124 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +714 -0
- package/src/responses/reasoning-envelope.ts +60 -0
- package/src/responses/reasoning-replay-cache.ts +106 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +431 -0
- package/src/responses/state.ts +1039 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +802 -0
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +205 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +605 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +424 -0
- package/src/routing/quota.ts +145 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/adapter-resolve.ts +83 -0
- package/src/server/auth-cors.ts +606 -0
- package/src/server/chat-completions.ts +379 -0
- package/src/server/claude-messages.ts +980 -0
- package/src/server/effort-policy.ts +251 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +152 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +485 -0
- package/src/server/index.ts +1633 -0
- package/src/server/lifecycle.ts +482 -0
- package/src/server/live.ts +609 -0
- package/src/server/management/agent-settings-routes.ts +1180 -0
- package/src/server/management/android-remote-routes.ts +390 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +244 -0
- package/src/server/management/config-routes.ts +602 -0
- package/src/server/management/context.ts +88 -0
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +516 -0
- package/src/server/management/model-routes.ts +519 -0
- package/src/server/management/model-rows.ts +143 -0
- package/src/server/management/native-integration-routes.ts +781 -0
- package/src/server/management/oauth-account-routes.ts +573 -0
- package/src/server/management/provider-routes.ts +781 -0
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +384 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +106 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +433 -0
- package/src/server/management/system-routes.ts +141 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +269 -0
- package/src/server/management-auth.ts +353 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +156 -0
- package/src/server/proxy-liveness.ts +326 -0
- package/src/server/proxy-stop.ts +92 -0
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +353 -0
- package/src/server/relay.ts +1179 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1072 -0
- package/src/server/responses/collaboration.ts +409 -0
- package/src/server/responses/compact.ts +710 -0
- package/src/server/responses/core.ts +3561 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +171 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/policy-fallback.ts +152 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +272 -0
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses.ts +10 -0
- package/src/server/search.ts +181 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +263 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +119 -0
- package/src/server/system-env.ts +418 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +470 -0
- package/src/service-manager-probe.ts +824 -0
- package/src/service.ts +3011 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +364 -0
- package/src/tray/windows.ts +738 -0
- package/src/types.ts +1531 -0
- package/src/update/badge.ts +72 -0
- package/src/update/desktop-release.ts +1620 -0
- package/src/update/index.ts +402 -0
- package/src/update/job.ts +1906 -0
- package/src/update/notify.ts +261 -0
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +283 -0
- package/src/usage/log.ts +695 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +127 -0
- package/src/vision/index.ts +558 -0
- package/src/vision/reasoning.ts +55 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +791 -0
- package/src/web-search/parse.ts +235 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, unlinkSync, writeSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { CODEX_HOME } from "./paths";
|
|
7
|
+
import { atomicWriteFile, getConfigDir } from "../config";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
11
|
+
* Bounds peak memory while reconstructing thread rows; never write the decoded
|
|
12
|
+
* JSONL to disk.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
const STATE_DB_PATH = join(CODEX_HOME, "state_5.sqlite");
|
|
17
|
+
/**
|
|
18
|
+
* The manifest that shadows one state database.
|
|
19
|
+
*
|
|
20
|
+
* Exported because the history job must resolve it at CALL time for a Worker
|
|
21
|
+
* that does not inherit this module's load-time constants — and must resolve it
|
|
22
|
+
* the same way, since a manifest addressed differently is a different manifest.
|
|
23
|
+
*/
|
|
24
|
+
export function historyBackupPathFor(stateDbPath: string): string {
|
|
25
|
+
const normalized = process.platform === "win32" ? resolve(stateDbPath).toLowerCase() : resolve(stateDbPath);
|
|
26
|
+
const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
27
|
+
return join(getConfigDir(), `codex-history-backup-${id}.json`);
|
|
28
|
+
}
|
|
29
|
+
const HISTORY_BACKUP_PATH = historyBackupPathFor(STATE_DB_PATH);
|
|
30
|
+
const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Open the live `state_5.sqlite` the way the Codex app expects a *secondary* writer to behave:
|
|
34
|
+
* wait on the WAL/file lock instead of failing instantly, so we never race the app's own
|
|
35
|
+
* connection pool into a half-applied checkpoint. The app opens this DB with `busy_timeout=5s`
|
|
36
|
+
* (see codex-rs `state::runtime::base_sqlite_options`); we mirror that here.
|
|
37
|
+
*/
|
|
38
|
+
let historyDbBusyTimeoutMs = 5000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Test-only knob: Windows CI can spend the FULL busy timeout on a transient file lock, which
|
|
42
|
+
* alone exceeds bun's 5s default per-test timeout. Tests shrink this so a busy DB fails fast
|
|
43
|
+
* into withHistoryRetry instead of stalling; production keeps the codex-rs-matching 5s.
|
|
44
|
+
*/
|
|
45
|
+
export function setHistoryDbBusyTimeoutForTests(ms: number): void {
|
|
46
|
+
historyDbBusyTimeoutMs = ms;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function openStateDb(stateDbPath: string): Database {
|
|
50
|
+
const db = new Database(stateDbPath);
|
|
51
|
+
try {
|
|
52
|
+
db.exec(`PRAGMA busy_timeout = ${historyDbBusyTimeoutMs}`);
|
|
53
|
+
} catch {
|
|
54
|
+
/* best-effort: an older sqlite without busy_timeout still works, just less politely */
|
|
55
|
+
}
|
|
56
|
+
return db;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Append one JSONL line to a rollout using an O_APPEND handle, exactly like the Codex app's own
|
|
61
|
+
* metadata writer (`append_rollout_item_to_path` in codex-rs `rollout/src/recorder.rs`).
|
|
62
|
+
*
|
|
63
|
+
* Why append instead of rewriting line 1:
|
|
64
|
+
* - The app caches the live session's append handle and only reopens it when the handle is gone
|
|
65
|
+
* (codex-rs `RolloutWriterState::ensure_writer_open`). A temp+rename swap would orphan that
|
|
66
|
+
* handle; an in-place truncate would race the app's concurrent appends and clip new turns.
|
|
67
|
+
* - The app folds metadata by replaying every `session_meta` line in file order, last-writer-wins
|
|
68
|
+
* (codex-rs `apply_session_meta_from_item`), so a trailing `session_meta` overrides earlier ones.
|
|
69
|
+
* Real rollouts already contain multiple `session_meta` lines for this reason.
|
|
70
|
+
* For legacy unnumbered rollouts, O_APPEND makes each write land at EOF atomically, so it composes
|
|
71
|
+
* with the app appending concurrently. Ordered/paginated rollouts remain writer-owned: a second
|
|
72
|
+
* writer cannot safely allocate their next ordinal. We do not touch mtime: a fresh mtime is correct
|
|
73
|
+
* here (the app uses mtime as the rollout's updated_at), and forcing it backwards could hide a real
|
|
74
|
+
* edit from list ordering.
|
|
75
|
+
*/
|
|
76
|
+
function appendRolloutLine(path: string, line: string): void {
|
|
77
|
+
const fd = openSync(path, "a");
|
|
78
|
+
try {
|
|
79
|
+
const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
|
|
80
|
+
let offset = 0;
|
|
81
|
+
while (offset < buf.length) {
|
|
82
|
+
offset += writeSync(fd, buf, offset, buf.length - offset, null);
|
|
83
|
+
}
|
|
84
|
+
try { fsyncSync(fd); } catch { /* best-effort durability */ }
|
|
85
|
+
} finally {
|
|
86
|
+
closeSync(fd);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Patch the `model_provider` value inside the FIRST line of a rollout *in place, length-preserving*.
|
|
92
|
+
*
|
|
93
|
+
* Why this exists in addition to {@link appendRolloutLine}: Codex resolves a thread's provider via
|
|
94
|
+
* two different readers. The SQLite replay path folds every `session_meta` line last-writer-wins
|
|
95
|
+
* (covered by appending a trailing meta), but `read_session_meta_line` reads only the FIRST line
|
|
96
|
+
* and `update_thread_metadata` clones it when the app later writes git/memory-mode metadata
|
|
97
|
+
* (codex-rs `thread-store/src/local/update_thread_metadata.rs`). If the first line still says
|
|
98
|
+
* `opencodex` after a native restore, that clone re-appends `opencodex` and last-writer-wins
|
|
99
|
+
* resurrects the routed provider. So a durable restore must also fix line 1.
|
|
100
|
+
*
|
|
101
|
+
* Safety: Codex parses each rollout line as `serde_json::from_str(line.trim())`, which tolerates
|
|
102
|
+
* insignificant JSON whitespace. We therefore replace the provider value and pad the removed bytes
|
|
103
|
+
* with spaces so the line's byte length is unchanged. Equal length means we can write at offset 0
|
|
104
|
+
* with no truncate and no inode swap, so this composes safely with the app's cached append handle.
|
|
105
|
+
* Only length-preserving shrinks are handled (e.g. "opencodex" -> "openai"); callers that would
|
|
106
|
+
* grow the value fall back to append-only, which is correct for the opencodex direction.
|
|
107
|
+
*
|
|
108
|
+
* Returns true when line 1 was patched, false when it could not be done safely (missing file,
|
|
109
|
+
* non-`session_meta` first line, id mismatch, value already correct, or a length-growing change).
|
|
110
|
+
*/
|
|
111
|
+
function patchFirstLineProviderInPlace(path: string, expectedId: string, provider: string): boolean {
|
|
112
|
+
if (!existsSync(path)) return false;
|
|
113
|
+
const fd = openSync(path, "r+");
|
|
114
|
+
try {
|
|
115
|
+
// Read the first line by growing the probe until we hit a newline. session_meta lines embed
|
|
116
|
+
// base_instructions and can be tens of KB; a fixed cap would silently skip the in-place patch
|
|
117
|
+
// (and fall back to append-only, re-opening the first-line-clone resurrection gap), so we read
|
|
118
|
+
// until the line actually ends rather than guessing a ceiling.
|
|
119
|
+
const CHUNK = 1 << 16;
|
|
120
|
+
const MAX_FIRST_LINE = 1 << 24; // 16 MiB hard stop so a newline-less/corrupt file can't OOM us.
|
|
121
|
+
let collected = Buffer.alloc(0);
|
|
122
|
+
let nlIndex = -1;
|
|
123
|
+
let pos = 0;
|
|
124
|
+
while (nlIndex === -1) {
|
|
125
|
+
const chunk = Buffer.alloc(CHUNK);
|
|
126
|
+
const read = readSync(fd, chunk, 0, CHUNK, pos);
|
|
127
|
+
if (read === 0) break; // EOF with no newline: single-line file, skip
|
|
128
|
+
collected = Buffer.concat([collected, chunk.subarray(0, read)]);
|
|
129
|
+
nlIndex = collected.indexOf(0x0a);
|
|
130
|
+
pos += read;
|
|
131
|
+
if (collected.length > MAX_FIRST_LINE) return false;
|
|
132
|
+
}
|
|
133
|
+
if (nlIndex === -1) return false; // no newline anywhere: skip
|
|
134
|
+
const firstLine = collected.subarray(0, nlIndex).toString("utf8");
|
|
135
|
+
|
|
136
|
+
const meta = parseSessionMetaLine(firstLine);
|
|
137
|
+
if (!meta) return false;
|
|
138
|
+
if (meta.record.payload.id !== expectedId) return false;
|
|
139
|
+
if (meta.record.payload.model_provider === provider) return false;
|
|
140
|
+
|
|
141
|
+
// Locate the exact `"model_provider":"<value>"` token (allowing whitespace after the colon).
|
|
142
|
+
const match = firstLine.match(/"model_provider"\s*:\s*"([^"\\]*)"/);
|
|
143
|
+
if (!match || match.index === undefined) return false;
|
|
144
|
+
const oldToken = match[0];
|
|
145
|
+
const newCore = `"model_provider":"${provider}"`;
|
|
146
|
+
if (Buffer.byteLength(newCore, "utf8") > Buffer.byteLength(oldToken, "utf8")) return false; // grow: not length-preserving
|
|
147
|
+
const pad = " ".repeat(Buffer.byteLength(oldToken, "utf8") - Buffer.byteLength(newCore, "utf8"));
|
|
148
|
+
const newToken = `${newCore}${pad}`;
|
|
149
|
+
|
|
150
|
+
const patchedLine = firstLine.slice(0, match.index) + newToken + firstLine.slice(match.index + oldToken.length);
|
|
151
|
+
// Length must be identical so the trailing bytes (newline + rest of file) are untouched.
|
|
152
|
+
if (Buffer.byteLength(patchedLine, "utf8") !== Buffer.byteLength(firstLine, "utf8")) return false;
|
|
153
|
+
// Sanity: the patched line must still parse and carry the new provider.
|
|
154
|
+
const reparsed = parseSessionMetaLine(patchedLine);
|
|
155
|
+
if (!reparsed || reparsed.record.payload.model_provider !== provider) return false;
|
|
156
|
+
|
|
157
|
+
const out = Buffer.from(patchedLine, "utf8");
|
|
158
|
+
let offset = 0;
|
|
159
|
+
while (offset < out.length) {
|
|
160
|
+
offset += writeSync(fd, out, offset, out.length - offset, offset);
|
|
161
|
+
}
|
|
162
|
+
try { fsyncSync(fd); } catch { /* best-effort durability */ }
|
|
163
|
+
return true;
|
|
164
|
+
} finally {
|
|
165
|
+
closeSync(fd);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type CodexHistoryProvider = "openai" | "opencodex";
|
|
170
|
+
|
|
171
|
+
export type CodexHistoryFailureReason = "busy" | "permission";
|
|
172
|
+
|
|
173
|
+
export interface CodexHistorySyncResult {
|
|
174
|
+
rows: number;
|
|
175
|
+
files: number;
|
|
176
|
+
ejectedRows?: number;
|
|
177
|
+
/** Set when a lock/busy error survived retries and the sync was SKIPPED, not empty. */
|
|
178
|
+
failed?: true;
|
|
179
|
+
/** Why the retry budget was exhausted when `failed` is set. */
|
|
180
|
+
failureReason?: CodexHistoryFailureReason;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface ThreadRow {
|
|
184
|
+
id: string;
|
|
185
|
+
rollout_path: string;
|
|
186
|
+
model_provider: string;
|
|
187
|
+
source: string;
|
|
188
|
+
has_user_event: number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
interface BackupEntry {
|
|
192
|
+
id: string;
|
|
193
|
+
rolloutPath: string;
|
|
194
|
+
modelProvider: string;
|
|
195
|
+
source: string;
|
|
196
|
+
hasUserEvent: number;
|
|
197
|
+
/** Model observed while the original codex-lb row was captured; used for compare-and-swap. */
|
|
198
|
+
codexLbOriginalModel?: string | null;
|
|
199
|
+
/** One-time marker: the adopted provider identity is now encoded in the task model slug. */
|
|
200
|
+
codexLbModelQualified?: boolean;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface BackupManifest {
|
|
204
|
+
version: 1;
|
|
205
|
+
stateDbPath?: string;
|
|
206
|
+
entries: Record<string, BackupEntry>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface NativeRestoreTarget {
|
|
210
|
+
modelProvider: string;
|
|
211
|
+
source: string;
|
|
212
|
+
hasUserEvent: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface ThreadRowWithModel extends ThreadRow {
|
|
216
|
+
model: string | null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function threadsHaveModelColumn(db: Database): boolean {
|
|
220
|
+
const row = db.query<{ n: number }, []>(`
|
|
221
|
+
SELECT count(*) AS n
|
|
222
|
+
FROM pragma_table_info('threads')
|
|
223
|
+
WHERE name = 'model'
|
|
224
|
+
`).get();
|
|
225
|
+
return (row?.n ?? 0) > 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function qualifyCodexLbModel(model: string | null): string | null {
|
|
229
|
+
if (!model || model.includes("/")) return model;
|
|
230
|
+
return `codex-lb/${model}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function samePath(a: string, b: string): boolean {
|
|
234
|
+
const left = resolve(a);
|
|
235
|
+
const right = resolve(b);
|
|
236
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function readBackup(path: string, stateDbPath?: string): BackupManifest {
|
|
240
|
+
if (!existsSync(path)) return { version: 1, stateDbPath, entries: {} };
|
|
241
|
+
try {
|
|
242
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<BackupManifest>;
|
|
243
|
+
if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") {
|
|
244
|
+
return { version: 1, stateDbPath, entries: {} };
|
|
245
|
+
}
|
|
246
|
+
if (stateDbPath && typeof parsed.stateDbPath === "string" && !samePath(parsed.stateDbPath, stateDbPath)) {
|
|
247
|
+
return { version: 1, stateDbPath, entries: {} };
|
|
248
|
+
}
|
|
249
|
+
return { version: 1, stateDbPath: parsed.stateDbPath ?? stateDbPath, entries: parsed.entries };
|
|
250
|
+
} catch {
|
|
251
|
+
return { version: 1, stateDbPath, entries: {} };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function writeBackup(path: string, manifest: BackupManifest, stateDbPath?: string): void {
|
|
256
|
+
if (Object.keys(manifest.entries).length === 0) {
|
|
257
|
+
if (existsSync(path)) unlinkSync(path);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
261
|
+
atomicWriteFile(path, JSON.stringify({ ...manifest, stateDbPath: manifest.stateDbPath ?? stateDbPath }, null, 2) + "\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function rememberOriginal(manifest: BackupManifest, row: ThreadRow): void {
|
|
265
|
+
if (manifest.entries[row.id]) return;
|
|
266
|
+
const model = "model" in row
|
|
267
|
+
? (typeof (row as ThreadRowWithModel).model === "string" ? (row as ThreadRowWithModel).model : null)
|
|
268
|
+
: undefined;
|
|
269
|
+
manifest.entries[row.id] = {
|
|
270
|
+
id: row.id,
|
|
271
|
+
rolloutPath: row.rollout_path,
|
|
272
|
+
modelProvider: row.model_provider,
|
|
273
|
+
source: row.source,
|
|
274
|
+
hasUserEvent: Number(row.has_user_event) || 0,
|
|
275
|
+
...(model !== undefined ? { codexLbOriginalModel: model } : {}),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
interface ParsedSessionMeta {
|
|
280
|
+
record: {
|
|
281
|
+
type?: unknown;
|
|
282
|
+
timestamp?: unknown;
|
|
283
|
+
ordinal?: unknown;
|
|
284
|
+
payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown>;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
|
|
289
|
+
function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
290
|
+
let parsed: unknown;
|
|
291
|
+
try {
|
|
292
|
+
parsed = JSON.parse(line);
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
297
|
+
const record = parsed as ParsedSessionMeta["record"];
|
|
298
|
+
if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
|
|
299
|
+
return { record };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Find the LAST `session_meta` line in a rollout, mirroring the app's last-writer-wins fold
|
|
304
|
+
* (codex-rs `apply_session_meta_from_item`). We base our patch on the most recent metadata so we
|
|
305
|
+
* never resurrect a stale provider that a later app-written `session_meta` already changed.
|
|
306
|
+
*/
|
|
307
|
+
export function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
|
|
308
|
+
const raw = readFileSync(path, "utf8");
|
|
309
|
+
const lines = raw.split("\n");
|
|
310
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
311
|
+
const line = lines[i];
|
|
312
|
+
if (!line) continue;
|
|
313
|
+
if (!line.includes("\"session_meta\"")) continue;
|
|
314
|
+
const meta = parseSessionMetaLine(line);
|
|
315
|
+
if (meta) return meta;
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Find the newest session metadata that belongs to one canonical task id. */
|
|
321
|
+
function readLatestSessionMetaForId(path: string, expectedId: string): ParsedSessionMeta | null {
|
|
322
|
+
const raw = readFileSync(path, "utf8");
|
|
323
|
+
const lines = raw.split("\n");
|
|
324
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
325
|
+
const line = lines[i];
|
|
326
|
+
if (!line || !line.includes("\"session_meta\"")) continue;
|
|
327
|
+
const meta = parseSessionMetaLine(line);
|
|
328
|
+
if (meta?.record.payload.id === expectedId) return meta;
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Ordered rollouts must only be changed by Codex, which owns their ordinal allocation. */
|
|
334
|
+
function isWriterOwnedRollout(record: ParsedSessionMeta["record"]): boolean {
|
|
335
|
+
return record.payload.history_mode === "paginated" || Object.hasOwn(record, "ordinal");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
340
|
+
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
341
|
+
*
|
|
342
|
+
* Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
|
|
343
|
+
* first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
|
|
344
|
+
*/
|
|
345
|
+
export interface RolloutThreadFields {
|
|
346
|
+
id: string;
|
|
347
|
+
modelProvider: string;
|
|
348
|
+
source: string;
|
|
349
|
+
firstUserMessage: string;
|
|
350
|
+
hasUserEvent: number;
|
|
351
|
+
cwd?: string;
|
|
352
|
+
historyMode?: string;
|
|
353
|
+
cliVersion?: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function textFromContentParts(content: unknown): string | null {
|
|
357
|
+
if (typeof content === "string" && content.trim()) return content.trim();
|
|
358
|
+
if (!Array.isArray(content)) return null;
|
|
359
|
+
const parts: string[] = [];
|
|
360
|
+
for (const part of content) {
|
|
361
|
+
if (!part || typeof part !== "object") continue;
|
|
362
|
+
const p = part as Record<string, unknown>;
|
|
363
|
+
if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
|
|
364
|
+
else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
|
|
365
|
+
}
|
|
366
|
+
const joined = parts.join("\n").trim();
|
|
367
|
+
return joined || null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Extract the first user-message preview from a rollout line, or null. */
|
|
371
|
+
function extractUserMessagePreview(line: string): string | null {
|
|
372
|
+
let parsed: unknown;
|
|
373
|
+
try {
|
|
374
|
+
parsed = JSON.parse(line);
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
379
|
+
const record = parsed as { type?: unknown; payload?: unknown };
|
|
380
|
+
const payload = record.payload;
|
|
381
|
+
if (!payload || typeof payload !== "object") return null;
|
|
382
|
+
const p = payload as Record<string, unknown>;
|
|
383
|
+
|
|
384
|
+
if (record.type === "event_msg") {
|
|
385
|
+
// codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
|
|
386
|
+
if (p.type === "user_message" || typeof p.message === "string") {
|
|
387
|
+
if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
|
|
388
|
+
const fromContent = textFromContentParts(p.content);
|
|
389
|
+
if (fromContent) return fromContent;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (record.type === "response_item") {
|
|
395
|
+
if (p.type === "message" && p.role === "user") {
|
|
396
|
+
return textFromContentParts(p.content);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
|
|
404
|
+
* Returns null when the file is missing or has no parseable `session_meta`.
|
|
405
|
+
*
|
|
406
|
+
* Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
|
|
407
|
+
* rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
|
|
408
|
+
* no decompressed copy is written to disk.
|
|
409
|
+
*/
|
|
410
|
+
export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
|
|
411
|
+
if (!path || !existsSync(path)) return null;
|
|
412
|
+
let raw: string;
|
|
413
|
+
try {
|
|
414
|
+
raw = path.endsWith(".zst")
|
|
415
|
+
? decompressRolloutZstUtf8(path)
|
|
416
|
+
: readFileSync(path, "utf8");
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
return parseThreadFieldsFromRolloutText(raw);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function decompressRolloutZstUtf8(
|
|
424
|
+
path: string,
|
|
425
|
+
maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
|
|
426
|
+
): string {
|
|
427
|
+
const compressed = readFileSync(path);
|
|
428
|
+
const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
|
|
429
|
+
maxOutputLength: maxBytes,
|
|
430
|
+
});
|
|
431
|
+
if (decoded.byteLength > maxBytes) {
|
|
432
|
+
throw new Error("rollout_zst_too_large");
|
|
433
|
+
}
|
|
434
|
+
return new TextDecoder().decode(decoded);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
|
|
438
|
+
const lines = raw.split("\n");
|
|
439
|
+
let latest: ParsedSessionMeta | null = null;
|
|
440
|
+
let firstUserMessage = "";
|
|
441
|
+
for (const line of lines) {
|
|
442
|
+
if (!line) continue;
|
|
443
|
+
if (line.includes("\"session_meta\"")) {
|
|
444
|
+
const meta = parseSessionMetaLine(line);
|
|
445
|
+
if (meta) latest = meta;
|
|
446
|
+
}
|
|
447
|
+
if (!firstUserMessage) {
|
|
448
|
+
const preview = extractUserMessagePreview(line);
|
|
449
|
+
if (preview) firstUserMessage = preview;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!latest) return null;
|
|
453
|
+
const payload = latest.record.payload;
|
|
454
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
455
|
+
if (!id) return null;
|
|
456
|
+
const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
|
|
457
|
+
? payload.model_provider
|
|
458
|
+
: "openai";
|
|
459
|
+
const source = typeof payload.source === "string" && payload.source
|
|
460
|
+
? payload.source
|
|
461
|
+
: "cli";
|
|
462
|
+
return {
|
|
463
|
+
id,
|
|
464
|
+
modelProvider,
|
|
465
|
+
source,
|
|
466
|
+
firstUserMessage,
|
|
467
|
+
hasUserEvent: firstUserMessage.trim() ? 1 : 0,
|
|
468
|
+
...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
|
|
469
|
+
...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
|
|
470
|
+
...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
|
|
476
|
+
* rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
|
|
477
|
+
* is accidentally reset to empty) and applies only the requested changes. Returns false when the
|
|
478
|
+
* rollout is missing, has no parseable `session_meta`, its latest `session_meta` belongs to a
|
|
479
|
+
* different thread id, it uses Codex-owned ordinal ordering, or it already matches the desired
|
|
480
|
+
* values.
|
|
481
|
+
*/
|
|
482
|
+
function updateSessionMeta(
|
|
483
|
+
path: string,
|
|
484
|
+
expectedId: string,
|
|
485
|
+
patch: { provider?: string; source?: string },
|
|
486
|
+
options: { patchFirstLine?: boolean; findLatestMatchingId?: boolean } = {},
|
|
487
|
+
): boolean {
|
|
488
|
+
if (!path || !existsSync(path)) return false;
|
|
489
|
+
|
|
490
|
+
const latest = options.findLatestMatchingId
|
|
491
|
+
? readLatestSessionMetaForId(path, expectedId)
|
|
492
|
+
: readLatestSessionMeta(path);
|
|
493
|
+
if (!latest) return false;
|
|
494
|
+
const record = latest.record;
|
|
495
|
+
|
|
496
|
+
// The app ignores `session_meta` lines whose payload id != the canonical thread id
|
|
497
|
+
// (codex-rs `apply_session_meta_from_item`). Forked rollouts can embed a source session's
|
|
498
|
+
// metadata, so an id-mismatched latest line means we'd be cloning the wrong thread's meta and
|
|
499
|
+
// appending a line the app would discard. Skip rather than write a no-op/misleading line.
|
|
500
|
+
const payloadId = record.payload.id;
|
|
501
|
+
if (typeof payloadId !== "string" || payloadId !== expectedId) return false;
|
|
502
|
+
|
|
503
|
+
// Paginated rollouts require one globally increasing ordinal per record. Cloning this metadata
|
|
504
|
+
// would clone its old ordinal, while computing max + 1 would race Codex's live writer. Leave the
|
|
505
|
+
// rollout byte-identical and let the SQLite/provider migration proceed independently.
|
|
506
|
+
if (isWriterOwnedRollout(record)) return false;
|
|
507
|
+
|
|
508
|
+
let changed = false;
|
|
509
|
+
if (patch.provider !== undefined && record.payload.model_provider !== patch.provider) {
|
|
510
|
+
record.payload.model_provider = patch.provider;
|
|
511
|
+
changed = true;
|
|
512
|
+
}
|
|
513
|
+
if (patch.source !== undefined && record.payload.source !== patch.source) {
|
|
514
|
+
record.payload.source = patch.source;
|
|
515
|
+
changed = true;
|
|
516
|
+
}
|
|
517
|
+
if (!changed) return false;
|
|
518
|
+
|
|
519
|
+
// Cover Codex's *other* provider reader: `read_session_meta_line` reads only line 1, and the
|
|
520
|
+
// app clones it when writing later git/memory-mode metadata. Appending alone leaves a stale
|
|
521
|
+
// line-1 provider that the clone would re-append, so for a length-preserving provider change we
|
|
522
|
+
// also patch line 1 in place (no inode swap, no truncate). Best-effort: when it can't be done
|
|
523
|
+
// safely (e.g. a length-growing change), the trailing append below is still correct for the
|
|
524
|
+
// SQLite replay path.
|
|
525
|
+
if (patch.provider !== undefined && options.patchFirstLine !== false) {
|
|
526
|
+
try { patchFirstLineProviderInPlace(path, expectedId, patch.provider); } catch { /* best-effort line-1 patch */ }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Refresh the line timestamp so the appended record reads as the newest metadata.
|
|
530
|
+
record.timestamp = new Date().toISOString();
|
|
531
|
+
appendRolloutLine(path, JSON.stringify(record));
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function toNativeRestoreTarget(entry: BackupEntry): NativeRestoreTarget {
|
|
536
|
+
if (entry.modelProvider !== "opencodex") {
|
|
537
|
+
return {
|
|
538
|
+
modelProvider: entry.modelProvider,
|
|
539
|
+
source: entry.source,
|
|
540
|
+
hasUserEvent: entry.hasUserEvent,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
modelProvider: "openai",
|
|
545
|
+
source: entry.source === "exec" ? "cli" : entry.source,
|
|
546
|
+
hasUserEvent: 1,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: number } {
|
|
551
|
+
const rows = db
|
|
552
|
+
.query<ThreadRow, []>(`
|
|
553
|
+
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
554
|
+
FROM threads
|
|
555
|
+
WHERE model_provider = 'opencodex'
|
|
556
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
557
|
+
`)
|
|
558
|
+
.all();
|
|
559
|
+
|
|
560
|
+
let files = 0;
|
|
561
|
+
for (const row of rows) {
|
|
562
|
+
try {
|
|
563
|
+
if (updateSessionMeta(row.rollout_path, row.id, {
|
|
564
|
+
provider: "openai",
|
|
565
|
+
source: row.source === "exec" ? "cli" : undefined,
|
|
566
|
+
})) files++;
|
|
567
|
+
} catch {
|
|
568
|
+
/* native restore should continue even if an old rollout is missing */
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const restore = db.transaction(() => {
|
|
573
|
+
const update = db.query(`
|
|
574
|
+
UPDATE threads
|
|
575
|
+
SET model_provider = 'openai',
|
|
576
|
+
source = CASE WHEN source = 'exec' THEN 'cli' ELSE source END,
|
|
577
|
+
has_user_event = 1
|
|
578
|
+
WHERE id = ?
|
|
579
|
+
`);
|
|
580
|
+
for (const row of rows) update.run(row.id);
|
|
581
|
+
});
|
|
582
|
+
restore();
|
|
583
|
+
return { rows: rows.length, files };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Move Desktop tasks that still carry the adopted codex-lb provider identity onto the built-in
|
|
588
|
+
* openai identity used by loopback injection. The original rows are retained in the ordinary
|
|
589
|
+
* history manifest so restore can put codex-lb back exactly. Unlike the legacy opencodex eject,
|
|
590
|
+
* keep line 1 unchanged: it already contains the native codex-lb identity that restore needs,
|
|
591
|
+
* while the appended openai metadata is the active last-writer-wins value during mediation.
|
|
592
|
+
*/
|
|
593
|
+
export function migrateCodexLbHistoryToOpenai(
|
|
594
|
+
stateDbPath = STATE_DB_PATH,
|
|
595
|
+
backupPath = HISTORY_BACKUP_PATH,
|
|
596
|
+
): CodexHistorySyncResult {
|
|
597
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
598
|
+
const retried = withHistoryRetryResult(() => {
|
|
599
|
+
const db = openStateDb(stateDbPath);
|
|
600
|
+
try {
|
|
601
|
+
const manifest = readBackup(backupPath, stateDbPath);
|
|
602
|
+
const hasModelColumn = threadsHaveModelColumn(db);
|
|
603
|
+
const backedTaskIds = new Set(Object.values(manifest.entries)
|
|
604
|
+
.filter(entry => entry.modelProvider === "codex-lb")
|
|
605
|
+
.map(entry => entry.id));
|
|
606
|
+
const rows = db
|
|
607
|
+
.query<ThreadRowWithModel, []>(`
|
|
608
|
+
SELECT id, rollout_path, model_provider, source, has_user_event,
|
|
609
|
+
${hasModelColumn ? "model" : "NULL"} AS model
|
|
610
|
+
FROM threads
|
|
611
|
+
WHERE model_provider IN ('codex-lb', 'openai')
|
|
612
|
+
`)
|
|
613
|
+
.all()
|
|
614
|
+
.filter(row => row.model_provider === "codex-lb" || backedTaskIds.has(row.id));
|
|
615
|
+
if (rows.length === 0) return { rows: 0, files: 0 };
|
|
616
|
+
|
|
617
|
+
const pendingRows = rows.filter(row => row.model_provider === "codex-lb");
|
|
618
|
+
if (pendingRows.length > 0) {
|
|
619
|
+
for (const row of pendingRows) rememberOriginal(manifest, row);
|
|
620
|
+
writeBackup(backupPath, manifest, stateDbPath);
|
|
621
|
+
}
|
|
622
|
+
// Backfill manifests written by versions that did not retain the selected model. Once this
|
|
623
|
+
// observation is persisted, every retry uses a compare-and-swap and cannot overwrite a newer
|
|
624
|
+
// Desktop/Android selection made between the database read and migration write.
|
|
625
|
+
let backfilledOriginalModel = false;
|
|
626
|
+
for (const row of rows) {
|
|
627
|
+
const entry = manifest.entries[row.id];
|
|
628
|
+
if (entry?.modelProvider !== "codex-lb"
|
|
629
|
+
|| entry.codexLbModelQualified === true
|
|
630
|
+
|| Object.hasOwn(entry, "codexLbOriginalModel")) continue;
|
|
631
|
+
entry.codexLbOriginalModel = row.model;
|
|
632
|
+
backfilledOriginalModel = true;
|
|
633
|
+
}
|
|
634
|
+
if (backfilledOriginalModel) writeBackup(backupPath, manifest, stateDbPath);
|
|
635
|
+
const modelNormalizationRows = rows.filter(row => {
|
|
636
|
+
const entry = manifest.entries[row.id];
|
|
637
|
+
return entry?.modelProvider === "codex-lb" && entry.codexLbModelQualified !== true;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
let files = 0;
|
|
641
|
+
for (const row of rows) {
|
|
642
|
+
try {
|
|
643
|
+
if (updateSessionMeta(
|
|
644
|
+
row.rollout_path,
|
|
645
|
+
row.id,
|
|
646
|
+
{ provider: "openai" },
|
|
647
|
+
{ patchFirstLine: false, findLatestMatchingId: true },
|
|
648
|
+
)) files++;
|
|
649
|
+
} catch {
|
|
650
|
+
/* keep the database migration moving if an old rollout is missing or malformed */
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (pendingRows.length > 0 || modelNormalizationRows.length > 0) {
|
|
655
|
+
const update = db.transaction(() => {
|
|
656
|
+
const providerStatement = db.query(`
|
|
657
|
+
UPDATE threads
|
|
658
|
+
SET model_provider = 'openai'
|
|
659
|
+
WHERE id = ?
|
|
660
|
+
AND model_provider = 'codex-lb'
|
|
661
|
+
`);
|
|
662
|
+
for (const row of pendingRows) providerStatement.run(row.id);
|
|
663
|
+
if (hasModelColumn) {
|
|
664
|
+
const modelStatement = db.query(`
|
|
665
|
+
UPDATE threads
|
|
666
|
+
SET model = ?
|
|
667
|
+
WHERE id = ?
|
|
668
|
+
AND model IS ?
|
|
669
|
+
`);
|
|
670
|
+
for (const row of modelNormalizationRows) {
|
|
671
|
+
const entry = manifest.entries[row.id];
|
|
672
|
+
const original = entry?.codexLbOriginalModel ?? row.model;
|
|
673
|
+
const qualified = qualifyCodexLbModel(original);
|
|
674
|
+
if (row.model === original) modelStatement.run(qualified, row.id, original);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
update();
|
|
679
|
+
}
|
|
680
|
+
if (modelNormalizationRows.length > 0) {
|
|
681
|
+
for (const row of modelNormalizationRows) {
|
|
682
|
+
const entry = manifest.entries[row.id];
|
|
683
|
+
if (entry) entry.codexLbModelQualified = true;
|
|
684
|
+
}
|
|
685
|
+
// Persist the idempotency marker only after the database transaction. If the process
|
|
686
|
+
// exits between them, the next pass safely observes an already-qualified model slug.
|
|
687
|
+
writeBackup(backupPath, manifest, stateDbPath);
|
|
688
|
+
}
|
|
689
|
+
const changedIds = new Set([
|
|
690
|
+
...pendingRows.map(row => row.id),
|
|
691
|
+
...modelNormalizationRows.map(row => row.id),
|
|
692
|
+
]);
|
|
693
|
+
return { rows: changedIds.size, files };
|
|
694
|
+
} finally {
|
|
695
|
+
db.close();
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
return retried.ok
|
|
699
|
+
? retried.value
|
|
700
|
+
: { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function classifyRecoverableHistoryError(error: unknown): CodexHistoryFailureReason | null {
|
|
704
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
705
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
706
|
+
if (code === "SQLITE_BUSY"
|
|
707
|
+
|| code === "SQLITE_LOCKED"
|
|
708
|
+
|| code === "EBUSY"
|
|
709
|
+
|| message.includes("database is locked")
|
|
710
|
+
|| message.includes("database is busy")
|
|
711
|
+
|| message.includes("resource busy")) return "busy";
|
|
712
|
+
if (code === "EPERM"
|
|
713
|
+
|| code === "EACCES"
|
|
714
|
+
|| message.includes("operation not permitted")
|
|
715
|
+
|| message.includes("permission denied")) return "permission";
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function isRecoverableHistoryError(error: unknown): boolean {
|
|
720
|
+
return classifyRecoverableHistoryError(error) !== null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const HISTORY_RETRY_DELAY_MS = 500;
|
|
724
|
+
const HISTORY_RETRY_ATTEMPTS = 2;
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Run a history mutation with one retry across recoverable lock/busy errors (the app's own
|
|
728
|
+
* connection holds `state_5.sqlite` in WAL with a 5 s busy_timeout; a transient writer
|
|
729
|
+
* usually clears within that window). Returns null when both attempts hit a recoverable
|
|
730
|
+
* error — callers surface that as `failed: true` instead of a silent no-op. Hard errors
|
|
731
|
+
* (corruption, programming bugs) still throw.
|
|
732
|
+
*/
|
|
733
|
+
function withHistoryRetryResult<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}):
|
|
734
|
+
| { ok: true; value: T }
|
|
735
|
+
| { ok: false; reason: CodexHistoryFailureReason } {
|
|
736
|
+
const sleepFn = io.sleepFn ?? Bun.sleepSync;
|
|
737
|
+
const attempts = Math.max(1, io.attempts ?? HISTORY_RETRY_ATTEMPTS);
|
|
738
|
+
const delayMs = io.delayMs ?? HISTORY_RETRY_DELAY_MS;
|
|
739
|
+
for (let attempt = 0; ; attempt++) {
|
|
740
|
+
try {
|
|
741
|
+
return { ok: true, value: fn() };
|
|
742
|
+
} catch (error) {
|
|
743
|
+
const reason = classifyRecoverableHistoryError(error);
|
|
744
|
+
if (!reason) throw error;
|
|
745
|
+
if (attempt >= attempts - 1) return { ok: false, reason };
|
|
746
|
+
try { sleepFn(delayMs); } catch { /* sleep is best-effort */ }
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export function withHistoryRetry<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}): T | null {
|
|
752
|
+
const result = withHistoryRetryResult(fn, io);
|
|
753
|
+
return result.ok ? result.value : null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* True when a READONLY probe proves the openai-direction restore would be a no-op:
|
|
758
|
+
* zero threads still tagged opencodex AND an empty backup manifest. Used to skip the
|
|
759
|
+
* write-open entirely in the Design B steady state — on Windows the Codex app holds
|
|
760
|
+
* `state_5.sqlite` (WAL, busy_timeout 5s), so an unnecessary write open can stall for
|
|
761
|
+
* seconds and surface a false lock warning, while WAL always admits readers. A failed
|
|
762
|
+
* probe (locked even for readers / schema drift) returns false so callers fall through
|
|
763
|
+
* to the write attempt and keep today's behavior for genuinely unknown state.
|
|
764
|
+
*/
|
|
765
|
+
function openaiRestoreIsNoop(stateDbPath: string, backupPath: string): boolean {
|
|
766
|
+
const pending = countPendingOpencodexHistory(stateDbPath, backupPath);
|
|
767
|
+
return !pending.failed && pending.pendingRows === 0 && pending.backupEntries === 0;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function syncCodexHistoryProvider(
|
|
771
|
+
provider: CodexHistoryProvider,
|
|
772
|
+
stateDbPath = STATE_DB_PATH,
|
|
773
|
+
backupPath = HISTORY_BACKUP_PATH,
|
|
774
|
+
opts: { skipWhenProvablyNoop?: boolean } = {},
|
|
775
|
+
): CodexHistorySyncResult {
|
|
776
|
+
// Opt-in steady-state gate (Design B loopback callers only): default semantics of
|
|
777
|
+
// this exported API are unchanged — legacy stop/restore paths never pass the flag.
|
|
778
|
+
if (opts.skipWhenProvablyNoop && provider === "openai" && existsSync(stateDbPath)
|
|
779
|
+
&& openaiRestoreIsNoop(stateDbPath, backupPath)) {
|
|
780
|
+
return { rows: 0, files: 0 };
|
|
781
|
+
}
|
|
782
|
+
const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe(provider, stateDbPath, backupPath));
|
|
783
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbPath: string, backupPath: string): CodexHistorySyncResult {
|
|
787
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
788
|
+
if (provider === "openai") return restoreCodexHistoryProvider(stateDbPath, backupPath);
|
|
789
|
+
|
|
790
|
+
const db = openStateDb(stateDbPath);
|
|
791
|
+
try {
|
|
792
|
+
const placeholders = RESUMABLE_SOURCES.map(() => "?").join(",");
|
|
793
|
+
const openaiRows = db
|
|
794
|
+
.query<ThreadRow, string[]>(`
|
|
795
|
+
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
796
|
+
FROM threads
|
|
797
|
+
WHERE model_provider = 'openai'
|
|
798
|
+
AND source IN (${placeholders})
|
|
799
|
+
`)
|
|
800
|
+
.all(...RESUMABLE_SOURCES);
|
|
801
|
+
const execRows = db
|
|
802
|
+
.query<ThreadRow, []>(`
|
|
803
|
+
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
804
|
+
FROM threads
|
|
805
|
+
WHERE model_provider = 'opencodex'
|
|
806
|
+
AND source = 'exec'
|
|
807
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
808
|
+
`)
|
|
809
|
+
.all();
|
|
810
|
+
|
|
811
|
+
const manifest = readBackup(backupPath, stateDbPath);
|
|
812
|
+
for (const row of [...openaiRows, ...execRows]) rememberOriginal(manifest, row);
|
|
813
|
+
writeBackup(backupPath, manifest, stateDbPath);
|
|
814
|
+
|
|
815
|
+
let files = 0;
|
|
816
|
+
for (const row of openaiRows) {
|
|
817
|
+
try {
|
|
818
|
+
if (updateSessionMeta(row.rollout_path, row.id, { provider: "opencodex" })) files++;
|
|
819
|
+
} catch {
|
|
820
|
+
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
for (const row of execRows) {
|
|
824
|
+
try {
|
|
825
|
+
if (updateSessionMeta(row.rollout_path, row.id, { source: "cli" })) files++;
|
|
826
|
+
} catch {
|
|
827
|
+
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const update = db.transaction(() => {
|
|
832
|
+
const markUserEvent = db.query(`
|
|
833
|
+
UPDATE threads
|
|
834
|
+
SET has_user_event = 1
|
|
835
|
+
WHERE id = ?
|
|
836
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
837
|
+
`);
|
|
838
|
+
for (const row of [...openaiRows, ...execRows]) markUserEvent.run(row.id);
|
|
839
|
+
db.query(`
|
|
840
|
+
UPDATE threads
|
|
841
|
+
SET model_provider = 'opencodex'
|
|
842
|
+
WHERE model_provider = 'openai'
|
|
843
|
+
AND source IN (${placeholders})
|
|
844
|
+
`).run(...RESUMABLE_SOURCES);
|
|
845
|
+
db.query(`
|
|
846
|
+
UPDATE threads
|
|
847
|
+
SET source = 'cli'
|
|
848
|
+
WHERE model_provider = 'opencodex'
|
|
849
|
+
AND source = 'exec'
|
|
850
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
851
|
+
`).run();
|
|
852
|
+
});
|
|
853
|
+
update();
|
|
854
|
+
|
|
855
|
+
return { rows: openaiRows.length + execRows.length, files };
|
|
856
|
+
} finally {
|
|
857
|
+
db.close();
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function restoreCodexHistoryProvider(
|
|
862
|
+
stateDbPath: string,
|
|
863
|
+
backupPath: string,
|
|
864
|
+
options: { preserveOriginalProviders?: ReadonlySet<string> } = {},
|
|
865
|
+
): CodexHistorySyncResult {
|
|
866
|
+
const manifest = readBackup(backupPath, stateDbPath);
|
|
867
|
+
const preservedEntries: Record<string, BackupEntry> = {};
|
|
868
|
+
const entries: BackupEntry[] = [];
|
|
869
|
+
for (const entry of Object.values(manifest.entries)) {
|
|
870
|
+
if (options.preserveOriginalProviders?.has(entry.modelProvider)) {
|
|
871
|
+
preservedEntries[entry.id] = entry;
|
|
872
|
+
} else {
|
|
873
|
+
entries.push(entry);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const db = openStateDb(stateDbPath);
|
|
878
|
+
try {
|
|
879
|
+
if (entries.length === 0) {
|
|
880
|
+
const ejected = ejectRemainingOpencodexHistory(db);
|
|
881
|
+
return ejected.rows > 0 ? { rows: 0, files: ejected.files, ejectedRows: ejected.rows } : { rows: 0, files: 0 };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
let files = 0;
|
|
885
|
+
for (const entry of entries) {
|
|
886
|
+
const target = toNativeRestoreTarget(entry);
|
|
887
|
+
try {
|
|
888
|
+
if (updateSessionMeta(entry.rolloutPath, entry.id, { provider: target.modelProvider, source: target.source })) files++;
|
|
889
|
+
} catch {
|
|
890
|
+
/* best-effort; keep DB restore moving even if one rollout disappeared */
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const restore = db.transaction(() => {
|
|
895
|
+
if (threadsHaveModelColumn(db)) {
|
|
896
|
+
const update = db.query(`
|
|
897
|
+
UPDATE threads
|
|
898
|
+
SET model_provider = ?,
|
|
899
|
+
source = ?,
|
|
900
|
+
has_user_event = ?,
|
|
901
|
+
model = CASE
|
|
902
|
+
WHEN ? = 1 AND model LIKE 'codex-lb/%' THEN substr(model, length('codex-lb/') + 1)
|
|
903
|
+
ELSE model
|
|
904
|
+
END
|
|
905
|
+
WHERE id = ?
|
|
906
|
+
`);
|
|
907
|
+
for (const entry of entries) {
|
|
908
|
+
const target = toNativeRestoreTarget(entry);
|
|
909
|
+
update.run(
|
|
910
|
+
target.modelProvider,
|
|
911
|
+
target.source,
|
|
912
|
+
target.hasUserEvent,
|
|
913
|
+
entry.codexLbModelQualified === true ? 1 : 0,
|
|
914
|
+
entry.id,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
} else {
|
|
918
|
+
const update = db.query(`
|
|
919
|
+
UPDATE threads
|
|
920
|
+
SET model_provider = ?,
|
|
921
|
+
source = ?,
|
|
922
|
+
has_user_event = ?
|
|
923
|
+
WHERE id = ?
|
|
924
|
+
`);
|
|
925
|
+
for (const entry of entries) {
|
|
926
|
+
const target = toNativeRestoreTarget(entry);
|
|
927
|
+
update.run(target.modelProvider, target.source, target.hasUserEvent, entry.id);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
restore();
|
|
932
|
+
writeBackup(backupPath, { version: 1, stateDbPath, entries: preservedEntries }, stateDbPath);
|
|
933
|
+
const ejected = ejectRemainingOpencodexHistory(db);
|
|
934
|
+
return ejected.rows > 0
|
|
935
|
+
? { rows: entries.length, files: files + ejected.files, ejectedRows: ejected.rows }
|
|
936
|
+
: { rows: entries.length, files };
|
|
937
|
+
} finally {
|
|
938
|
+
db.close();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Restore legacy Remodex history while mediation stays active, without consuming the native
|
|
944
|
+
* codex-lb identities retained for a later full restore. This is deliberately separate from the
|
|
945
|
+
* public native restore: stop/eject must still restore every provider exactly.
|
|
946
|
+
*/
|
|
947
|
+
export function restoreManagedOpenaiHistory(
|
|
948
|
+
stateDbPath = STATE_DB_PATH,
|
|
949
|
+
backupPath = HISTORY_BACKUP_PATH,
|
|
950
|
+
): CodexHistorySyncResult {
|
|
951
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
952
|
+
const retried = withHistoryRetryResult(() => restoreCodexHistoryProvider(
|
|
953
|
+
stateDbPath,
|
|
954
|
+
backupPath,
|
|
955
|
+
{ preserveOriginalProviders: new Set(["codex-lb"]) },
|
|
956
|
+
));
|
|
957
|
+
return retried.ok
|
|
958
|
+
? retried.value
|
|
959
|
+
: { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function restoreLegacyOpenaiHistory(stateDbPath = STATE_DB_PATH): CodexHistorySyncResult {
|
|
963
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
964
|
+
const retried = withHistoryRetryResult(() => {
|
|
965
|
+
const db = openStateDb(stateDbPath);
|
|
966
|
+
try {
|
|
967
|
+
return ejectRemainingOpencodexHistory(db);
|
|
968
|
+
} finally {
|
|
969
|
+
db.close();
|
|
970
|
+
}
|
|
971
|
+
});
|
|
972
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* One-time Design-B migration: restore backed-up originals, then eject any remaining
|
|
977
|
+
* opencodex-tagged threads to openai. Thin wrapper over the restore path with a
|
|
978
|
+
* configurable retry budget — the daemon migration guardian uses `{ attempts: 1 }`
|
|
979
|
+
* per tick so a locked DB never stalls the event loop beyond one sqlite busy wait.
|
|
980
|
+
*/
|
|
981
|
+
export function migrateHistoryToOpenai(
|
|
982
|
+
stateDbPath = STATE_DB_PATH,
|
|
983
|
+
backupPath = HISTORY_BACKUP_PATH,
|
|
984
|
+
opts: { attempts?: number; delayMs?: number; sleepFn?: (ms: number) => void } = {},
|
|
985
|
+
): CodexHistorySyncResult {
|
|
986
|
+
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
987
|
+
// Steady-state gate: this migration is Design-B-specific (inject + guardian callers),
|
|
988
|
+
// and after the one-time migration every start would otherwise write-open the DB for
|
|
989
|
+
// nothing. A missing DB with a leftover backup manifest does NOT satisfy the gate
|
|
990
|
+
// (backupEntries > 0), so the guardian's fresh-reinstall re-count protection holds.
|
|
991
|
+
if (openaiRestoreIsNoop(stateDbPath, backupPath)) return { rows: 0, files: 0 };
|
|
992
|
+
const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe("openai", stateDbPath, backupPath), opts);
|
|
993
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export interface PendingHistoryCount {
|
|
997
|
+
/** Threads still tagged opencodex that the eject path WOULD move (mirrors its WHERE). */
|
|
998
|
+
pendingRows: number;
|
|
999
|
+
/** Entries still recorded in the backup manifest (restore targets). */
|
|
1000
|
+
backupEntries: number;
|
|
1001
|
+
/** Set when the DB could not be opened/read (locked); counts are then unknown, not zero. */
|
|
1002
|
+
failed?: true;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Read-only migration progress probe for the guardian and `rmx doctor`. Opens sqlite
|
|
1007
|
+
* readonly with a SHORT busy timeout so a locked DB cannot stall a daemon tick. The
|
|
1008
|
+
* pending predicate mirrors ejectRemainingOpencodexHistory exactly — rows eject ignores
|
|
1009
|
+
* (empty first_user_message) are not counted, so 0 really means "migration done".
|
|
1010
|
+
*/
|
|
1011
|
+
export function countPendingOpencodexHistory(stateDbPath = STATE_DB_PATH, backupPath = HISTORY_BACKUP_PATH): PendingHistoryCount {
|
|
1012
|
+
let backupEntries = 0;
|
|
1013
|
+
try {
|
|
1014
|
+
const manifest = readBackup(backupPath, stateDbPath);
|
|
1015
|
+
backupEntries = Object.keys(manifest.entries).length;
|
|
1016
|
+
} catch { /* unreadable manifest counts as 0 — restore treats it the same way */ }
|
|
1017
|
+
|
|
1018
|
+
if (!existsSync(stateDbPath)) return { pendingRows: 0, backupEntries };
|
|
1019
|
+
try {
|
|
1020
|
+
const db = new Database(stateDbPath, { readonly: true });
|
|
1021
|
+
try {
|
|
1022
|
+
db.exec("PRAGMA busy_timeout = 100");
|
|
1023
|
+
const row = db.query<{ n: number }, []>(`
|
|
1024
|
+
SELECT count(*) AS n
|
|
1025
|
+
FROM threads
|
|
1026
|
+
WHERE model_provider = 'opencodex'
|
|
1027
|
+
AND trim(coalesce(first_user_message, '')) != ''
|
|
1028
|
+
`).get();
|
|
1029
|
+
return { pendingRows: row?.n ?? 0, backupEntries };
|
|
1030
|
+
} finally {
|
|
1031
|
+
db.close();
|
|
1032
|
+
}
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
if (isRecoverableHistoryError(error)) return { pendingRows: 0, backupEntries, failed: true };
|
|
1035
|
+
// Schema drift (e.g. a future codex renames the table) is a "cannot know" too, not a crash.
|
|
1036
|
+
return { pendingRows: 0, backupEntries, failed: true };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Read-only progress for the managed OpenAI migration. Preserved codex-lb manifest entries are
|
|
1042
|
+
* restore state, not pending migration work; newly discovered codex-lb database rows are work.
|
|
1043
|
+
*/
|
|
1044
|
+
export function countPendingManagedOpenaiHistory(
|
|
1045
|
+
stateDbPath = STATE_DB_PATH,
|
|
1046
|
+
backupPath = HISTORY_BACKUP_PATH,
|
|
1047
|
+
): PendingHistoryCount {
|
|
1048
|
+
let backupEntries = 0;
|
|
1049
|
+
try {
|
|
1050
|
+
const manifest = readBackup(backupPath, stateDbPath);
|
|
1051
|
+
backupEntries = Object.values(manifest.entries)
|
|
1052
|
+
.filter(entry => entry.modelProvider !== "codex-lb" || entry.codexLbModelQualified !== true)
|
|
1053
|
+
.length;
|
|
1054
|
+
} catch { /* unreadable manifest counts as 0 — migration treats it the same way */ }
|
|
1055
|
+
|
|
1056
|
+
if (!existsSync(stateDbPath)) return { pendingRows: 0, backupEntries };
|
|
1057
|
+
try {
|
|
1058
|
+
const db = new Database(stateDbPath, { readonly: true });
|
|
1059
|
+
try {
|
|
1060
|
+
db.exec("PRAGMA busy_timeout = 100");
|
|
1061
|
+
const row = db.query<{ n: number }, []>(`
|
|
1062
|
+
SELECT count(*) AS n
|
|
1063
|
+
FROM threads
|
|
1064
|
+
WHERE model_provider = 'codex-lb'
|
|
1065
|
+
OR (model_provider = 'opencodex'
|
|
1066
|
+
AND trim(coalesce(first_user_message, '')) != '')
|
|
1067
|
+
`).get();
|
|
1068
|
+
return { pendingRows: row?.n ?? 0, backupEntries };
|
|
1069
|
+
} finally {
|
|
1070
|
+
db.close();
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
return { pendingRows: 0, backupEntries, failed: true };
|
|
1074
|
+
}
|
|
1075
|
+
}
|