@iislee/opencodex 2.11.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +117 -22
- package/bin/ocx.mjs +245 -88
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/commandcode-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +21 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +475 -62
- package/src/adapters/base.ts +82 -7
- package/src/adapters/client-fingerprint.ts +18 -12
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
- package/src/adapters/command-code.ts +637 -0
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +308 -0
- package/src/adapters/cursor/cursor-errors.ts +144 -6
- package/src/adapters/cursor/discovery.ts +122 -14
- package/src/adapters/cursor/effort-map.ts +106 -4
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/h2-pool.ts +123 -0
- package/src/adapters/cursor/http1-bidi.ts +361 -0
- package/src/adapters/cursor/images.ts +704 -0
- package/src/adapters/cursor/live-models.ts +180 -59
- package/src/adapters/cursor/live-transport.ts +623 -170
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +10 -7
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -3
- package/src/adapters/cursor/native-exec.ts +121 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +447 -75
- package/src/adapters/cursor/request-builder.ts +271 -35
- package/src/adapters/cursor/tool-definitions.ts +252 -12
- package/src/adapters/cursor/tool-result-normalize.ts +115 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +48 -1
- package/src/adapters/cursor.ts +336 -39
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +713 -41
- package/src/adapters/google-antigravity-wire.ts +48 -10
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google-http.ts +38 -10
- package/src/adapters/google-tool-schema.ts +80 -15
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +742 -86
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +18 -2
- package/src/adapters/kiro-tools.ts +65 -17
- package/src/adapters/kiro.ts +270 -73
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1377 -344
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +1164 -64
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +105 -21
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +186 -0
- package/src/bridge.ts +512 -92
- package/src/chat/inbound.ts +57 -20
- package/src/chat/outbound.ts +97 -33
- package/src/claude/agents-inject.ts +40 -10
- package/src/claude/context-windows.ts +37 -14
- package/src/claude/desktop-3p.ts +243 -9
- package/src/claude/gateway-cache.ts +41 -4
- package/src/claude/inbound.ts +72 -3
- package/src/claude/model-info.ts +38 -15
- package/src/claude/outbound.ts +88 -21
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +150 -19
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +648 -28
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +97 -19
- package/src/cli/agent.ts +53 -2
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +60 -15
- package/src/cli/claude.ts +129 -35
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/codex-shim-autorestore.ts +2 -0
- package/src/cli/codex-shim-readiness.ts +76 -0
- package/src/cli/combo.ts +8 -0
- package/src/cli/config-command.ts +74 -10
- package/src/cli/dispatch.ts +821 -0
- package/src/cli/doctor.ts +440 -56
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +34 -274
- package/src/cli/index.ts +417 -528
- package/src/cli/init.ts +5 -17
- package/src/cli/inspect.ts +230 -0
- package/src/cli/integrations.ts +120 -2
- package/src/cli/lab.ts +607 -0
- package/src/cli/launcher-context.ts +77 -0
- package/src/cli/minimax.ts +497 -0
- package/src/cli/models-runtime.ts +130 -2
- package/src/cli/models.ts +118 -16
- package/src/cli/observe.ts +123 -11
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +34 -3
- package/src/cli/provider.ts +25 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +476 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +57 -10
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +51 -4
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +137 -18
- package/src/cli/version-skew.ts +46 -0
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1442 -23
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +35 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +84 -0
- package/src/codex/account-store.ts +29 -2
- package/src/codex/account-usability.ts +25 -2
- package/src/codex/admission.ts +256 -0
- package/src/codex/affinity-debug.ts +162 -0
- package/src/codex/app-server-processes.ts +570 -107
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +893 -246
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +348 -32
- package/src/codex/autostart-health.ts +24 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +86 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +138 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +555 -45
- package/src/codex/catalog/native-models.ts +75 -0
- package/src/codex/catalog/parsing.ts +269 -30
- package/src/codex/catalog/provider-fetch.ts +1626 -144
- package/src/codex/catalog/sync.ts +1576 -187
- package/src/codex/catalog-admission.ts +199 -0
- package/src/codex/catalog-refresh-status.ts +105 -0
- package/src/codex/catalog-write-serialization.ts +242 -0
- package/src/codex/catalog.ts +6 -3
- package/src/codex/codex-write-lock.ts +386 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +676 -0
- package/src/codex/coordinator-doctor.ts +332 -0
- package/src/codex/custom-model-catalog-migration.ts +176 -0
- package/src/codex/desired-state.ts +230 -0
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +436 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +30 -24
- package/src/codex/history-provider.ts +1016 -235
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +223 -0
- package/src/codex/inject-coordination.ts +373 -0
- package/src/codex/inject.ts +1114 -152
- package/src/codex/injected-marker.ts +37 -3
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +81 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +524 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +88 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/main-account.ts +29 -1
- package/src/codex/management-convergence.ts +167 -0
- package/src/codex/model-cache.ts +56 -10
- package/src/codex/model-entitlements.ts +360 -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 +178 -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 +622 -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 +675 -0
- package/src/codex/paths.ts +80 -1
- package/src/codex/plan-from-token.ts +140 -0
- package/src/codex/plan.ts +40 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/project-config-warnings.ts +20 -6
- package/src/codex/prompt-journal.ts +352 -0
- package/src/codex/prompt-layers.ts +1313 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +264 -21
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +514 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1065 -31
- package/src/codex/subagent-model-fallback.ts +437 -43
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +720 -0
- package/src/codex/upstream-host-health.ts +368 -0
- package/src/codex/user-identity.ts +557 -0
- package/src/codex/warmup.ts +187 -81
- package/src/codex/write-coordination.ts +114 -0
- package/src/combos/failover.ts +47 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +91 -9
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-name.ts +24 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +1713 -709
- package/src/generated/compatibility-version.json +3324 -0
- package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
- package/src/grok/inject.ts +16 -5
- package/src/grok/inspect.ts +45 -0
- package/src/grok/sync.ts +2 -2
- package/src/images/loop.ts +157 -33
- package/src/images/plan.ts +24 -13
- package/src/integrations/config-io.ts +269 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/mutation-flight.ts +71 -0
- package/src/integrations/native/ownership-preflight.ts +202 -0
- package/src/integrations/omp-yaml-source.ts +358 -0
- package/src/integrations/owned-refresh.ts +74 -0
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +155 -0
- package/src/integrations/registry.ts +166 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +433 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +715 -0
- package/src/lab/artifacts/sanitize.ts +586 -0
- package/src/lab/artifacts/secure-fs.ts +475 -0
- package/src/lab/artifacts/store.ts +310 -0
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +499 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/assertion.ts +325 -0
- package/src/lab/conformance/digest.ts +22 -0
- package/src/lab/conformance/executor.ts +741 -0
- package/src/lab/conformance/fixture-provider.ts +27 -0
- package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
- package/src/lab/conformance/harness-budget.ts +47 -0
- package/src/lab/conformance/index.ts +5 -0
- package/src/lab/conformance/jcs.ts +64 -0
- package/src/lab/conformance/json-pointer.ts +39 -0
- package/src/lab/conformance/manifest.ts +180 -0
- package/src/lab/conformance/mcp-stub.ts +179 -0
- package/src/lab/conformance/negative-controls.ts +164 -0
- package/src/lab/conformance/observation.ts +355 -0
- package/src/lab/conformance/runner.ts +68 -0
- package/src/lab/conformance/sse-normalize.ts +59 -0
- package/src/lab/conformance/suite-manifest.ts +78 -0
- package/src/lab/conformance/types.ts +214 -0
- package/src/lab/constants.ts +126 -0
- package/src/lab/digest.ts +64 -0
- package/src/lab/events/errors.ts +9 -0
- package/src/lab/events/limits.ts +117 -0
- package/src/lab/events/types.ts +229 -0
- package/src/lab/events/validate.ts +781 -0
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +489 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +54 -0
- package/src/lab/ledger/artifact-refs.ts +127 -0
- package/src/lab/ledger/invalidation.ts +136 -0
- package/src/lab/ledger/purge.ts +310 -0
- package/src/lab/ledger/store.ts +532 -0
- package/src/lab/live/credential-lease.ts +53 -0
- package/src/lab/live/destination.ts +155 -0
- package/src/lab/live/executor.ts +336 -0
- package/src/lab/live/inert-tools.ts +56 -0
- package/src/lab/live/manifest.ts +85 -0
- package/src/lab/live/mcp-loopback.ts +57 -0
- package/src/lab/live/runner.ts +19 -0
- package/src/lab/live/sandbox.ts +61 -0
- package/src/lab/live/suite-manifest.ts +41 -0
- package/src/lab/live/transport.ts +118 -0
- package/src/lab/live/types.ts +197 -0
- package/src/lab/observe/from-conformance.ts +301 -0
- package/src/lab/observe/from-live.ts +117 -0
- package/src/lab/paths.ts +153 -0
- package/src/lab/projection/rebuild.ts +495 -0
- package/src/lab/projection/schema.ts +135 -0
- package/src/lab/projection/verdicts.ts +474 -0
- package/src/lab/projection/verification.ts +412 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +243 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lab/query/catalog.ts +101 -0
- package/src/lab/query/connection.ts +107 -0
- package/src/lab/query/constants.ts +4 -0
- package/src/lab/query/cursor.ts +132 -0
- package/src/lab/query/dto-map.ts +277 -0
- package/src/lab/query/errors.ts +22 -0
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +45 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/query/queries.ts +444 -0
- package/src/lab/query/types.ts +266 -0
- package/src/lab/subject/behavior-fingerprint.ts +77 -0
- package/src/lab/subject/installation-salt.ts +112 -0
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +74 -0
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +167 -11
- package/src/lib/bun-runtime.ts +125 -12
- package/src/lib/bun-stream-caps.ts +13 -9
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/config-ownership.ts +26 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +80 -5
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-activation.ts +223 -0
- package/src/lib/lab-live-execution-authority.ts +13 -0
- package/src/lib/lab-live-host.ts +30 -0
- package/src/lib/lab-live-pinned-sender.ts +56 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/lab-passive-linker-registration.ts +26 -0
- package/src/lib/local-management-attestation.ts +51 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/local-provider-reload-contract.ts +100 -0
- package/src/lib/optional-shutdown-hooks.ts +57 -0
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +6 -2
- package/src/lib/provider-outbound.ts +49 -9
- package/src/lib/redact.ts +419 -3
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/shadow-call.ts +35 -4
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/state-store-registrations.ts +10 -2
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/tool-argument-integers.ts +253 -0
- package/src/lib/translator-budget.ts +44 -0
- package/src/lib/upstream-http-version.ts +57 -0
- package/src/lib/upstream-reachability.ts +95 -0
- package/src/lib/upstream-retry.ts +156 -3
- package/src/lib/windows-atomic-replace.ts +155 -0
- package/src/lib/windows-elevation.ts +70 -2
- package/src/lib/windows-secret-acl.ts +409 -69
- package/src/lib/windows-service-wrappers.ts +72 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +341 -0
- package/src/lib/winsw.ts +33 -5
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +30 -4
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +462 -71
- package/src/oauth/key-providers.ts +25 -0
- package/src/oauth/kimi.ts +25 -3
- package/src/oauth/kiro.ts +95 -6
- package/src/oauth/local-token-detect.ts +13 -2
- package/src/oauth/log.ts +3 -1
- package/src/oauth/login-cli.ts +88 -28
- package/src/oauth/nous.ts +798 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +133 -26
- package/src/oauth/token-guardian.ts +9 -3
- package/src/oauth/types.ts +15 -0
- package/src/pi/models.ts +2 -2
- package/src/providers/alibaba-region-migration.ts +1 -1
- package/src/providers/antigravity-models.ts +521 -31
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +176 -0
- package/src/providers/context-cap.ts +26 -8
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +281 -6
- package/src/providers/fastwire.ts +507 -0
- package/src/providers/free-directory.ts +10 -7
- package/src/providers/google-vertex-location.ts +14 -0
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/label.ts +35 -2
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/openai-sidecar.ts +72 -4
- package/src/providers/openai-tier-startup.ts +31 -2
- package/src/providers/openai-tiers.ts +119 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/opencode-zen-rate-limit.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +30 -0
- package/src/providers/quota.ts +1379 -40
- package/src/providers/registry.ts +1545 -112
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +94 -6
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/providers/xai-responses-opt-in.ts +15 -0
- package/src/providers/xai-transport.ts +32 -4
- package/src/reasoning-effort.ts +68 -3
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +384 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +435 -0
- package/src/responses/parser.ts +225 -38
- package/src/responses/provider-continuation.ts +98 -0
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +504 -0
- package/src/responses/schema.ts +12 -2
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +659 -31
- package/src/responses/thought-signature-replay.ts +364 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +405 -31
- package/src/routing/analytics.ts +379 -0
- package/src/routing/capability.ts +244 -0
- package/src/routing/compatibility/assemble.ts +73 -0
- package/src/routing/compatibility/behavior.ts +278 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/provider-slot.ts +56 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +495 -0
- package/src/routing/health.ts +412 -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 +547 -0
- package/src/routing/quota.ts +155 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +776 -0
- package/src/server/adapter-resolve.ts +2 -29
- package/src/server/auth-cors.ts +289 -48
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +154 -72
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +445 -0
- package/src/server/claude-messages.ts +160 -44
- package/src/server/direct-local-http.ts +347 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +94 -12
- package/src/server/index.ts +1021 -190
- package/src/server/lifecycle.ts +336 -14
- package/src/server/live.ts +136 -17
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/local-provider-reload-client.ts +137 -0
- package/src/server/management/agent-settings-routes.ts +433 -116
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +64 -25
- package/src/server/management/config-routes.ts +511 -51
- package/src/server/management/context.ts +89 -2
- package/src/server/management/integration-routes.ts +498 -0
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +563 -0
- package/src/server/management/logs-usage-routes.ts +127 -52
- package/src/server/management/model-routes.ts +461 -133
- package/src/server/management/model-rows.ts +167 -0
- package/src/server/management/native-integration-routes.ts +770 -0
- package/src/server/management/oauth-account-routes.ts +90 -8
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +836 -159
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +380 -0
- package/src/server/management/shared.ts +27 -11
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +289 -33
- package/src/server/management/system-routes.ts +77 -0
- package/src/server/management/usage-summary-cache.ts +9 -1
- package/src/server/management/vision-sidecar-options.ts +167 -0
- package/src/server/management/web-search-sidecar-options.ts +120 -0
- package/src/server/management-api.ts +126 -23
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +161 -10
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +221 -54
- package/src/server/relay.ts +479 -87
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +298 -31
- package/src/server/responses/agent-task-recovery-cache.ts +143 -0
- package/src/server/responses/agent-task-recovery.ts +465 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +239 -36
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +444 -72
- package/src/server/responses/core.ts +3579 -436
- package/src/server/responses/empty-completion-guard.ts +333 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +89 -99
- package/src/server/responses/input-admission.ts +187 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +343 -0
- package/src/server/responses/terminal-guard.ts +26 -5
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses/ws-upstream.ts +381 -0
- package/src/server/responses-custom-tool-repair.ts +363 -0
- package/src/server/responses-item-id-repair.ts +54 -6
- package/src/server/responses-json-events.ts +90 -0
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/responses-terminal-repair.ts +363 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +376 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +319 -0
- package/src/server/sse-payload-rewrite.ts +110 -22
- package/src/server/startup-action-control.ts +8 -1
- package/src/server/startup-health-cache.ts +19 -1
- package/src/server/system-env.ts +80 -9
- package/src/server/ws-bridge.ts +53 -40
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1153 -94
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/storage/scanner.ts +1 -1
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +83 -9
- package/src/tray/windows.ts +43 -16
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +911 -0
- package/src/types/provider.ts +614 -0
- package/src/types/request.ts +397 -0
- package/src/types/tools.ts +236 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +107 -1237
- package/src/update/index.ts +37 -23
- package/src/update/job.ts +445 -68
- package/src/update/notify.ts +12 -6
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/update/transactional-install.d.mts +22 -0
- package/src/update/transactional-install.mjs +266 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +287 -16
- package/src/usage/log.ts +666 -50
- package/src/usage/summary.ts +388 -17
- package/src/usage/user-cost-overlay-reconciler.ts +313 -0
- package/src/usage/user-cost-overlays.ts +314 -0
- package/src/vision/anthropic-describe.ts +10 -6
- package/src/vision/backends.ts +97 -0
- package/src/vision/describe.ts +27 -16
- package/src/vision/eligibility.ts +250 -0
- package/src/vision/index.ts +238 -24
- package/src/vision/reasoning.ts +55 -0
- package/src/vision/routed-describe.ts +175 -0
- package/src/vision/timeout-bounds.ts +9 -0
- package/src/web-search/anthropic-executor.ts +13 -7
- package/src/web-search/backends.ts +108 -0
- package/src/web-search/exa-executor.ts +119 -0
- package/src/web-search/executor.ts +21 -6
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +284 -54
- package/src/web-search/parse.ts +125 -30
- package/src/web-search/sources.ts +60 -0
- package/src/web-search/xai-executor.ts +219 -0
- package/gui/dist/assets/index-DTpMHS4F.js +0 -67
- package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, unlinkSync, writeSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { zstdDecompressSync } from "node:zlib";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveCodexStateDbPath } from "./paths";
|
|
7
7
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
8
|
+
import {
|
|
9
|
+
CODEX_HISTORY_RESUMABLE_SOURCES,
|
|
10
|
+
codexHistoryBackupId,
|
|
11
|
+
sameCodexHistoryPath,
|
|
12
|
+
validateCodexHistoryBackupManifest,
|
|
13
|
+
type CodexHistoryBackupEntry,
|
|
14
|
+
type CodexHistoryBackupManifest,
|
|
15
|
+
} from "./history-manifest";
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
|
|
@@ -13,14 +21,16 @@ import { atomicWriteFile, getConfigDir } from "../config";
|
|
|
13
21
|
*/
|
|
14
22
|
export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The manifest that shadows one state database.
|
|
26
|
+
*
|
|
27
|
+
* Exported because the history job must resolve it at CALL time for a Worker
|
|
28
|
+
* that does not inherit this module's load-time constants — and must resolve it
|
|
29
|
+
* the same way, since a manifest addressed differently is a different manifest.
|
|
30
|
+
*/
|
|
31
|
+
export function historyBackupPathFor(stateDbPath: string): string {
|
|
32
|
+
return join(getConfigDir(), `codex-history-backup-${codexHistoryBackupId(stateDbPath)}.json`);
|
|
21
33
|
}
|
|
22
|
-
const HISTORY_BACKUP_PATH = historyBackupPathFor(STATE_DB_PATH);
|
|
23
|
-
const RESUMABLE_SOURCES = ["cli", "vscode"] as const;
|
|
24
34
|
|
|
25
35
|
/**
|
|
26
36
|
* Open the live `state_5.sqlite` the way the Codex app expects a *secondary* writer to behave:
|
|
@@ -64,10 +74,10 @@ function openStateDb(stateDbPath: string): Database {
|
|
|
64
74
|
* concurrently. We do not touch mtime: a fresh mtime is correct here (the app uses mtime as the
|
|
65
75
|
* rollout's updated_at), and forcing it backwards could hide a real edit from list ordering.
|
|
66
76
|
*/
|
|
67
|
-
function appendRolloutLine(path: string, line: string):
|
|
77
|
+
function appendRolloutLine(path: string, line: string): Buffer {
|
|
68
78
|
const fd = openSync(path, "a");
|
|
79
|
+
const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
|
|
69
80
|
try {
|
|
70
|
-
const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
|
|
71
81
|
let offset = 0;
|
|
72
82
|
while (offset < buf.length) {
|
|
73
83
|
offset += writeSync(fd, buf, offset, buf.length - offset, null);
|
|
@@ -76,6 +86,7 @@ function appendRolloutLine(path: string, line: string): void {
|
|
|
76
86
|
} finally {
|
|
77
87
|
closeSync(fd);
|
|
78
88
|
}
|
|
89
|
+
return buf;
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
/**
|
|
@@ -93,78 +104,141 @@ function appendRolloutLine(path: string, line: string): void {
|
|
|
93
104
|
* insignificant JSON whitespace. We therefore replace the provider value and pad the removed bytes
|
|
94
105
|
* with spaces so the line's byte length is unchanged. Equal length means we can write at offset 0
|
|
95
106
|
* with no truncate and no inode swap, so this composes safely with the app's cached append handle.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
107
|
+
* A previous shrink leaves JSON whitespace in the token slot. That padding is part of the slot,
|
|
108
|
+
* so an exact restore can later grow "openai" back to "opencodex" without moving any bytes.
|
|
98
109
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
110
|
+
* Distinguishes an already-correct line from an unsafe one so exact restore never consumes its
|
|
111
|
+
* manifest after only the trailing metadata was repaired.
|
|
101
112
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
type FirstLineProviderResult = "current" | "patched" | "unsafe";
|
|
114
|
+
|
|
115
|
+
type FirstLineProviderPlan =
|
|
116
|
+
| { readonly state: "current" }
|
|
117
|
+
| { readonly state: "patchable"; readonly patchedLine: string }
|
|
118
|
+
| { readonly state: "unsafe" };
|
|
119
|
+
|
|
120
|
+
function readFirstRolloutLine(fd: number): string | null {
|
|
121
|
+
// session_meta lines embed base_instructions and can be tens of KB; grow until the line
|
|
122
|
+
// actually ends rather than imposing a small fixed probe that would reject valid history.
|
|
123
|
+
const CHUNK = 1 << 16;
|
|
124
|
+
const MAX_FIRST_LINE = 1 << 24;
|
|
125
|
+
let collected = Buffer.alloc(0);
|
|
126
|
+
let nlIndex = -1;
|
|
127
|
+
let pos = 0;
|
|
128
|
+
while (nlIndex === -1) {
|
|
129
|
+
const chunk = Buffer.alloc(CHUNK);
|
|
130
|
+
const read = readSync(fd, chunk, 0, CHUNK, pos);
|
|
131
|
+
if (read === 0) break;
|
|
132
|
+
collected = Buffer.concat([collected, chunk.subarray(0, read)]);
|
|
133
|
+
nlIndex = collected.indexOf(0x0a);
|
|
134
|
+
pos += read;
|
|
135
|
+
if (collected.length > MAX_FIRST_LINE) return null;
|
|
136
|
+
}
|
|
137
|
+
return nlIndex === -1 ? null : collected.subarray(0, nlIndex).toString("utf8");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function planFirstLineProvider(firstLine: string, expectedId: string, provider: string): FirstLineProviderPlan {
|
|
141
|
+
const meta = parseSessionMetaLine(firstLine);
|
|
142
|
+
if (!meta || meta.record.payload.id !== expectedId) return { state: "unsafe" };
|
|
143
|
+
if (meta.record.payload.model_provider === provider) return { state: "current" };
|
|
144
|
+
|
|
145
|
+
// Include JSON whitespace after the value. A prior length-preserving shrink stores its spare
|
|
146
|
+
// bytes there, so the exact reverse restore may grow back into that padding.
|
|
147
|
+
const match = firstLine.match(/"model_provider"\s*:\s*"([^"\\]*)"[ \t]*/);
|
|
148
|
+
if (!match || match.index === undefined) return { state: "unsafe" };
|
|
149
|
+
const oldToken = match[0];
|
|
150
|
+
const newCore = `"model_provider":"${provider}"`;
|
|
151
|
+
if (Buffer.byteLength(newCore, "utf8") > Buffer.byteLength(oldToken, "utf8")) return { state: "unsafe" };
|
|
152
|
+
const pad = " ".repeat(Buffer.byteLength(oldToken, "utf8") - Buffer.byteLength(newCore, "utf8"));
|
|
153
|
+
const patchedLine = firstLine.slice(0, match.index) + newCore + pad + firstLine.slice(match.index + oldToken.length);
|
|
154
|
+
if (Buffer.byteLength(patchedLine, "utf8") !== Buffer.byteLength(firstLine, "utf8")) return { state: "unsafe" };
|
|
155
|
+
const reparsed = parseSessionMetaLine(patchedLine);
|
|
156
|
+
if (!reparsed
|
|
157
|
+
|| reparsed.record.payload.id !== expectedId
|
|
158
|
+
|| reparsed.record.payload.model_provider !== provider) return { state: "unsafe" };
|
|
159
|
+
return { state: "patchable", patchedLine };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function inspectFirstLineProvider(path: string, expectedId: string, provider: string): "current" | "patchable" | "unsafe" {
|
|
163
|
+
if (!existsSync(path)) return "unsafe";
|
|
164
|
+
const fd = openSync(path, "r");
|
|
105
165
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
let collected = Buffer.alloc(0);
|
|
113
|
-
let nlIndex = -1;
|
|
114
|
-
let pos = 0;
|
|
115
|
-
while (nlIndex === -1) {
|
|
116
|
-
const chunk = Buffer.alloc(CHUNK);
|
|
117
|
-
const read = readSync(fd, chunk, 0, CHUNK, pos);
|
|
118
|
-
if (read === 0) break; // EOF with no newline: single-line file, skip
|
|
119
|
-
collected = Buffer.concat([collected, chunk.subarray(0, read)]);
|
|
120
|
-
nlIndex = collected.indexOf(0x0a);
|
|
121
|
-
pos += read;
|
|
122
|
-
if (collected.length > MAX_FIRST_LINE) return false;
|
|
123
|
-
}
|
|
124
|
-
if (nlIndex === -1) return false; // no newline anywhere: skip
|
|
125
|
-
const firstLine = collected.subarray(0, nlIndex).toString("utf8");
|
|
166
|
+
const firstLine = readFirstRolloutLine(fd);
|
|
167
|
+
return firstLine === null ? "unsafe" : planFirstLineProvider(firstLine, expectedId, provider).state;
|
|
168
|
+
} finally {
|
|
169
|
+
closeSync(fd);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
126
172
|
|
|
173
|
+
function readFirstLineProviderValue(path: string, expectedId: string): string | null {
|
|
174
|
+
if (!existsSync(path)) return null;
|
|
175
|
+
const fd = openSync(path, "r");
|
|
176
|
+
try {
|
|
177
|
+
const firstLine = readFirstRolloutLine(fd);
|
|
178
|
+
if (firstLine === null) return null;
|
|
127
179
|
const meta = parseSessionMetaLine(firstLine);
|
|
128
|
-
if (!meta) return
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const out = Buffer.from(patchedLine, "utf8");
|
|
180
|
+
if (!meta || meta.record.payload.id !== expectedId) return null;
|
|
181
|
+
return typeof meta.record.payload.model_provider === "string"
|
|
182
|
+
? meta.record.payload.model_provider
|
|
183
|
+
: null;
|
|
184
|
+
} finally {
|
|
185
|
+
closeSync(fd);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function patchFirstLineProviderInPlace(path: string, expectedId: string, provider: string): FirstLineProviderResult {
|
|
190
|
+
if (!existsSync(path)) return "unsafe";
|
|
191
|
+
const fd = openSync(path, "r+");
|
|
192
|
+
try {
|
|
193
|
+
const firstLine = readFirstRolloutLine(fd);
|
|
194
|
+
if (firstLine === null) return "unsafe";
|
|
195
|
+
const plan = planFirstLineProvider(firstLine, expectedId, provider);
|
|
196
|
+
if (plan.state === "unsafe") return "unsafe";
|
|
197
|
+
if (plan.state === "current") return "current";
|
|
198
|
+
const out = Buffer.from(plan.patchedLine, "utf8");
|
|
149
199
|
let offset = 0;
|
|
150
200
|
while (offset < out.length) {
|
|
151
201
|
offset += writeSync(fd, out, offset, out.length - offset, offset);
|
|
152
202
|
}
|
|
153
203
|
try { fsyncSync(fd); } catch { /* best-effort durability */ }
|
|
154
|
-
return
|
|
204
|
+
return "patched";
|
|
155
205
|
} finally {
|
|
156
206
|
closeSync(fd);
|
|
157
207
|
}
|
|
158
208
|
}
|
|
159
209
|
|
|
160
|
-
type CodexHistoryProvider = "openai" | "opencodex";
|
|
210
|
+
export type CodexHistoryProvider = "openai" | "opencodex";
|
|
211
|
+
|
|
212
|
+
export type CodexHistoryFailureReason = "busy" | "permission" | "integrity";
|
|
213
|
+
|
|
214
|
+
class CodexHistoryIntegrityError extends Error {
|
|
215
|
+
constructor(
|
|
216
|
+
code: string,
|
|
217
|
+
readonly progress: { readonly rows: number; readonly files: number } = { rows: 0, files: 0 },
|
|
218
|
+
) {
|
|
219
|
+
super(code);
|
|
220
|
+
this.name = "CodexHistoryIntegrityError";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function integrityFailureResult(error: CodexHistoryIntegrityError): CodexHistorySyncResult {
|
|
225
|
+
return {
|
|
226
|
+
rows: error.progress.rows,
|
|
227
|
+
files: error.progress.files,
|
|
228
|
+
failed: true,
|
|
229
|
+
failureReason: "integrity",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
161
232
|
|
|
162
233
|
export interface CodexHistorySyncResult {
|
|
234
|
+
/** Rows/files changed before a last-moment integrity race; may be nonzero with `failed`. */
|
|
163
235
|
rows: number;
|
|
164
236
|
files: number;
|
|
165
237
|
ejectedRows?: number;
|
|
166
238
|
/** Set when a lock/busy error survived retries and the sync was SKIPPED, not empty. */
|
|
167
239
|
failed?: true;
|
|
240
|
+
/** Why the retry budget was exhausted when `failed` is set. */
|
|
241
|
+
failureReason?: CodexHistoryFailureReason;
|
|
168
242
|
}
|
|
169
243
|
|
|
170
244
|
interface ThreadRow {
|
|
@@ -175,49 +249,211 @@ interface ThreadRow {
|
|
|
175
249
|
has_user_event: number;
|
|
176
250
|
}
|
|
177
251
|
|
|
178
|
-
interface
|
|
179
|
-
|
|
180
|
-
rolloutPath: string;
|
|
181
|
-
modelProvider: string;
|
|
182
|
-
source: string;
|
|
183
|
-
hasUserEvent: number;
|
|
252
|
+
interface RestoreRowSnapshot extends ThreadRow {
|
|
253
|
+
first_user_message: string | null;
|
|
184
254
|
}
|
|
185
255
|
|
|
186
|
-
interface
|
|
187
|
-
|
|
188
|
-
stateDbPath?: string;
|
|
189
|
-
entries: Record<string, BackupEntry>;
|
|
256
|
+
interface ApplyRowSnapshot extends ThreadRow {
|
|
257
|
+
first_user_message: string | null;
|
|
190
258
|
}
|
|
191
259
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
source: string;
|
|
195
|
-
hasUserEvent: number;
|
|
260
|
+
function hasFirstUserMessage(value: string | null): boolean {
|
|
261
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
196
262
|
}
|
|
197
263
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
264
|
+
export interface CodexHistoryVerifiedNoopProof {
|
|
265
|
+
readonly kind: "verified-noop";
|
|
266
|
+
readonly pendingRows: 0;
|
|
267
|
+
readonly backupEntries: 0;
|
|
268
|
+
readonly canonicalStateDbPath: string;
|
|
269
|
+
readonly stateDbPresent: true;
|
|
270
|
+
readonly canonicalBackupPath: string;
|
|
271
|
+
readonly backupPresent: boolean;
|
|
202
272
|
}
|
|
203
273
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
274
|
+
export type CodexHistoryNoopSnapshot =
|
|
275
|
+
| CodexHistoryVerifiedNoopProof
|
|
276
|
+
| {
|
|
277
|
+
readonly kind: "work-pending";
|
|
278
|
+
readonly pendingRows: number;
|
|
279
|
+
readonly backupEntries: number;
|
|
280
|
+
readonly canonicalStateDbPath: string;
|
|
281
|
+
readonly stateDbPresent: boolean;
|
|
282
|
+
readonly canonicalBackupPath: string;
|
|
283
|
+
readonly backupPresent: boolean;
|
|
210
284
|
}
|
|
211
|
-
|
|
212
|
-
|
|
285
|
+
| {
|
|
286
|
+
readonly kind: "unknown";
|
|
287
|
+
readonly pendingRows: null;
|
|
288
|
+
readonly backupEntries: null;
|
|
289
|
+
readonly canonicalStateDbPath: string;
|
|
290
|
+
readonly stateDbPresent: boolean;
|
|
291
|
+
readonly canonicalBackupPath: string;
|
|
292
|
+
readonly backupPresent: boolean;
|
|
293
|
+
readonly reason: "backup-path" | "database-absent" | "manifest-read" | "manifest-schema" | "manifest-foreign" | "database-query" | "snapshot-race";
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
type StrictBackupInspection =
|
|
297
|
+
| { readonly kind: "known"; readonly present: boolean; readonly entries: number; readonly fingerprint: string }
|
|
298
|
+
| { readonly kind: "unknown"; readonly present: boolean; readonly reason: "manifest-read" | "manifest-schema" | "manifest-foreign";
|
|
299
|
+
readonly failureReason?: "busy" | "permission" };
|
|
300
|
+
|
|
301
|
+
type StrictBackupRead =
|
|
302
|
+
| {
|
|
303
|
+
readonly kind: "known";
|
|
304
|
+
readonly present: boolean;
|
|
305
|
+
readonly manifest: CodexHistoryBackupManifest;
|
|
306
|
+
readonly fingerprint: string;
|
|
307
|
+
}
|
|
308
|
+
| {
|
|
309
|
+
readonly kind: "unknown";
|
|
310
|
+
readonly present: true;
|
|
311
|
+
readonly reason: "manifest-read" | "manifest-schema" | "manifest-foreign";
|
|
312
|
+
readonly failureReason?: "busy" | "permission";
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
let afterNoopPendingCountForTests: (() => void) | undefined;
|
|
316
|
+
let beforeHistoryBackupConsumeForTests: (() => void) | undefined;
|
|
317
|
+
let beforeStrictHistoryRolloutAppendForTests: (() => void) | undefined;
|
|
318
|
+
let afterStrictHistoryRolloutAppendForTests: (() => void) | undefined;
|
|
319
|
+
let beforeHistoryApplyTransactionForTests: (() => void) | undefined;
|
|
320
|
+
|
|
321
|
+
/** Test seam: runs after the pending count and before stability validation. */
|
|
322
|
+
export function setAfterNoopPendingCountForTests(hook: (() => void) | undefined): void {
|
|
323
|
+
afterNoopPendingCountForTests = hook;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Test seam: runs after exact DB/rollout readback and before manifest fingerprint CAS. */
|
|
327
|
+
export function setBeforeHistoryBackupConsumeForTests(hook: (() => void) | undefined): void {
|
|
328
|
+
beforeHistoryBackupConsumeForTests = hook;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Test seam: models a same-file append after strict snapshot validation but before our append. */
|
|
332
|
+
export function setBeforeStrictHistoryRolloutAppendForTests(hook: (() => void) | undefined): void {
|
|
333
|
+
beforeStrictHistoryRolloutAppendForTests = hook;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Test seam: models a write/finalization failure after the strict append reached disk. */
|
|
337
|
+
export function setAfterStrictHistoryRolloutAppendForTests(hook: (() => void) | undefined): void {
|
|
338
|
+
afterStrictHistoryRolloutAppendForTests = hook;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Test seam: runs after manifest snapshot publication and before apply's database CAS. */
|
|
342
|
+
export function setBeforeHistoryApplyTransactionForTests(hook: (() => void) | undefined): void {
|
|
343
|
+
beforeHistoryApplyTransactionForTests = hook;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function readBackupStrict(path: string, stateDbPath: string): StrictBackupRead {
|
|
347
|
+
let pathStat: ReturnType<typeof lstatSync>;
|
|
348
|
+
try {
|
|
349
|
+
pathStat = lstatSync(path);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
352
|
+
if (code !== "ENOENT") {
|
|
353
|
+
const failureReason = classifyRecoverableHistoryError(error);
|
|
354
|
+
return {
|
|
355
|
+
kind: "unknown",
|
|
356
|
+
present: true,
|
|
357
|
+
reason: "manifest-read",
|
|
358
|
+
...(failureReason === "busy" || failureReason === "permission" ? { failureReason } : {}),
|
|
359
|
+
};
|
|
213
360
|
}
|
|
214
|
-
return {
|
|
361
|
+
return {
|
|
362
|
+
kind: "known",
|
|
363
|
+
present: false,
|
|
364
|
+
manifest: { version: 1, stateDbPath, entries: {} },
|
|
365
|
+
fingerprint: "absent",
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (!pathStat.isFile() || pathStat.isSymbolicLink()) {
|
|
369
|
+
return { kind: "unknown", present: true, reason: "manifest-read" };
|
|
370
|
+
}
|
|
371
|
+
let raw: string;
|
|
372
|
+
try {
|
|
373
|
+
raw = readFileSync(path, "utf8");
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const failureReason = classifyRecoverableHistoryError(error);
|
|
376
|
+
return {
|
|
377
|
+
kind: "unknown",
|
|
378
|
+
present: true,
|
|
379
|
+
reason: "manifest-read",
|
|
380
|
+
...(failureReason === "busy" || failureReason === "permission" ? { failureReason } : {}),
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
let parsed: unknown;
|
|
384
|
+
try {
|
|
385
|
+
parsed = JSON.parse(raw);
|
|
215
386
|
} catch {
|
|
216
|
-
return {
|
|
387
|
+
return { kind: "unknown", present: true, reason: "manifest-read" };
|
|
217
388
|
}
|
|
389
|
+
const validated = validateCodexHistoryBackupManifest(parsed, stateDbPath);
|
|
390
|
+
if (!validated.ok) {
|
|
391
|
+
return {
|
|
392
|
+
kind: "unknown",
|
|
393
|
+
present: true,
|
|
394
|
+
reason: validated.reason === "foreign-database" ? "manifest-foreign" : "manifest-schema",
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
kind: "known",
|
|
399
|
+
present: true,
|
|
400
|
+
manifest: validated.manifest,
|
|
401
|
+
fingerprint: createHash("sha256").update(raw).digest("hex"),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function inspectBackupForNoop(path: string, stateDbPath: string): StrictBackupInspection {
|
|
406
|
+
const read = readBackupStrict(path, stateDbPath);
|
|
407
|
+
return read.kind === "unknown"
|
|
408
|
+
? read
|
|
409
|
+
: {
|
|
410
|
+
kind: "known",
|
|
411
|
+
present: read.present,
|
|
412
|
+
entries: Object.keys(read.manifest.entries).length,
|
|
413
|
+
fingerprint: read.fingerprint,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function historyFileIdentity(path: string): string | null {
|
|
418
|
+
try {
|
|
419
|
+
const stat = statSync(path);
|
|
420
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
421
|
+
} catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function readHistoryDataVersion(db: Database): number | null {
|
|
427
|
+
const row = db.query<{ data_version: number }, []>("PRAGMA data_version").get();
|
|
428
|
+
const value = row?.data_version;
|
|
429
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function readBackup(path: string, stateDbPath: string): Extract<StrictBackupRead, { kind: "known" }> {
|
|
433
|
+
const read = readBackupStrict(path, stateDbPath);
|
|
434
|
+
if (read.kind === "unknown") {
|
|
435
|
+
if (read.failureReason) {
|
|
436
|
+
throw Object.assign(
|
|
437
|
+
new Error(read.failureReason === "busy" ? "history backup is busy" : "history backup permission denied"),
|
|
438
|
+
{ code: read.failureReason === "busy" ? "EBUSY" : "EACCES" },
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
throw new CodexHistoryIntegrityError(`history_backup_${read.reason.replaceAll("-", "_")}`);
|
|
442
|
+
}
|
|
443
|
+
return read;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function consumeBackupIfUnchanged(path: string, stateDbPath: string, expectedFingerprint: string): void {
|
|
447
|
+
const current = readBackupStrict(path, stateDbPath);
|
|
448
|
+
if (current.kind !== "known"
|
|
449
|
+
|| !current.present
|
|
450
|
+
|| current.fingerprint !== expectedFingerprint) {
|
|
451
|
+
throw new CodexHistoryIntegrityError("history_backup_changed_during_restore");
|
|
452
|
+
}
|
|
453
|
+
unlinkSync(path);
|
|
218
454
|
}
|
|
219
455
|
|
|
220
|
-
function writeBackup(path: string, manifest:
|
|
456
|
+
function writeBackup(path: string, manifest: CodexHistoryBackupManifest, stateDbPath?: string): void {
|
|
221
457
|
if (Object.keys(manifest.entries).length === 0) {
|
|
222
458
|
if (existsSync(path)) unlinkSync(path);
|
|
223
459
|
return;
|
|
@@ -226,17 +462,181 @@ function writeBackup(path: string, manifest: BackupManifest, stateDbPath?: strin
|
|
|
226
462
|
atomicWriteFile(path, JSON.stringify({ ...manifest, stateDbPath: manifest.stateDbPath ?? stateDbPath }, null, 2) + "\n");
|
|
227
463
|
}
|
|
228
464
|
|
|
229
|
-
function rememberOriginal(manifest:
|
|
465
|
+
function rememberOriginal(manifest: CodexHistoryBackupManifest, row: ThreadRow): void {
|
|
230
466
|
if (manifest.entries[row.id]) return;
|
|
231
467
|
manifest.entries[row.id] = {
|
|
232
468
|
id: row.id,
|
|
233
469
|
rolloutPath: row.rollout_path,
|
|
234
470
|
modelProvider: row.model_provider,
|
|
235
471
|
source: row.source,
|
|
236
|
-
hasUserEvent: Number(row.has_user_event)
|
|
472
|
+
hasUserEvent: Number(row.has_user_event) === 1 ? 1 : 0,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function rowMatchesRestoreTuple(
|
|
477
|
+
row: RestoreRowSnapshot,
|
|
478
|
+
modelProvider: string,
|
|
479
|
+
source: string,
|
|
480
|
+
hasUserEvent: number,
|
|
481
|
+
): boolean {
|
|
482
|
+
return row.model_provider === modelProvider
|
|
483
|
+
&& row.source === source
|
|
484
|
+
&& row.has_user_event === hasUserEvent;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function rowMatchesExpectedPostImage(row: RestoreRowSnapshot, entry: CodexHistoryBackupEntry): boolean {
|
|
488
|
+
if (entry.modelProvider === "openai") {
|
|
489
|
+
const postHasUserEvent = hasFirstUserMessage(row.first_user_message) ? 1 : entry.hasUserEvent;
|
|
490
|
+
return rowMatchesRestoreTuple(row, "opencodex", entry.source, postHasUserEvent);
|
|
491
|
+
}
|
|
492
|
+
return hasFirstUserMessage(row.first_user_message)
|
|
493
|
+
&& (
|
|
494
|
+
rowMatchesRestoreTuple(row, "opencodex", "cli", 1)
|
|
495
|
+
// Older restore code coerced an opencodex/exec original into this exact tuple before
|
|
496
|
+
// consuming its manifest. Accept that one known post-image so an interrupted old restore
|
|
497
|
+
// can use its preserved first-line padding to recover exact provenance.
|
|
498
|
+
|| rowMatchesRestoreTuple(row, "openai", "cli", 1)
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
interface RestoreRolloutSnapshot {
|
|
503
|
+
readonly identity: string;
|
|
504
|
+
readonly latestProvider: string;
|
|
505
|
+
readonly latestSource: string;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function normalizedSessionMetaTuple(meta: ParsedSessionMeta): { provider: string; source: string } {
|
|
509
|
+
const payload = meta.record.payload;
|
|
510
|
+
return {
|
|
511
|
+
provider: typeof payload.model_provider === "string" && payload.model_provider
|
|
512
|
+
? payload.model_provider
|
|
513
|
+
: "openai",
|
|
514
|
+
source: typeof payload.source === "string" && payload.source ? payload.source : "cli",
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function rolloutMatchesRestoreTuple(
|
|
519
|
+
meta: ParsedSessionMeta,
|
|
520
|
+
entry: CodexHistoryBackupEntry,
|
|
521
|
+
provider: string,
|
|
522
|
+
source: string,
|
|
523
|
+
): boolean {
|
|
524
|
+
const tuple = normalizedSessionMetaTuple(meta);
|
|
525
|
+
return meta.record.payload.id === entry.id
|
|
526
|
+
&& tuple.provider === provider
|
|
527
|
+
&& tuple.source === source;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function rolloutMatchesExpectedPostImage(meta: ParsedSessionMeta, entry: CodexHistoryBackupEntry): boolean {
|
|
531
|
+
if (entry.modelProvider === "openai") {
|
|
532
|
+
const tuple = normalizedSessionMetaTuple(meta);
|
|
533
|
+
const rawSource = meta.record.payload.source;
|
|
534
|
+
return meta.record.payload.id === entry.id
|
|
535
|
+
&& tuple.provider === "opencodex"
|
|
536
|
+
// Older/native session_meta records can omit source even when SQLite identifies the
|
|
537
|
+
// surface as vscode. Apply changes only the provider, so absence is a valid post-image;
|
|
538
|
+
// restore appends the exact manifest source before consuming provenance.
|
|
539
|
+
&& ((typeof rawSource !== "string" || !rawSource) || tuple.source === entry.source);
|
|
540
|
+
}
|
|
541
|
+
return rolloutMatchesRestoreTuple(meta, entry, "opencodex", "cli")
|
|
542
|
+
// Keep the same one-version recovery bridge as the database tuple check: older forced
|
|
543
|
+
// restore code could already have produced openai/cli before consuming this manifest.
|
|
544
|
+
|| rolloutMatchesRestoreTuple(meta, entry, "openai", "cli");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function snapshotRolloutForRestore(entry: CodexHistoryBackupEntry): RestoreRolloutSnapshot {
|
|
548
|
+
const identityBefore = historyFileIdentity(entry.rolloutPath);
|
|
549
|
+
if (identityBefore === null) {
|
|
550
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_unrestorable");
|
|
551
|
+
}
|
|
552
|
+
const latest = readLatestSessionMeta(entry.rolloutPath);
|
|
553
|
+
if (!latest
|
|
554
|
+
|| (!rolloutMatchesRestoreTuple(latest, entry, entry.modelProvider, entry.source)
|
|
555
|
+
&& !rolloutMatchesExpectedPostImage(latest, entry))) {
|
|
556
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_postimage_mismatch");
|
|
557
|
+
}
|
|
558
|
+
const firstProvider = readFirstLineProviderValue(entry.rolloutPath, entry.id);
|
|
559
|
+
if (firstProvider !== "openai" && firstProvider !== "opencodex") {
|
|
560
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_postimage_mismatch");
|
|
561
|
+
}
|
|
562
|
+
if (inspectFirstLineProvider(entry.rolloutPath, entry.id, entry.modelProvider) === "unsafe") {
|
|
563
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_unrestorable");
|
|
564
|
+
}
|
|
565
|
+
if (historyFileIdentity(entry.rolloutPath) !== identityBefore) {
|
|
566
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_changed_during_restore");
|
|
567
|
+
}
|
|
568
|
+
const tuple = normalizedSessionMetaTuple(latest);
|
|
569
|
+
return {
|
|
570
|
+
identity: identityBefore,
|
|
571
|
+
latestProvider: tuple.provider,
|
|
572
|
+
latestSource: tuple.source,
|
|
237
573
|
};
|
|
238
574
|
}
|
|
239
575
|
|
|
576
|
+
interface RestoreTargetPreflight {
|
|
577
|
+
readonly snapshots: Map<string, RestoreRowSnapshot>;
|
|
578
|
+
readonly rolloutSnapshots: Map<string, RestoreRolloutSnapshot>;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Read-only authority shared by restore and status/doctor. Every manifest entry must still
|
|
583
|
+
* identify either its exact target tuple or the one OpenCodex post-image, and its rollout
|
|
584
|
+
* must be present, stable, same-id, and durably restorable before callers call it pending.
|
|
585
|
+
*/
|
|
586
|
+
function preflightRestoreTargets(
|
|
587
|
+
getCurrent: (id: string) => RestoreRowSnapshot | null,
|
|
588
|
+
entries: CodexHistoryBackupEntry[],
|
|
589
|
+
): RestoreTargetPreflight {
|
|
590
|
+
const snapshots = preflightRestoreRows(getCurrent, entries);
|
|
591
|
+
const rolloutSnapshots = new Map<string, RestoreRolloutSnapshot>();
|
|
592
|
+
for (const entry of entries) {
|
|
593
|
+
// Validate every rollout before the first mutation. A later missing, foreign, or
|
|
594
|
+
// unpatchable entry must not leave an earlier file partially restored.
|
|
595
|
+
rolloutSnapshots.set(entry.id, snapshotRolloutForRestore(entry));
|
|
596
|
+
}
|
|
597
|
+
return { snapshots, rolloutSnapshots };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** Cheap manifest-to-database authority check used by recurring no-op probes. */
|
|
601
|
+
function preflightRestoreRows(
|
|
602
|
+
getCurrent: (id: string) => RestoreRowSnapshot | null,
|
|
603
|
+
entries: CodexHistoryBackupEntry[],
|
|
604
|
+
): Map<string, RestoreRowSnapshot> {
|
|
605
|
+
const snapshots = new Map<string, RestoreRowSnapshot>();
|
|
606
|
+
for (const entry of entries) {
|
|
607
|
+
const row = getCurrent(entry.id);
|
|
608
|
+
if (!row || typeof row.rollout_path !== "string" || !sameCodexHistoryPath(row.rollout_path, entry.rolloutPath)) {
|
|
609
|
+
throw new CodexHistoryIntegrityError("history_backup_target_mismatch");
|
|
610
|
+
}
|
|
611
|
+
if (!rowMatchesRestoreTuple(row, entry.modelProvider, entry.source, entry.hasUserEvent)
|
|
612
|
+
&& !rowMatchesExpectedPostImage(row, entry)) {
|
|
613
|
+
throw new CodexHistoryIntegrityError("history_backup_postimage_mismatch");
|
|
614
|
+
}
|
|
615
|
+
snapshots.set(entry.id, row);
|
|
616
|
+
}
|
|
617
|
+
return snapshots;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function assertRestoreReadback(
|
|
621
|
+
getCurrent: (id: string) => RestoreRowSnapshot | null,
|
|
622
|
+
entries: CodexHistoryBackupEntry[],
|
|
623
|
+
): void {
|
|
624
|
+
for (const entry of entries) {
|
|
625
|
+
const row = getCurrent(entry.id);
|
|
626
|
+
if (!row
|
|
627
|
+
|| !sameCodexHistoryPath(row.rollout_path, entry.rolloutPath)
|
|
628
|
+
|| !rowMatchesRestoreTuple(row, entry.modelProvider, entry.source, entry.hasUserEvent)) {
|
|
629
|
+
throw new CodexHistoryIntegrityError("history_backup_database_readback_mismatch");
|
|
630
|
+
}
|
|
631
|
+
const latest = readLatestSessionMeta(entry.rolloutPath);
|
|
632
|
+
if (inspectFirstLineProvider(entry.rolloutPath, entry.id, entry.modelProvider) !== "current"
|
|
633
|
+
|| !latest
|
|
634
|
+
|| !rolloutMatchesRestoreTuple(latest, entry, entry.modelProvider, entry.source)) {
|
|
635
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_readback_mismatch");
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
240
640
|
interface ParsedSessionMeta {
|
|
241
641
|
record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
|
|
242
642
|
}
|
|
@@ -262,6 +662,10 @@ function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
|
|
|
262
662
|
*/
|
|
263
663
|
export function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
|
|
264
664
|
const raw = readFileSync(path, "utf8");
|
|
665
|
+
return readLatestSessionMetaFromText(raw);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function readLatestSessionMetaFromText(raw: string): ParsedSessionMeta | null {
|
|
265
669
|
const lines = raw.split("\n");
|
|
266
670
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
267
671
|
const line = lines[i];
|
|
@@ -273,6 +677,39 @@ export function readLatestSessionMeta(path: string): ParsedSessionMeta | null {
|
|
|
273
677
|
return null;
|
|
274
678
|
}
|
|
275
679
|
|
|
680
|
+
function readLatestSessionMetaForIdFromText(raw: string, expectedId: string): ParsedSessionMeta | null {
|
|
681
|
+
const lines = raw.split("\n");
|
|
682
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
683
|
+
const line = lines[i];
|
|
684
|
+
if (!line || !line.includes("\"session_meta\"")) continue;
|
|
685
|
+
const meta = parseSessionMetaLine(line);
|
|
686
|
+
if (meta?.record.payload.id === expectedId) return meta;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function compensateConcurrentSessionMetaAppend(
|
|
692
|
+
path: string,
|
|
693
|
+
expectedId: string,
|
|
694
|
+
appended: Buffer,
|
|
695
|
+
minimumOffset: number,
|
|
696
|
+
): void {
|
|
697
|
+
try {
|
|
698
|
+
const raw = readFileSync(path);
|
|
699
|
+
const ownOffset = raw.lastIndexOf(appended);
|
|
700
|
+
if (ownOffset < minimumOffset) return;
|
|
701
|
+
const withoutOwnAppend = Buffer.concat([
|
|
702
|
+
raw.subarray(0, ownOffset),
|
|
703
|
+
raw.subarray(ownOffset + appended.length),
|
|
704
|
+
]).toString("utf8");
|
|
705
|
+
const prior = readLatestSessionMetaForIdFromText(withoutOwnAppend, expectedId);
|
|
706
|
+
if (prior) appendRolloutLine(path, JSON.stringify(prior.record));
|
|
707
|
+
} catch {
|
|
708
|
+
// The caller reports an integrity conflict and retains the manifest. Compensation is
|
|
709
|
+
// best-effort because a second write failure must not erase the original failure evidence.
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
276
713
|
/**
|
|
277
714
|
* Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
|
|
278
715
|
* Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
|
|
@@ -412,15 +849,34 @@ function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | nu
|
|
|
412
849
|
/**
|
|
413
850
|
* Make a thread's rollout reflect a provider/source change by APPENDING a new `session_meta` line,
|
|
414
851
|
* rather than rewriting line 1. The appended line clones the latest metadata payload (so no field
|
|
415
|
-
* is accidentally reset to empty) and applies only the requested changes.
|
|
416
|
-
*
|
|
417
|
-
* different thread id, or it already matches the desired values.
|
|
852
|
+
* is accidentally reset to empty) and applies only the requested changes. `durableProvider`
|
|
853
|
+
* reports whether line 1 already carried (or was safely patched to) the requested provider.
|
|
418
854
|
*/
|
|
419
|
-
|
|
420
|
-
|
|
855
|
+
interface SessionMetaUpdateResult {
|
|
856
|
+
changed: boolean;
|
|
857
|
+
durableProvider: boolean;
|
|
858
|
+
conflict?: true;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function updateSessionMeta(
|
|
862
|
+
path: string,
|
|
863
|
+
expectedId: string,
|
|
864
|
+
patch: { provider?: string; source?: string },
|
|
865
|
+
options: {
|
|
866
|
+
requireDurableProvider?: boolean;
|
|
867
|
+
expectedFileIdentity?: string;
|
|
868
|
+
expectedLatestProvider?: string;
|
|
869
|
+
expectedLatestSource?: string;
|
|
870
|
+
} = {},
|
|
871
|
+
): SessionMetaUpdateResult {
|
|
872
|
+
if (!path || !existsSync(path)) return { changed: false, durableProvider: false };
|
|
873
|
+
if (options.expectedFileIdentity !== undefined
|
|
874
|
+
&& historyFileIdentity(path) !== options.expectedFileIdentity) {
|
|
875
|
+
return { changed: false, durableProvider: false, conflict: true };
|
|
876
|
+
}
|
|
421
877
|
|
|
422
878
|
const latest = readLatestSessionMeta(path);
|
|
423
|
-
if (!latest) return false;
|
|
879
|
+
if (!latest) return { changed: false, durableProvider: false };
|
|
424
880
|
const record = latest.record;
|
|
425
881
|
|
|
426
882
|
// The app ignores `session_meta` lines whose payload id != the canonical thread id
|
|
@@ -428,8 +884,23 @@ function updateSessionMeta(path: string, expectedId: string, patch: { provider?:
|
|
|
428
884
|
// metadata, so an id-mismatched latest line means we'd be cloning the wrong thread's meta and
|
|
429
885
|
// appending a line the app would discard. Skip rather than write a no-op/misleading line.
|
|
430
886
|
const payloadId = record.payload.id;
|
|
431
|
-
if (typeof payloadId !== "string" || payloadId !== expectedId)
|
|
887
|
+
if (typeof payloadId !== "string" || payloadId !== expectedId) {
|
|
888
|
+
return { changed: false, durableProvider: false };
|
|
889
|
+
}
|
|
890
|
+
const latestProvider = typeof record.payload.model_provider === "string" && record.payload.model_provider
|
|
891
|
+
? record.payload.model_provider
|
|
892
|
+
: "openai";
|
|
893
|
+
const latestSource = typeof record.payload.source === "string" && record.payload.source
|
|
894
|
+
? record.payload.source
|
|
895
|
+
: "cli";
|
|
896
|
+
if ((options.expectedLatestProvider !== undefined && latestProvider !== options.expectedLatestProvider)
|
|
897
|
+
|| (options.expectedLatestSource !== undefined && latestSource !== options.expectedLatestSource)
|
|
898
|
+
|| (options.expectedFileIdentity !== undefined
|
|
899
|
+
&& historyFileIdentity(path) !== options.expectedFileIdentity)) {
|
|
900
|
+
return { changed: false, durableProvider: false, conflict: true };
|
|
901
|
+
}
|
|
432
902
|
|
|
903
|
+
const previousRecord = JSON.stringify(record);
|
|
433
904
|
let changed = false;
|
|
434
905
|
if (patch.provider !== undefined && record.payload.model_provider !== patch.provider) {
|
|
435
906
|
record.payload.model_provider = patch.provider;
|
|
@@ -439,40 +910,85 @@ function updateSessionMeta(path: string, expectedId: string, patch: { provider?:
|
|
|
439
910
|
record.payload.source = patch.source;
|
|
440
911
|
changed = true;
|
|
441
912
|
}
|
|
442
|
-
|
|
913
|
+
const strictRestore = options.expectedFileIdentity !== undefined;
|
|
914
|
+
if (strictRestore) {
|
|
915
|
+
if (historyFileIdentity(path) !== options.expectedFileIdentity) {
|
|
916
|
+
return { changed: false, durableProvider: false, conflict: true };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
let appended: Buffer | null = null;
|
|
920
|
+
let beforeSize = 0;
|
|
921
|
+
if (changed) {
|
|
922
|
+
beforeSize = statSync(path).size;
|
|
923
|
+
beforeStrictHistoryRolloutAppendForTests?.();
|
|
924
|
+
record.timestamp = new Date().toISOString();
|
|
925
|
+
appended = appendRolloutLine(path, JSON.stringify(record));
|
|
926
|
+
afterStrictHistoryRolloutAppendForTests?.();
|
|
927
|
+
let cleanAppend = false;
|
|
928
|
+
try {
|
|
929
|
+
const raw = readFileSync(path);
|
|
930
|
+
cleanAppend = raw.length === beforeSize + appended.length
|
|
931
|
+
&& raw.subarray(beforeSize).equals(appended);
|
|
932
|
+
} catch {
|
|
933
|
+
cleanAppend = false;
|
|
934
|
+
}
|
|
935
|
+
if (!cleanAppend) {
|
|
936
|
+
compensateConcurrentSessionMetaAppend(path, expectedId, appended, beforeSize);
|
|
937
|
+
return { changed: true, durableProvider: false, conflict: true };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let firstLine: FirstLineProviderResult = "current";
|
|
942
|
+
if (patch.provider !== undefined) {
|
|
943
|
+
try {
|
|
944
|
+
firstLine = patchFirstLineProviderInPlace(path, expectedId, patch.provider);
|
|
945
|
+
} catch {
|
|
946
|
+
firstLine = "unsafe";
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (options.requireDurableProvider && firstLine === "unsafe") {
|
|
950
|
+
// Restore the pre-operation last-writer-wins tuple after an append succeeded but the
|
|
951
|
+
// first-line durability repair failed. The extra lines remain audit evidence; the manifest
|
|
952
|
+
// remains authoritative and the retry cannot mistake this for convergence.
|
|
953
|
+
if (appended) appendRolloutLine(path, previousRecord);
|
|
954
|
+
return {
|
|
955
|
+
changed: changed || historyFileIdentity(path) !== options.expectedFileIdentity,
|
|
956
|
+
durableProvider: false,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
return {
|
|
960
|
+
changed: changed || firstLine === "patched",
|
|
961
|
+
durableProvider: firstLine !== "unsafe",
|
|
962
|
+
};
|
|
963
|
+
}
|
|
443
964
|
|
|
444
965
|
// Cover Codex's *other* provider reader: `read_session_meta_line` reads only line 1, and the
|
|
445
966
|
// app clones it when writing later git/memory-mode metadata. Appending alone leaves a stale
|
|
446
|
-
// line-1 provider that the clone would re-append
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
// SQLite replay path.
|
|
967
|
+
// line-1 provider that the clone would re-append. Exact restore requires this repair before it
|
|
968
|
+
// may update SQLite or consume the only provenance manifest; forward routing remains best-effort.
|
|
969
|
+
let firstLine: FirstLineProviderResult = "current";
|
|
450
970
|
if (patch.provider !== undefined) {
|
|
451
|
-
try {
|
|
971
|
+
try {
|
|
972
|
+
firstLine = patchFirstLineProviderInPlace(path, expectedId, patch.provider);
|
|
973
|
+
} catch {
|
|
974
|
+
firstLine = "unsafe";
|
|
975
|
+
}
|
|
976
|
+
if (options.requireDurableProvider && firstLine === "unsafe") {
|
|
977
|
+
return { changed: false, durableProvider: false };
|
|
978
|
+
}
|
|
452
979
|
}
|
|
453
980
|
|
|
454
|
-
|
|
981
|
+
const firstLineChanged = firstLine === "patched";
|
|
982
|
+
if (!changed) return { changed: firstLineChanged, durableProvider: firstLine !== "unsafe" };
|
|
983
|
+
|
|
984
|
+
// Forward/legacy mode remains best-effort. Strict manifest restore uses the CAS-style append
|
|
985
|
+
// branch above so a concurrent same-id provider decision cannot be overwritten.
|
|
455
986
|
record.timestamp = new Date().toISOString();
|
|
456
987
|
appendRolloutLine(path, JSON.stringify(record));
|
|
457
|
-
return true;
|
|
988
|
+
return { changed: true, durableProvider: firstLine !== "unsafe" };
|
|
458
989
|
}
|
|
459
990
|
|
|
460
|
-
function
|
|
461
|
-
if (entry.modelProvider !== "opencodex") {
|
|
462
|
-
return {
|
|
463
|
-
modelProvider: entry.modelProvider,
|
|
464
|
-
source: entry.source,
|
|
465
|
-
hasUserEvent: entry.hasUserEvent,
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
return {
|
|
469
|
-
modelProvider: "openai",
|
|
470
|
-
source: entry.source === "exec" ? "cli" : entry.source,
|
|
471
|
-
hasUserEvent: 1,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: number } {
|
|
991
|
+
function relabelAllRoutedHistoryToOpenai(db: Database): { rows: number; files: number } {
|
|
476
992
|
const rows = db
|
|
477
993
|
.query<ThreadRow, []>(`
|
|
478
994
|
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
@@ -488,9 +1004,9 @@ function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: nu
|
|
|
488
1004
|
if (updateSessionMeta(row.rollout_path, row.id, {
|
|
489
1005
|
provider: "openai",
|
|
490
1006
|
source: row.source === "exec" ? "cli" : undefined,
|
|
491
|
-
})) files++;
|
|
1007
|
+
}).changed) files++;
|
|
492
1008
|
} catch {
|
|
493
|
-
/*
|
|
1009
|
+
/* explicit legacy recovery still relabels the DB when an old rollout is missing */
|
|
494
1010
|
}
|
|
495
1011
|
}
|
|
496
1012
|
|
|
@@ -508,19 +1024,24 @@ function ejectRemainingOpencodexHistory(db: Database): { rows: number; files: nu
|
|
|
508
1024
|
return { rows: rows.length, files };
|
|
509
1025
|
}
|
|
510
1026
|
|
|
511
|
-
export function
|
|
1027
|
+
export function classifyRecoverableHistoryError(error: unknown): CodexHistoryFailureReason | null {
|
|
512
1028
|
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
513
1029
|
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
514
|
-
|
|
1030
|
+
if (code === "SQLITE_BUSY"
|
|
515
1031
|
|| code === "SQLITE_LOCKED"
|
|
516
1032
|
|| code === "EBUSY"
|
|
517
|
-
|| code === "EPERM"
|
|
518
|
-
|| code === "EACCES"
|
|
519
1033
|
|| message.includes("database is locked")
|
|
520
1034
|
|| message.includes("database is busy")
|
|
521
|
-
|| message.includes("resource busy")
|
|
1035
|
+
|| message.includes("resource busy")) return "busy";
|
|
1036
|
+
if (code === "EPERM"
|
|
1037
|
+
|| code === "EACCES"
|
|
522
1038
|
|| message.includes("operation not permitted")
|
|
523
|
-
|| message.includes("permission denied");
|
|
1039
|
+
|| message.includes("permission denied")) return "permission";
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function isRecoverableHistoryError(error: unknown): boolean {
|
|
1044
|
+
return classifyRecoverableHistoryError(error) !== null;
|
|
524
1045
|
}
|
|
525
1046
|
|
|
526
1047
|
const HISTORY_RETRY_DELAY_MS = 500;
|
|
@@ -533,24 +1054,34 @@ const HISTORY_RETRY_ATTEMPTS = 2;
|
|
|
533
1054
|
* error — callers surface that as `failed: true` instead of a silent no-op. Hard errors
|
|
534
1055
|
* (corruption, programming bugs) still throw.
|
|
535
1056
|
*/
|
|
536
|
-
|
|
1057
|
+
function withHistoryRetryResult<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}):
|
|
1058
|
+
| { ok: true; value: T }
|
|
1059
|
+
| { ok: false; reason: CodexHistoryFailureReason } {
|
|
537
1060
|
const sleepFn = io.sleepFn ?? Bun.sleepSync;
|
|
538
1061
|
const attempts = Math.max(1, io.attempts ?? HISTORY_RETRY_ATTEMPTS);
|
|
539
1062
|
const delayMs = io.delayMs ?? HISTORY_RETRY_DELAY_MS;
|
|
540
1063
|
for (let attempt = 0; ; attempt++) {
|
|
541
1064
|
try {
|
|
542
|
-
return fn();
|
|
1065
|
+
return { ok: true, value: fn() };
|
|
543
1066
|
} catch (error) {
|
|
544
|
-
|
|
545
|
-
if (
|
|
1067
|
+
const reason = classifyRecoverableHistoryError(error);
|
|
1068
|
+
if (!reason) throw error;
|
|
1069
|
+
if (attempt >= attempts - 1) return { ok: false, reason };
|
|
546
1070
|
try { sleepFn(delayMs); } catch { /* sleep is best-effort */ }
|
|
547
1071
|
}
|
|
548
1072
|
}
|
|
549
1073
|
}
|
|
550
1074
|
|
|
1075
|
+
export function withHistoryRetry<T>(fn: () => T, io: { sleepFn?: (ms: number) => void; attempts?: number; delayMs?: number } = {}): T | null {
|
|
1076
|
+
const result = withHistoryRetryResult(fn, io);
|
|
1077
|
+
return result.ok ? result.value : null;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
551
1080
|
/**
|
|
552
|
-
* True when a READONLY probe proves the
|
|
553
|
-
*
|
|
1081
|
+
* True when a READONLY probe proves the native-direction restore would be a no-op:
|
|
1082
|
+
* the history database is readable and the backup manifest has no restore entries. Bare
|
|
1083
|
+
* opencodex-tagged rows are not actionable: without a manifest their original provider is
|
|
1084
|
+
* unknown, so only the explicit legacy recovery command may relabel them. Used to skip the
|
|
554
1085
|
* write-open entirely in the Design B steady state — on Windows the Codex app holds
|
|
555
1086
|
* `state_5.sqlite` (WAL, busy_timeout 5s), so an unnecessary write open can stall for
|
|
556
1087
|
* seconds and surface a false lock warning, while WAL always admits readers. A failed
|
|
@@ -558,14 +1089,16 @@ export function withHistoryRetry<T>(fn: () => T, io: { sleepFn?: (ms: number) =>
|
|
|
558
1089
|
* to the write attempt and keep today's behavior for genuinely unknown state.
|
|
559
1090
|
*/
|
|
560
1091
|
function openaiRestoreIsNoop(stateDbPath: string, backupPath: string): boolean {
|
|
561
|
-
const pending = countPendingOpencodexHistory(stateDbPath, backupPath
|
|
1092
|
+
const pending = countPendingOpencodexHistory(stateDbPath, backupPath, {
|
|
1093
|
+
validateRestoreTargets: false,
|
|
1094
|
+
});
|
|
562
1095
|
return !pending.failed && pending.pendingRows === 0 && pending.backupEntries === 0;
|
|
563
1096
|
}
|
|
564
1097
|
|
|
565
1098
|
export function syncCodexHistoryProvider(
|
|
566
1099
|
provider: CodexHistoryProvider,
|
|
567
|
-
stateDbPath =
|
|
568
|
-
backupPath =
|
|
1100
|
+
stateDbPath = resolveCodexStateDbPath(),
|
|
1101
|
+
backupPath = historyBackupPathFor(stateDbPath),
|
|
569
1102
|
opts: { skipWhenProvablyNoop?: boolean } = {},
|
|
570
1103
|
): CodexHistorySyncResult {
|
|
571
1104
|
// Opt-in steady-state gate (Design B loopback callers only): default semantics of
|
|
@@ -574,28 +1107,41 @@ export function syncCodexHistoryProvider(
|
|
|
574
1107
|
&& openaiRestoreIsNoop(stateDbPath, backupPath)) {
|
|
575
1108
|
return { rows: 0, files: 0 };
|
|
576
1109
|
}
|
|
577
|
-
|
|
578
|
-
|
|
1110
|
+
try {
|
|
1111
|
+
const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe(provider, stateDbPath, backupPath));
|
|
1112
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
if (error instanceof CodexHistoryIntegrityError) {
|
|
1115
|
+
return integrityFailureResult(error);
|
|
1116
|
+
}
|
|
1117
|
+
throw error;
|
|
1118
|
+
}
|
|
579
1119
|
}
|
|
580
1120
|
|
|
581
1121
|
function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbPath: string, backupPath: string): CodexHistorySyncResult {
|
|
582
|
-
if (!existsSync(stateDbPath))
|
|
1122
|
+
if (!existsSync(stateDbPath)) {
|
|
1123
|
+
const backup = readBackup(backupPath, stateDbPath);
|
|
1124
|
+
if (provider === "openai" && Object.keys(backup.manifest.entries).length > 0) {
|
|
1125
|
+
throw new CodexHistoryIntegrityError("history_state_database_missing");
|
|
1126
|
+
}
|
|
1127
|
+
return { rows: 0, files: 0 };
|
|
1128
|
+
}
|
|
583
1129
|
if (provider === "openai") return restoreCodexHistoryProvider(stateDbPath, backupPath);
|
|
584
1130
|
|
|
585
1131
|
const db = openStateDb(stateDbPath);
|
|
586
1132
|
try {
|
|
587
|
-
const placeholders =
|
|
1133
|
+
const placeholders = CODEX_HISTORY_RESUMABLE_SOURCES.map(() => "?").join(",");
|
|
588
1134
|
const openaiRows = db
|
|
589
|
-
.query<
|
|
590
|
-
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
1135
|
+
.query<ApplyRowSnapshot, string[]>(`
|
|
1136
|
+
SELECT id, rollout_path, model_provider, source, has_user_event, first_user_message
|
|
591
1137
|
FROM threads
|
|
592
1138
|
WHERE model_provider = 'openai'
|
|
593
1139
|
AND source IN (${placeholders})
|
|
594
1140
|
`)
|
|
595
|
-
.all(...
|
|
1141
|
+
.all(...CODEX_HISTORY_RESUMABLE_SOURCES);
|
|
596
1142
|
const execRows = db
|
|
597
|
-
.query<
|
|
598
|
-
SELECT id, rollout_path, model_provider, source, has_user_event
|
|
1143
|
+
.query<ApplyRowSnapshot, []>(`
|
|
1144
|
+
SELECT id, rollout_path, model_provider, source, has_user_event, first_user_message
|
|
599
1145
|
FROM threads
|
|
600
1146
|
WHERE model_provider = 'opencodex'
|
|
601
1147
|
AND source = 'exec'
|
|
@@ -603,49 +1149,92 @@ function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbP
|
|
|
603
1149
|
`)
|
|
604
1150
|
.all();
|
|
605
1151
|
|
|
606
|
-
const manifest = readBackup(backupPath, stateDbPath);
|
|
1152
|
+
const manifest = readBackup(backupPath, stateDbPath).manifest;
|
|
607
1153
|
for (const row of [...openaiRows, ...execRows]) rememberOriginal(manifest, row);
|
|
608
1154
|
writeBackup(backupPath, manifest, stateDbPath);
|
|
609
1155
|
|
|
610
1156
|
let files = 0;
|
|
611
|
-
for (const row of openaiRows) {
|
|
612
|
-
try {
|
|
613
|
-
if (updateSessionMeta(row.rollout_path, row.id, { provider: "opencodex" })) files++;
|
|
614
|
-
} catch {
|
|
615
|
-
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
for (const row of execRows) {
|
|
619
|
-
try {
|
|
620
|
-
if (updateSessionMeta(row.rollout_path, row.id, { source: "cli" })) files++;
|
|
621
|
-
} catch {
|
|
622
|
-
/* best-effort; keep DB migration moving even if one old rollout is malformed */
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
1157
|
const update = db.transaction(() => {
|
|
627
|
-
const
|
|
1158
|
+
const routeOpenai = db.query(`
|
|
628
1159
|
UPDATE threads
|
|
629
|
-
SET
|
|
1160
|
+
SET model_provider = 'opencodex',
|
|
1161
|
+
has_user_event = ?
|
|
630
1162
|
WHERE id = ?
|
|
631
|
-
AND
|
|
1163
|
+
AND rollout_path = ?
|
|
1164
|
+
AND model_provider = ?
|
|
1165
|
+
AND source = ?
|
|
1166
|
+
AND has_user_event = ?
|
|
1167
|
+
AND first_user_message IS ?
|
|
632
1168
|
`);
|
|
633
|
-
|
|
634
|
-
db.query(`
|
|
1169
|
+
const routeExec = db.query(`
|
|
635
1170
|
UPDATE threads
|
|
636
|
-
SET
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
AND source = 'exec'
|
|
1171
|
+
SET source = 'cli',
|
|
1172
|
+
has_user_event = 1
|
|
1173
|
+
WHERE id = ?
|
|
1174
|
+
AND rollout_path = ?
|
|
1175
|
+
AND model_provider = ?
|
|
1176
|
+
AND source = ?
|
|
1177
|
+
AND has_user_event = ?
|
|
1178
|
+
AND first_user_message IS ?
|
|
645
1179
|
AND trim(coalesce(first_user_message, '')) != ''
|
|
646
|
-
`)
|
|
1180
|
+
`);
|
|
1181
|
+
// CAS only the rows that were recorded in this manifest. A thread inserted after the
|
|
1182
|
+
// snapshot must stay native rather than becoming an untracked bare routed row.
|
|
1183
|
+
for (const row of openaiRows) {
|
|
1184
|
+
const targetEvent = hasFirstUserMessage(row.first_user_message) ? 1 : row.has_user_event;
|
|
1185
|
+
const result = routeOpenai.run(
|
|
1186
|
+
targetEvent,
|
|
1187
|
+
row.id,
|
|
1188
|
+
row.rollout_path,
|
|
1189
|
+
row.model_provider,
|
|
1190
|
+
row.source,
|
|
1191
|
+
row.has_user_event,
|
|
1192
|
+
row.first_user_message,
|
|
1193
|
+
);
|
|
1194
|
+
if (result.changes !== 1) {
|
|
1195
|
+
throw new CodexHistoryIntegrityError("history_apply_database_changed_during_route");
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
for (const row of execRows) {
|
|
1199
|
+
const result = routeExec.run(
|
|
1200
|
+
row.id,
|
|
1201
|
+
row.rollout_path,
|
|
1202
|
+
row.model_provider,
|
|
1203
|
+
row.source,
|
|
1204
|
+
row.has_user_event,
|
|
1205
|
+
row.first_user_message,
|
|
1206
|
+
);
|
|
1207
|
+
if (result.changes !== 1) {
|
|
1208
|
+
throw new CodexHistoryIntegrityError("history_apply_database_changed_during_route");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// File metadata remains best-effort, but only after every database CAS matched. Thus a
|
|
1213
|
+
// stale snapshot or a newly inserted row cannot be routed before its provenance exists.
|
|
1214
|
+
for (const row of openaiRows) {
|
|
1215
|
+
try {
|
|
1216
|
+
if (updateSessionMeta(row.rollout_path, row.id, { provider: "opencodex" }).changed) files++;
|
|
1217
|
+
} catch {
|
|
1218
|
+
/* keep DB migration moving; the manifest still carries exact original metadata */
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
for (const row of execRows) {
|
|
1222
|
+
try {
|
|
1223
|
+
if (updateSessionMeta(row.rollout_path, row.id, { source: "cli" }).changed) files++;
|
|
1224
|
+
} catch {
|
|
1225
|
+
/* keep DB migration moving; the manifest still carries exact original metadata */
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
647
1228
|
});
|
|
648
|
-
|
|
1229
|
+
try {
|
|
1230
|
+
beforeHistoryApplyTransactionForTests?.();
|
|
1231
|
+
update();
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
if (files > 0) {
|
|
1234
|
+
throw new CodexHistoryIntegrityError("history_apply_partial_route", { rows: 0, files });
|
|
1235
|
+
}
|
|
1236
|
+
throw error;
|
|
1237
|
+
}
|
|
649
1238
|
|
|
650
1239
|
return { rows: openaiRows.length + execRows.length, files };
|
|
651
1240
|
} finally {
|
|
@@ -654,26 +1243,24 @@ function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbP
|
|
|
654
1243
|
}
|
|
655
1244
|
|
|
656
1245
|
function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): CodexHistorySyncResult {
|
|
657
|
-
const
|
|
1246
|
+
const backup = readBackup(backupPath, stateDbPath);
|
|
1247
|
+
const manifest = backup.manifest;
|
|
658
1248
|
const entries = Object.values(manifest.entries);
|
|
659
1249
|
|
|
660
1250
|
const db = openStateDb(stateDbPath);
|
|
661
1251
|
try {
|
|
662
|
-
if (entries.length === 0) {
|
|
663
|
-
const ejected = ejectRemainingOpencodexHistory(db);
|
|
664
|
-
return ejected.rows > 0 ? { rows: 0, files: ejected.files, ejectedRows: ejected.rows } : { rows: 0, files: 0 };
|
|
665
|
-
}
|
|
1252
|
+
if (entries.length === 0) return { rows: 0, files: 0 };
|
|
666
1253
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
1254
|
+
// Validate the whole manifest-to-database target set before touching a rollout. Only the
|
|
1255
|
+
// OpenCodex post-image (or an already-restored target from an interrupted retry) is owned by
|
|
1256
|
+
// this manifest. Any other tuple is a newer/foreign provider decision and must win.
|
|
1257
|
+
const current = db.query<RestoreRowSnapshot, [string]>(`
|
|
1258
|
+
SELECT id, rollout_path, model_provider, source, has_user_event, first_user_message
|
|
1259
|
+
FROM threads WHERE id = ?
|
|
1260
|
+
`);
|
|
1261
|
+
const { snapshots, rolloutSnapshots } = preflightRestoreTargets(id => current.get(id), entries);
|
|
676
1262
|
|
|
1263
|
+
let files = 0;
|
|
677
1264
|
const restore = db.transaction(() => {
|
|
678
1265
|
const update = db.query(`
|
|
679
1266
|
UPDATE threads
|
|
@@ -681,96 +1268,290 @@ function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): C
|
|
|
681
1268
|
source = ?,
|
|
682
1269
|
has_user_event = ?
|
|
683
1270
|
WHERE id = ?
|
|
1271
|
+
AND rollout_path = ?
|
|
1272
|
+
AND model_provider = ?
|
|
1273
|
+
AND source = ?
|
|
1274
|
+
AND has_user_event = ?
|
|
1275
|
+
AND first_user_message IS ?
|
|
684
1276
|
`);
|
|
685
1277
|
for (const entry of entries) {
|
|
686
|
-
const
|
|
687
|
-
|
|
1278
|
+
const before = snapshots.get(entry.id);
|
|
1279
|
+
if (!before) throw new CodexHistoryIntegrityError("history_backup_snapshot_missing");
|
|
1280
|
+
const result = update.run(
|
|
1281
|
+
entry.modelProvider,
|
|
1282
|
+
entry.source,
|
|
1283
|
+
entry.hasUserEvent,
|
|
1284
|
+
entry.id,
|
|
1285
|
+
before.rollout_path,
|
|
1286
|
+
before.model_provider,
|
|
1287
|
+
before.source,
|
|
1288
|
+
before.has_user_event,
|
|
1289
|
+
before.first_user_message,
|
|
1290
|
+
);
|
|
1291
|
+
if (result.changes !== 1) {
|
|
1292
|
+
throw new CodexHistoryIntegrityError("history_backup_database_changed_during_restore");
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// Only after every database CAS matched may a rollout move. Keeping the SQLite
|
|
1296
|
+
// transaction open means a file-side refusal rolls the database back, while the manifest
|
|
1297
|
+
// remains the durable retry journal for an exceptional I/O failure.
|
|
1298
|
+
for (const entry of entries) {
|
|
1299
|
+
const before = rolloutSnapshots.get(entry.id);
|
|
1300
|
+
if (!before) throw new CodexHistoryIntegrityError("history_backup_rollout_snapshot_missing");
|
|
1301
|
+
let updated: SessionMetaUpdateResult;
|
|
1302
|
+
try {
|
|
1303
|
+
updated = updateSessionMeta(
|
|
1304
|
+
entry.rolloutPath,
|
|
1305
|
+
entry.id,
|
|
1306
|
+
{ provider: entry.modelProvider, source: entry.source },
|
|
1307
|
+
{
|
|
1308
|
+
requireDurableProvider: true,
|
|
1309
|
+
expectedFileIdentity: before.identity,
|
|
1310
|
+
expectedLatestProvider: before.latestProvider,
|
|
1311
|
+
expectedLatestSource: before.latestSource,
|
|
1312
|
+
},
|
|
1313
|
+
);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
if (historyFileIdentity(entry.rolloutPath) !== before.identity) files++;
|
|
1316
|
+
throw error;
|
|
1317
|
+
}
|
|
1318
|
+
if (updated.changed) files++;
|
|
1319
|
+
if (updated.conflict) {
|
|
1320
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_changed_during_restore");
|
|
1321
|
+
}
|
|
1322
|
+
if (!updated.durableProvider) {
|
|
1323
|
+
throw new CodexHistoryIntegrityError("history_backup_rollout_unrestorable");
|
|
1324
|
+
}
|
|
688
1325
|
}
|
|
689
1326
|
});
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1327
|
+
try {
|
|
1328
|
+
restore();
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (files > 0) {
|
|
1331
|
+
throw new CodexHistoryIntegrityError("history_backup_partial_restore", { rows: 0, files });
|
|
1332
|
+
}
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
const getCurrent = (id: string) => current.get(id);
|
|
1338
|
+
assertRestoreReadback(getCurrent, entries);
|
|
1339
|
+
beforeHistoryBackupConsumeForTests?.();
|
|
1340
|
+
// The hook models the exact last-moment race: neither a newer database decision nor a
|
|
1341
|
+
// same-id foreign session_meta may be hidden by deleting the only provenance manifest.
|
|
1342
|
+
assertRestoreReadback(getCurrent, entries);
|
|
1343
|
+
consumeBackupIfUnchanged(backupPath, stateDbPath, backup.fingerprint);
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
if (error instanceof CodexHistoryIntegrityError) {
|
|
1346
|
+
throw new CodexHistoryIntegrityError(error.message, { rows: entries.length, files });
|
|
1347
|
+
}
|
|
1348
|
+
const failureReason = classifyRecoverableHistoryError(error);
|
|
1349
|
+
if (failureReason) {
|
|
1350
|
+
return {
|
|
1351
|
+
rows: entries.length,
|
|
1352
|
+
files,
|
|
1353
|
+
failed: true,
|
|
1354
|
+
failureReason,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
// Once exact targets were written, an unclassified finalization failure is an
|
|
1358
|
+
// applied-but-not-converged integrity state. Preserve that progress instead of
|
|
1359
|
+
// reporting a zero-change failure that invites an unsafe blind retry.
|
|
1360
|
+
throw new CodexHistoryIntegrityError("history_backup_finalization_failed", {
|
|
1361
|
+
rows: entries.length,
|
|
1362
|
+
files,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
return { rows: entries.length, files };
|
|
696
1366
|
} finally {
|
|
697
1367
|
db.close();
|
|
698
1368
|
}
|
|
699
1369
|
}
|
|
700
1370
|
|
|
701
|
-
export function restoreLegacyOpenaiHistory(stateDbPath =
|
|
1371
|
+
export function restoreLegacyOpenaiHistory(stateDbPath = resolveCodexStateDbPath()): CodexHistorySyncResult {
|
|
702
1372
|
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
703
|
-
|
|
1373
|
+
const retried = withHistoryRetryResult(() => {
|
|
704
1374
|
const db = openStateDb(stateDbPath);
|
|
705
1375
|
try {
|
|
706
|
-
return
|
|
1376
|
+
return relabelAllRoutedHistoryToOpenai(db);
|
|
707
1377
|
} finally {
|
|
708
1378
|
db.close();
|
|
709
1379
|
}
|
|
710
|
-
})
|
|
1380
|
+
});
|
|
1381
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
711
1382
|
}
|
|
712
1383
|
|
|
713
1384
|
/**
|
|
714
|
-
* One-time Design-B migration: restore backed
|
|
715
|
-
* opencodex-tagged threads
|
|
1385
|
+
* One-time Design-B migration: restore only manifest-backed originals. Untracked
|
|
1386
|
+
* opencodex-tagged threads have unknown provider provenance and remain routed unless the
|
|
1387
|
+
* user explicitly invokes legacy OpenAI recovery. Thin wrapper over the restore path with a
|
|
716
1388
|
* configurable retry budget — the daemon migration guardian uses `{ attempts: 1 }`
|
|
717
1389
|
* per tick so a locked DB never stalls the event loop beyond one sqlite busy wait.
|
|
718
1390
|
*/
|
|
719
1391
|
export function migrateHistoryToOpenai(
|
|
720
|
-
stateDbPath =
|
|
721
|
-
backupPath =
|
|
1392
|
+
stateDbPath = resolveCodexStateDbPath(),
|
|
1393
|
+
backupPath = historyBackupPathFor(stateDbPath),
|
|
722
1394
|
opts: { attempts?: number; delayMs?: number; sleepFn?: (ms: number) => void } = {},
|
|
723
1395
|
): CodexHistorySyncResult {
|
|
724
|
-
if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
|
|
725
1396
|
// Steady-state gate: this migration is Design-B-specific (inject + guardian callers),
|
|
726
1397
|
// and after the one-time migration every start would otherwise write-open the DB for
|
|
727
1398
|
// nothing. A missing DB with a leftover backup manifest does NOT satisfy the gate
|
|
728
1399
|
// (backupEntries > 0), so the guardian's fresh-reinstall re-count protection holds.
|
|
729
1400
|
if (openaiRestoreIsNoop(stateDbPath, backupPath)) return { rows: 0, files: 0 };
|
|
730
|
-
|
|
731
|
-
|
|
1401
|
+
try {
|
|
1402
|
+
const retried = withHistoryRetryResult(() => syncCodexHistoryProviderUnsafe("openai", stateDbPath, backupPath), opts);
|
|
1403
|
+
return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
if (error instanceof CodexHistoryIntegrityError) {
|
|
1406
|
+
return integrityFailureResult(error);
|
|
1407
|
+
}
|
|
1408
|
+
throw error;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Captures no-op evidence while the caller holds the history serialization
|
|
1414
|
+
* lock H. This function does not acquire H itself. Unknown or foreign backup
|
|
1415
|
+
* state is never collapsed into an empty manifest.
|
|
1416
|
+
*/
|
|
1417
|
+
export function snapshotCodexHistoryNoop(
|
|
1418
|
+
stateDbPath: string,
|
|
1419
|
+
backupPath: string,
|
|
1420
|
+
): CodexHistoryNoopSnapshot {
|
|
1421
|
+
const canonicalStateDbPath = resolve(stateDbPath);
|
|
1422
|
+
const canonicalBackupPath = resolve(backupPath);
|
|
1423
|
+
const stateDbPresent = existsSync(stateDbPath);
|
|
1424
|
+
const backupPresent = existsSync(backupPath);
|
|
1425
|
+
const base = { canonicalStateDbPath, stateDbPresent, canonicalBackupPath, backupPresent };
|
|
1426
|
+
if (!sameCodexHistoryPath(backupPath, historyBackupPathFor(stateDbPath))) {
|
|
1427
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "backup-path" };
|
|
1428
|
+
}
|
|
1429
|
+
const backup = inspectBackupForNoop(backupPath, stateDbPath);
|
|
1430
|
+
if (backup.kind === "unknown") {
|
|
1431
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: backup.reason };
|
|
1432
|
+
}
|
|
1433
|
+
if (!stateDbPresent) {
|
|
1434
|
+
return backup.entries > 0
|
|
1435
|
+
? { kind: "work-pending", pendingRows: 0, backupEntries: backup.entries, ...base }
|
|
1436
|
+
: { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "database-absent" };
|
|
1437
|
+
}
|
|
1438
|
+
const stateDbIdentity = historyFileIdentity(stateDbPath);
|
|
1439
|
+
if (stateDbIdentity === null) {
|
|
1440
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "snapshot-race" };
|
|
1441
|
+
}
|
|
1442
|
+
let monitor: Database | undefined;
|
|
1443
|
+
try {
|
|
1444
|
+
monitor = new Database(stateDbPath, { readonly: true });
|
|
1445
|
+
monitor.exec("PRAGMA busy_timeout = 100");
|
|
1446
|
+
const dataVersionBefore = readHistoryDataVersion(monitor);
|
|
1447
|
+
if (dataVersionBefore === null) {
|
|
1448
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "database-query" };
|
|
1449
|
+
}
|
|
1450
|
+
const pending = countPendingOpencodexHistory(stateDbPath, backupPath, {
|
|
1451
|
+
validateRestoreTargets: false,
|
|
1452
|
+
});
|
|
1453
|
+
if (pending.failed) {
|
|
1454
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "database-query" };
|
|
1455
|
+
}
|
|
1456
|
+
afterNoopPendingCountForTests?.();
|
|
1457
|
+
const backupAfter = inspectBackupForNoop(backupPath, stateDbPath);
|
|
1458
|
+
if (backupAfter.kind === "unknown") {
|
|
1459
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: backupAfter.reason };
|
|
1460
|
+
}
|
|
1461
|
+
const dataVersionAfter = readHistoryDataVersion(monitor);
|
|
1462
|
+
if (dataVersionAfter === null) {
|
|
1463
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "database-query" };
|
|
1464
|
+
}
|
|
1465
|
+
if (dataVersionAfter !== dataVersionBefore
|
|
1466
|
+
|| pending.backupEntries !== backup.entries
|
|
1467
|
+
|| backupAfter.entries !== backup.entries
|
|
1468
|
+
|| backupAfter.present !== backup.present
|
|
1469
|
+
|| backupAfter.fingerprint !== backup.fingerprint
|
|
1470
|
+
|| historyFileIdentity(stateDbPath) !== stateDbIdentity
|
|
1471
|
+
|| existsSync(stateDbPath) !== stateDbPresent
|
|
1472
|
+
|| existsSync(backupPath) !== backupPresent) {
|
|
1473
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "snapshot-race" };
|
|
1474
|
+
}
|
|
1475
|
+
return pending.pendingRows === 0 && backup.entries === 0
|
|
1476
|
+
? { kind: "verified-noop", pendingRows: 0, backupEntries: 0, ...base, stateDbPresent: true }
|
|
1477
|
+
: { kind: "work-pending", pendingRows: pending.pendingRows, backupEntries: backup.entries, ...base };
|
|
1478
|
+
} catch {
|
|
1479
|
+
return { kind: "unknown", pendingRows: null, backupEntries: null, ...base, reason: "database-query" };
|
|
1480
|
+
} finally {
|
|
1481
|
+
try {
|
|
1482
|
+
monitor?.close();
|
|
1483
|
+
} catch {
|
|
1484
|
+
// Read-only monitor cleanup cannot make an uncertain snapshot authoritative.
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
732
1487
|
}
|
|
733
1488
|
|
|
734
1489
|
export interface PendingHistoryCount {
|
|
735
|
-
/**
|
|
1490
|
+
/** Compatibility field; bare routed rows are never automatic restore work. */
|
|
736
1491
|
pendingRows: number;
|
|
737
1492
|
/** Entries still recorded in the backup manifest (restore targets). */
|
|
738
1493
|
backupEntries: number;
|
|
739
|
-
/** Set when the DB could not be
|
|
1494
|
+
/** Set when the DB/manifest could not be read or their bound identity is invalid. */
|
|
740
1495
|
failed?: true;
|
|
1496
|
+
/** Distinguishes retryable contention/access from a manifest or target that needs review. */
|
|
1497
|
+
failureReason?: CodexHistoryFailureReason;
|
|
741
1498
|
}
|
|
742
1499
|
|
|
743
1500
|
/**
|
|
744
1501
|
* Read-only migration progress probe for the guardian and `ocx doctor`. Opens sqlite
|
|
745
|
-
* readonly with a SHORT busy timeout so a locked DB cannot stall a daemon tick.
|
|
746
|
-
*
|
|
747
|
-
*
|
|
1502
|
+
* readonly with a SHORT busy timeout so a locked DB cannot stall a daemon tick. Only a
|
|
1503
|
+
* valid, database-bound backup manifest is actionable work; bare routed rows remain
|
|
1504
|
+
* untouched because their original provider is not known. Operator diagnostics keep the
|
|
1505
|
+
* default deep rollout validation. Recurring no-op probes explicitly opt out because any
|
|
1506
|
+
* nonempty manifest already prevents a no-op and the mutation path always preflights files.
|
|
748
1507
|
*/
|
|
749
|
-
export function countPendingOpencodexHistory(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1508
|
+
export function countPendingOpencodexHistory(
|
|
1509
|
+
stateDbPath = resolveCodexStateDbPath(),
|
|
1510
|
+
backupPath = historyBackupPathFor(stateDbPath),
|
|
1511
|
+
opts: { validateRestoreTargets?: boolean } = {},
|
|
1512
|
+
): PendingHistoryCount {
|
|
1513
|
+
const backup = readBackupStrict(backupPath, stateDbPath);
|
|
1514
|
+
if (backup.kind === "unknown") {
|
|
1515
|
+
return {
|
|
1516
|
+
pendingRows: 0,
|
|
1517
|
+
backupEntries: 0,
|
|
1518
|
+
failed: true,
|
|
1519
|
+
failureReason: backup.failureReason ?? "integrity",
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const entries = Object.values(backup.manifest.entries);
|
|
1523
|
+
const backupEntries = entries.length;
|
|
755
1524
|
|
|
756
|
-
if (!existsSync(stateDbPath))
|
|
1525
|
+
if (!existsSync(stateDbPath)) {
|
|
1526
|
+
return backupEntries > 0
|
|
1527
|
+
? { pendingRows: 0, backupEntries, failed: true, failureReason: "integrity" }
|
|
1528
|
+
: { pendingRows: 0, backupEntries };
|
|
1529
|
+
}
|
|
757
1530
|
try {
|
|
758
1531
|
const db = new Database(stateDbPath, { readonly: true });
|
|
759
1532
|
try {
|
|
760
1533
|
db.exec("PRAGMA busy_timeout = 100");
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1534
|
+
// Prove the expected history schema is readable without counting unowned routed rows.
|
|
1535
|
+
db.query("SELECT 1 FROM threads LIMIT 1").get();
|
|
1536
|
+
if (entries.length > 0) {
|
|
1537
|
+
const current = db.query<RestoreRowSnapshot, [string]>(`
|
|
1538
|
+
SELECT id, rollout_path, model_provider, source, has_user_event, first_user_message
|
|
1539
|
+
FROM threads WHERE id = ?
|
|
1540
|
+
`);
|
|
1541
|
+
if (opts.validateRestoreTargets === false) {
|
|
1542
|
+
preflightRestoreRows(id => current.get(id), entries);
|
|
1543
|
+
} else {
|
|
1544
|
+
preflightRestoreTargets(id => current.get(id), entries);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return { pendingRows: 0, backupEntries };
|
|
768
1548
|
} finally {
|
|
769
1549
|
db.close();
|
|
770
1550
|
}
|
|
771
1551
|
} catch (error) {
|
|
772
|
-
|
|
1552
|
+
const reason = classifyRecoverableHistoryError(error);
|
|
1553
|
+
if (reason) return { pendingRows: 0, backupEntries, failed: true, failureReason: reason };
|
|
773
1554
|
// Schema drift (e.g. a future codex renames the table) is a "cannot know" too, not a crash.
|
|
774
|
-
return { pendingRows: 0, backupEntries, failed: true };
|
|
1555
|
+
return { pendingRows: 0, backupEntries, failed: true, failureReason: "integrity" };
|
|
775
1556
|
}
|
|
776
1557
|
}
|