@iislee/opencodex 2.11.0 → 2.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_INSTALL.md +109 -0
- package/README.md +114 -19
- package/bin/ocx.mjs +164 -36
- package/bin/package-main.mjs +1 -1
- package/gui/dist/assets/index-BF38heuV.js +104 -0
- package/gui/dist/assets/index-DMiI18Kv.css +1 -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 +19 -10
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +376 -52
- package/src/adapters/base.ts +54 -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 +601 -0
- package/src/adapters/cursor/checkpoint-store.ts +303 -0
- package/src/adapters/cursor/cursor-errors.ts +129 -6
- package/src/adapters/cursor/discovery.ts +61 -4
- package/src/adapters/cursor/effort-map.ts +27 -3
- 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 +174 -58
- package/src/adapters/cursor/live-transport.ts +609 -170
- package/src/adapters/cursor/native-exec-common.ts +23 -2
- 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 +0 -1
- package/src/adapters/cursor/native-exec.ts +101 -14
- package/src/adapters/cursor/protobuf-events.ts +829 -11
- package/src/adapters/cursor/protobuf-request.ts +383 -65
- package/src/adapters/cursor/request-builder.ts +237 -31
- package/src/adapters/cursor/tool-definitions.ts +242 -12
- package/src/adapters/cursor/tool-result-normalize.ts +92 -0
- package/src/adapters/cursor/transport.ts +22 -0
- package/src/adapters/cursor/types.ts +28 -1
- package/src/adapters/cursor.ts +133 -7
- package/src/adapters/google-antigravity-replay.ts +558 -34
- package/src/adapters/google-antigravity-wire.ts +43 -10
- 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 +618 -74
- package/src/adapters/identity.ts +39 -6
- 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 +10 -1
- package/src/adapters/kiro.ts +92 -54
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +1311 -336
- package/src/adapters/openai-responses-url.ts +16 -0
- package/src/adapters/openai-responses.ts +830 -56
- package/src/adapters/registry.ts +175 -0
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/adapters/tool-call-id.ts +119 -0
- package/src/adapters/tool-catalog-nudge.ts +104 -21
- package/src/adapters/xai-web-search.ts +185 -0
- package/src/bridge.ts +428 -61
- package/src/chat/inbound.ts +43 -19
- package/src/chat/outbound.ts +82 -26
- package/src/claude/agents-inject.ts +32 -9
- package/src/claude/context-windows.ts +21 -5
- 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 +70 -16
- package/src/cli/account-api.ts +35 -1
- package/src/cli/account-auth.ts +33 -6
- package/src/cli/account-catalog-refresh.ts +14 -0
- package/src/cli/account-extended.ts +389 -2
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +30 -6
- package/src/cli/agent.ts +45 -1
- package/src/cli/claude-agent-startup-sync.ts +73 -0
- package/src/cli/claude-desktop.ts +29 -4
- package/src/cli/claude.ts +103 -34
- 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 +593 -0
- package/src/cli/doctor.ts +315 -43
- package/src/cli/ensure-desired-integrations.ts +152 -0
- package/src/cli/export-command.ts +46 -20
- package/src/cli/help.ts +20 -271
- package/src/cli/index.ts +378 -519
- package/src/cli/init.ts +4 -17
- 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 +35 -2
- package/src/cli/models.ts +100 -14
- package/src/cli/observe.ts +92 -3
- package/src/cli/opencode.ts +4 -2
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/provider.ts +24 -3
- package/src/cli/ready.ts +301 -0
- package/src/cli/registry.ts +437 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +10 -3
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/cli/v2.ts +105 -10
- package/src/cli.ts +1 -1
- package/src/clients/config-export.ts +1358 -21
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +130 -13
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -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 +493 -106
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/auth-api.ts +849 -242
- package/src/codex/auth-collision.ts +5 -3
- package/src/codex/auth-context.ts +345 -32
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +67 -0
- package/src/codex/catalog/aggregation.ts +68 -10
- package/src/codex/catalog/bundled.ts +331 -33
- package/src/codex/catalog/effort.ts +121 -30
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/kinds.ts +2 -0
- package/src/codex/catalog/metadata.ts +529 -45
- package/src/codex/catalog/native-models.ts +72 -0
- package/src/codex/catalog/parsing.ts +224 -30
- package/src/codex/catalog/provider-fetch.ts +1460 -134
- package/src/codex/catalog/sync.ts +1449 -186
- 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 +384 -0
- package/src/codex/convergence-types.ts +614 -0
- package/src/codex/convergence.ts +651 -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/features.ts +636 -39
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +407 -0
- package/src/codex/history-lock.ts +242 -0
- package/src/codex/history-migration-guardian.ts +26 -20
- package/src/codex/history-provider.ts +231 -28
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +220 -0
- package/src/codex/inject-coordination.ts +290 -0
- package/src/codex/inject.ts +1073 -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 +80 -0
- package/src/codex/journal.ts +66 -4
- package/src/codex/log-guard/inspect.ts +506 -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 +353 -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 +492 -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 +682 -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 +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +298 -0
- package/src/codex/quota.ts +175 -13
- package/src/codex/refresh.ts +11 -2
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/routing.ts +505 -94
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +1009 -28
- package/src/codex/subagent-model-fallback.ts +350 -35
- package/src/codex/sync.ts +191 -2
- package/src/codex/transition-state.ts +612 -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 +20 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/request.ts +32 -0
- package/src/combos/types.ts +81 -9
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +1762 -140
- package/src/generated/compatibility-version.json +3164 -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 +152 -29
- package/src/images/plan.ts +23 -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.ts +111 -0
- package/src/integrations/registry.ts +159 -0
- package/src/integrations/serialize.ts +314 -0
- package/src/integrations/state.ts +361 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer-lock.ts +98 -0
- package/src/integrations/writer.ts +691 -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/app-owned-memory-stores.ts +22 -0
- package/src/lib/bounded-body.ts +153 -9
- 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 +6 -2
- package/src/lib/destination-policy.ts +65 -1
- package/src/lib/errors.ts +44 -2
- 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/pinned-http.ts +145 -26
- package/src/lib/process-control.ts +4 -1
- 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/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 +202 -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 +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/command-code.ts +239 -0
- package/src/oauth/cursor.ts +46 -5
- package/src/oauth/google-antigravity.ts +35 -3
- package/src/oauth/health.ts +20 -12
- package/src/oauth/index.ts +398 -66
- package/src/oauth/key-providers.ts +16 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/local-token-detect.ts +11 -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/store.ts +119 -21
- package/src/oauth/token-guardian.ts +9 -3
- 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/base-url-choices.ts +10 -0
- package/src/providers/codex-capacity.ts +292 -0
- package/src/providers/command-code-efforts.ts +144 -0
- package/src/providers/context-cap.ts +22 -5
- package/src/providers/cursor-pool.ts +72 -0
- package/src/providers/derive.ts +253 -6
- package/src/providers/fastwire.ts +501 -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 +1 -1
- package/src/providers/model-discovery-limits.ts +16 -0
- package/src/providers/model-discovery.ts +115 -22
- package/src/providers/model-rename-migration.ts +255 -0
- package/src/providers/model-rename-startup.ts +28 -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 +29 -0
- package/src/providers/quota.ts +1319 -38
- package/src/providers/registry.ts +1429 -111
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +277 -0
- package/src/providers/slug-codec.ts +42 -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 +11 -4
- package/src/reasoning-effort.ts +49 -1
- package/src/responses/compaction.ts +26 -1
- package/src/responses/custom-tool-compat.ts +266 -0
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/namespace-tool-compat.ts +355 -0
- package/src/responses/parser.ts +220 -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 +426 -0
- package/src/responses/schema.ts +7 -1
- package/src/responses/spill-store.ts +75 -10
- package/src/responses/state.ts +565 -27
- package/src/responses/thought-signature-replay.ts +347 -0
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/responses/truncated-stop-reason.ts +60 -0
- package/src/router.ts +366 -30
- package/src/routing/analytics.ts +378 -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 +145 -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 +267 -46
- package/src/server/background-lifecycle.ts +182 -0
- package/src/server/chat-completions.ts +130 -56
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +426 -0
- package/src/server/claude-messages.ts +159 -43
- 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 +865 -181
- package/src/server/lifecycle.ts +284 -13
- 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 +398 -103
- package/src/server/management/api-key-usage.ts +31 -5
- package/src/server/management/body.ts +6 -0
- package/src/server/management/combo-routes.ts +62 -24
- package/src/server/management/config-routes.ts +464 -51
- package/src/server/management/context.ts +80 -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 +101 -32
- package/src/server/management/model-routes.ts +189 -131
- package/src/server/management/model-rows.ts +163 -0
- package/src/server/management/native-integration-routes.ts +769 -0
- package/src/server/management/oauth-account-routes.ts +80 -4
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +764 -157
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +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 +288 -32
- 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 +115 -14
- package/src/server/management-auth.ts +220 -5
- package/src/server/passive-route-linker.ts +66 -0
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +132 -5
- package/src/server/readiness.ts +99 -0
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +236 -76
- package/src/server/request-decompress.ts +113 -6
- package/src/server/request-log.ts +235 -22
- 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/collaboration.ts +204 -35
- package/src/server/responses/compact.ts +442 -55
- package/src/server/responses/core.ts +2872 -331
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/encrypted-payload.ts +62 -39
- package/src/server/responses/fetch-helpers.ts +79 -4
- package/src/server/responses/input-admission.ts +185 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +178 -0
- package/src/server/responses/responses-field-backfill.ts +251 -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 +308 -0
- package/src/server/responses-custom-tool-repair.ts +282 -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 +342 -0
- package/src/server/responses-tool-search-repair.ts +267 -0
- package/src/server/responses-undeclared-tool-guard.ts +153 -0
- package/src/server/responses.ts +18 -2
- package/src/server/search.ts +78 -13
- package/src/server/sse-frame-buffer.ts +292 -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 +39 -38
- package/src/service-manager-probe.ts +892 -0
- package/src/service.ts +1111 -90
- package/src/sidecar/auth.ts +92 -0
- package/src/sidecar/candidates.ts +83 -0
- package/src/storage/cleanup.ts +2 -2
- 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 +845 -0
- package/src/types/provider.ts +545 -0
- package/src/types/request.ts +384 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +104 -1236
- package/src/update/index.ts +32 -19
- package/src/update/job.ts +442 -67
- 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 +259 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +268 -16
- package/src/usage/log.ts +606 -41
- package/src/usage/summary.ts +177 -9
- 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 +9 -3
- 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 +88 -0
- package/src/web-search/executor.ts +11 -3
- package/src/web-search/gemini-executor.ts +141 -0
- package/src/web-search/index.ts +150 -15
- package/src/web-search/loop.ts +279 -50
- 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
package/src/usage/log.ts
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
+
import { createHash, type Hash } from "node:crypto";
|
|
1
2
|
import { chmodSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, appendFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { getConfigDir } from "../config";
|
|
5
|
+
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
4
6
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
7
|
+
import { sanitizeLogMetadataString } from "../lib/redact";
|
|
5
8
|
import { usageDisplayTotalTokens } from "./totals";
|
|
6
|
-
import type { OcxUsage } from "../types";
|
|
9
|
+
import type { AttemptTierOutcome, OcxUsage } from "../types";
|
|
10
|
+
import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
|
|
11
|
+
import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
7
12
|
|
|
8
13
|
export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
|
|
14
|
+
export type CodexUsageAccountLogLabel = "main" | `p${string}`;
|
|
9
15
|
|
|
16
|
+
export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel {
|
|
17
|
+
return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Recovery kinds recorded per attempt in the usage log; the GUI renders localized labels
|
|
22
|
+
* for these wire values.
|
|
23
|
+
*/
|
|
10
24
|
export type AttemptRecoveryKind =
|
|
11
25
|
| "transient-5xx"
|
|
12
26
|
| "connection-reset"
|
|
13
27
|
| "oauth-401"
|
|
14
28
|
| "key-429"
|
|
29
|
+
| "rate-limit-429"
|
|
15
30
|
| "anthropic-oauth-429"
|
|
16
|
-
| "image-413"
|
|
31
|
+
| "image-413"
|
|
32
|
+
| "opaque-blob-rejection"
|
|
33
|
+
| "empty-completion";
|
|
17
34
|
|
|
18
35
|
export interface PersistedUsageAttempt {
|
|
19
36
|
ordinal: number;
|
|
@@ -22,20 +39,32 @@ export interface PersistedUsageAttempt {
|
|
|
22
39
|
adapter: string;
|
|
23
40
|
status: number;
|
|
24
41
|
durationMs: number;
|
|
42
|
+
/**
|
|
43
|
+
* True only when the upstream stream died after its 200 head was committed,
|
|
44
|
+
* so the row must not meter as a success the client never received.
|
|
45
|
+
* Absent on ordinary attempts so old rows keep their exact shape.
|
|
46
|
+
*/
|
|
47
|
+
streamAborted?: boolean;
|
|
25
48
|
/** TTFT relative to THIS attempt's start (WP4); unset for non-streaming/tool-only. */
|
|
26
49
|
firstOutputMs?: number;
|
|
27
50
|
sendCount: number;
|
|
28
51
|
recoveryKinds: AttemptRecoveryKind[];
|
|
29
52
|
usageStatus: UsageStatus;
|
|
53
|
+
/** Stable non-PII identity for the Codex pool account that served this attempt. */
|
|
54
|
+
accountLogLabel?: CodexUsageAccountLogLabel;
|
|
30
55
|
inputTokenEstimate?: number;
|
|
31
56
|
usage?: OcxUsage;
|
|
32
57
|
totalTokens?: number;
|
|
33
58
|
errorCode?: string;
|
|
59
|
+
/** Installation-local exact Compatibility Lab route-subject digest for this attempt. */
|
|
60
|
+
labRouteSubjectId?: string;
|
|
34
61
|
/** Target-specific reasoning intent and exact adapter-normalized wire parameter. */
|
|
35
62
|
requestedEffort?: string;
|
|
36
63
|
effectiveEffort?: string;
|
|
37
64
|
reasoningWireField?: string;
|
|
38
|
-
reasoningWireValue?: string | number;
|
|
65
|
+
reasoningWireValue?: string | number | boolean;
|
|
66
|
+
/** Adapter-produced tier fact for this physical attempt; absent on pre-B0 rows. */
|
|
67
|
+
tierOutcome?: AttemptTierOutcome;
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
export interface PersistedUsageEntry {
|
|
@@ -50,22 +79,30 @@ export interface PersistedUsageEntry {
|
|
|
50
79
|
admissionKind?: "configured" | "environment" | "loopback";
|
|
51
80
|
/** The inbound wire, not the client product — see `surface`. */
|
|
52
81
|
inboundProtocol?: "responses" | "chat" | "messages";
|
|
82
|
+
/** Stable non-PII identity for Codex Pool usage; absent for Direct/non-Codex traffic. */
|
|
83
|
+
accountLogLabel?: CodexUsageAccountLogLabel;
|
|
53
84
|
/** Best-effort chat/session correlation for Logs grouping (#330). */
|
|
54
85
|
conversationId?: string;
|
|
55
86
|
resolvedModel?: string;
|
|
56
87
|
requestedModel?: string;
|
|
88
|
+
/** Original bare helper model when the opt-in shadow-call route rewrote this request. */
|
|
89
|
+
shadowCallRewrittenFrom?: string;
|
|
57
90
|
/** Reasoning effort / service-tier metadata for GUI Logs after restart. */
|
|
58
91
|
requestedEffort?: string;
|
|
59
92
|
/** Adapter-normalized tier and exact upstream parameter emitted for this request. */
|
|
60
93
|
effectiveEffort?: string;
|
|
61
94
|
reasoningWireField?: string;
|
|
62
|
-
reasoningWireValue?: string | number;
|
|
95
|
+
reasoningWireValue?: string | number | boolean;
|
|
96
|
+
/** Raw caller tier captured before routing, sanitized and bounded for durable logs. */
|
|
97
|
+
callerServiceTier?: string;
|
|
63
98
|
requestedServiceTier?: string;
|
|
64
99
|
requestedSpeedLabel?: string;
|
|
65
100
|
configuredServiceTier?: string;
|
|
66
101
|
configuredSpeedLabel?: string;
|
|
67
102
|
modelSupportsServiceTier?: boolean;
|
|
68
103
|
responseServiceTier?: string;
|
|
104
|
+
/** Summary of the final physical attempt for dashboard consumers. */
|
|
105
|
+
tierOutcome?: AttemptTierOutcome;
|
|
69
106
|
status: number;
|
|
70
107
|
durationMs: number;
|
|
71
108
|
/** TTFT relative to the request start (WP4); unset for non-streaming/tool-only. */
|
|
@@ -81,6 +118,12 @@ export interface PersistedUsageEntry {
|
|
|
81
118
|
closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
|
|
82
119
|
/** Already redacted + capped at capture (request-log.ts redactSecretString().slice(0,500)). */
|
|
83
120
|
upstreamError?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Bounded route-decision trace (RI-01): why this provider/model/account was
|
|
123
|
+
* selected. Additive field; old rows without it parse unchanged. Never
|
|
124
|
+
* contains prompts, credentials, or hidden reasoning.
|
|
125
|
+
*/
|
|
126
|
+
routeDecision?: RouteDecisionTraceV1;
|
|
84
127
|
}
|
|
85
128
|
|
|
86
129
|
const KNOWN_USAGE_SURFACES = new Set<NonNullable<PersistedUsageEntry["surface"]>>([
|
|
@@ -118,8 +161,8 @@ export function isKnownInboundProtocol(value: unknown): value is NonNullable<Per
|
|
|
118
161
|
return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
|
|
119
162
|
}
|
|
120
163
|
|
|
121
|
-
export function usageLogPath(): string {
|
|
122
|
-
return join(getConfigDir(), "usage.jsonl");
|
|
164
|
+
export function usageLogPath(configDir?: string): string {
|
|
165
|
+
return join(configDir ?? getConfigDir(), "usage.jsonl");
|
|
123
166
|
}
|
|
124
167
|
|
|
125
168
|
export function usageTotalTokens(usage: OcxUsage | undefined): number | undefined {
|
|
@@ -173,8 +216,11 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
|
|
|
173
216
|
"connection-reset",
|
|
174
217
|
"oauth-401",
|
|
175
218
|
"key-429",
|
|
219
|
+
"rate-limit-429",
|
|
176
220
|
"anthropic-oauth-429",
|
|
177
221
|
"image-413",
|
|
222
|
+
"opaque-blob-rejection",
|
|
223
|
+
"empty-completion",
|
|
178
224
|
]);
|
|
179
225
|
const USAGE_STATUSES = new Set<UsageStatus>([
|
|
180
226
|
"reported",
|
|
@@ -182,6 +228,20 @@ const USAGE_STATUSES = new Set<UsageStatus>([
|
|
|
182
228
|
"unsupported",
|
|
183
229
|
"estimated",
|
|
184
230
|
]);
|
|
231
|
+
const LAB_ROUTE_SUBJECT_ID_RE = /^[0-9a-f]{64}$/;
|
|
232
|
+
const FAST_OUTCOMES = new Set<AttemptTierOutcome["fastOutcome"]>([
|
|
233
|
+
"not-requested", "applied", "downgraded", "unknown",
|
|
234
|
+
]);
|
|
235
|
+
const TIER_CONFIRMATIONS = new Set<AttemptTierOutcome["confirmation"]>([
|
|
236
|
+
"confirmed", "assumed", "downgraded", "unknown",
|
|
237
|
+
]);
|
|
238
|
+
const FAST_DOWNGRADE_REASONS = new Set<NonNullable<AttemptTierOutcome["fastDowngradeReason"]>>([
|
|
239
|
+
"route-unsupported", "wire-unavailable", "response-declined",
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
export function isLabRouteSubjectId(value: unknown): value is string {
|
|
243
|
+
return typeof value === "string" && LAB_ROUTE_SUBJECT_ID_RE.test(value);
|
|
244
|
+
}
|
|
185
245
|
|
|
186
246
|
function isNonNegativeFiniteNumber(value: unknown): value is number {
|
|
187
247
|
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -206,6 +266,53 @@ function normalizeAttemptUsage(raw: unknown): OcxUsage | null {
|
|
|
206
266
|
return normalizeUsageValue(usage as unknown as OcxUsage) ?? null;
|
|
207
267
|
}
|
|
208
268
|
|
|
269
|
+
function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
|
|
270
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
271
|
+
const outcome = raw as Record<string, unknown>;
|
|
272
|
+
if (typeof outcome.fastOutcome !== "string"
|
|
273
|
+
|| !FAST_OUTCOMES.has(outcome.fastOutcome as AttemptTierOutcome["fastOutcome"])
|
|
274
|
+
|| typeof outcome.confirmation !== "string"
|
|
275
|
+
|| !TIER_CONFIRMATIONS.has(outcome.confirmation as AttemptTierOutcome["confirmation"])) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
if ("canonical" in outcome && outcome.canonical !== "priority") return null;
|
|
279
|
+
if ("wireKind" in outcome
|
|
280
|
+
&& outcome.wireKind !== null
|
|
281
|
+
&& outcome.wireKind !== "service-tier"
|
|
282
|
+
&& outcome.wireKind !== "anthropic-speed") return null;
|
|
283
|
+
if ("wireValue" in outcome && outcome.wireValue !== null && typeof outcome.wireValue !== "string") return null;
|
|
284
|
+
if ("fastDowngradeReason" in outcome
|
|
285
|
+
&& (typeof outcome.fastDowngradeReason !== "string"
|
|
286
|
+
|| !FAST_DOWNGRADE_REASONS.has(outcome.fastDowngradeReason as NonNullable<AttemptTierOutcome["fastDowngradeReason"]>))) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if ("callerTierDropped" in outcome && typeof outcome.callerTierDropped !== "boolean") return null;
|
|
290
|
+
if ("callerFastSuppressedByConfig" in outcome
|
|
291
|
+
&& typeof outcome.callerFastSuppressedByConfig !== "boolean") return null;
|
|
292
|
+
if ("responseServiceTier" in outcome && typeof outcome.responseServiceTier !== "string") return null;
|
|
293
|
+
const wireValue = sanitizeLogMetadataString(outcome.wireValue);
|
|
294
|
+
const responseServiceTier = sanitizeLogMetadataString(outcome.responseServiceTier);
|
|
295
|
+
return {
|
|
296
|
+
...(outcome.canonical === "priority" ? { canonical: "priority" as const } : {}),
|
|
297
|
+
...(outcome.wireKind === null || outcome.wireKind === "service-tier" || outcome.wireKind === "anthropic-speed"
|
|
298
|
+
? { wireKind: outcome.wireKind }
|
|
299
|
+
: {}),
|
|
300
|
+
...(outcome.wireValue === null
|
|
301
|
+
? { wireValue: null }
|
|
302
|
+
: wireValue ? { wireValue } : {}),
|
|
303
|
+
fastOutcome: outcome.fastOutcome as AttemptTierOutcome["fastOutcome"],
|
|
304
|
+
...(typeof outcome.fastDowngradeReason === "string"
|
|
305
|
+
? { fastDowngradeReason: outcome.fastDowngradeReason as NonNullable<AttemptTierOutcome["fastDowngradeReason"]> }
|
|
306
|
+
: {}),
|
|
307
|
+
...(typeof outcome.callerTierDropped === "boolean" ? { callerTierDropped: outcome.callerTierDropped } : {}),
|
|
308
|
+
...(typeof outcome.callerFastSuppressedByConfig === "boolean"
|
|
309
|
+
? { callerFastSuppressedByConfig: outcome.callerFastSuppressedByConfig }
|
|
310
|
+
: {}),
|
|
311
|
+
confirmation: outcome.confirmation as AttemptTierOutcome["confirmation"],
|
|
312
|
+
...(responseServiceTier ? { responseServiceTier } : {}),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
209
316
|
function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
210
317
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
211
318
|
const attempt = raw as Record<string, unknown>;
|
|
@@ -232,6 +339,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
232
339
|
&& !isNonNegativeFiniteNumber(attempt.totalTokens)) return null;
|
|
233
340
|
const usage = "usage" in attempt ? normalizeAttemptUsage(attempt.usage) : undefined;
|
|
234
341
|
if ("usage" in attempt && usage === null) return null;
|
|
342
|
+
const tierOutcome = "tierOutcome" in attempt
|
|
343
|
+
? normalizeAttemptTierOutcome(attempt.tierOutcome)
|
|
344
|
+
: undefined;
|
|
235
345
|
const recoveryKinds = Array.isArray(attempt.recoveryKinds)
|
|
236
346
|
? [...new Set(attempt.recoveryKinds.filter(
|
|
237
347
|
(value): value is AttemptRecoveryKind => typeof value === "string"
|
|
@@ -245,12 +355,17 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
245
355
|
adapter: attempt.adapter,
|
|
246
356
|
status: attempt.status,
|
|
247
357
|
durationMs: attempt.durationMs,
|
|
358
|
+
// Absent by default; only the literal `true` marker survives the round trip.
|
|
359
|
+
...(attempt.streamAborted === true ? { streamAborted: true } : {}),
|
|
248
360
|
...(isNonNegativeFiniteNumber(attempt.firstOutputMs)
|
|
249
361
|
? { firstOutputMs: attempt.firstOutputMs }
|
|
250
362
|
: {}),
|
|
251
363
|
sendCount: attempt.sendCount as number,
|
|
252
364
|
recoveryKinds,
|
|
253
365
|
usageStatus: attempt.usageStatus as UsageStatus,
|
|
366
|
+
...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
|
|
367
|
+
? { accountLogLabel: attempt.accountLogLabel }
|
|
368
|
+
: {}),
|
|
254
369
|
...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate)
|
|
255
370
|
? { inputTokenEstimate: attempt.inputTokenEstimate }
|
|
256
371
|
: {}),
|
|
@@ -259,6 +374,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
259
374
|
? { totalTokens: attempt.totalTokens }
|
|
260
375
|
: {}),
|
|
261
376
|
...(typeof attempt.errorCode === "string" ? { errorCode: attempt.errorCode } : {}),
|
|
377
|
+
...(isLabRouteSubjectId(attempt.labRouteSubjectId)
|
|
378
|
+
? { labRouteSubjectId: attempt.labRouteSubjectId }
|
|
379
|
+
: {}),
|
|
262
380
|
...(typeof attempt.requestedEffort === "string" && attempt.requestedEffort
|
|
263
381
|
? { requestedEffort: capMetadataString(attempt.requestedEffort) }
|
|
264
382
|
: {}),
|
|
@@ -268,14 +386,30 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
|
|
|
268
386
|
...(typeof attempt.reasoningWireField === "string" && attempt.reasoningWireField
|
|
269
387
|
? { reasoningWireField: capMetadataString(attempt.reasoningWireField) }
|
|
270
388
|
: {}),
|
|
271
|
-
...(
|
|
272
|
-
?
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
389
|
+
...(isValidReasoningWireValue(attempt.reasoningWireField, attempt.reasoningWireValue)
|
|
390
|
+
? typeof attempt.reasoningWireValue === "string"
|
|
391
|
+
? { reasoningWireValue: capMetadataString(attempt.reasoningWireValue) }
|
|
392
|
+
: { reasoningWireValue: attempt.reasoningWireValue }
|
|
393
|
+
: {}),
|
|
394
|
+
...(tierOutcome ? { tierOutcome } : {}),
|
|
276
395
|
};
|
|
277
396
|
}
|
|
278
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Pairing rule for reasoning diagnostics, shared with the live request-log capture path:
|
|
400
|
+
* a non-empty string, a non-negative finite number, or a boolean only for
|
|
401
|
+
* `reasoning.enabled`. The field name itself is validated separately at capture time;
|
|
402
|
+
* persisted rows may carry legacy field names, so this checks only the value shape.
|
|
403
|
+
*/
|
|
404
|
+
export function isValidReasoningWireValue(
|
|
405
|
+
wireField: unknown,
|
|
406
|
+
wireValue: unknown,
|
|
407
|
+
): wireValue is string | number | boolean {
|
|
408
|
+
return (typeof wireValue === "string" && wireValue.length > 0)
|
|
409
|
+
|| (typeof wireValue === "number" && Number.isFinite(wireValue) && wireValue >= 0)
|
|
410
|
+
|| (wireField === "reasoning.enabled" && typeof wireValue === "boolean");
|
|
411
|
+
}
|
|
412
|
+
|
|
279
413
|
function normalizedAttempts(raw: unknown): PersistedUsageAttempt[] {
|
|
280
414
|
if (!Array.isArray(raw)) return [];
|
|
281
415
|
return raw.map(normalizeUsageAttempt)
|
|
@@ -294,6 +428,13 @@ export function normalizeUsageEntryForTest(entry: PersistedUsageEntry): Persiste
|
|
|
294
428
|
|
|
295
429
|
function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
296
430
|
const attempts = normalizedAttempts(entry.attempts);
|
|
431
|
+
const tierOutcome = entry.tierOutcome ? normalizeAttemptTierOutcome(entry.tierOutcome) : undefined;
|
|
432
|
+
const callerServiceTier = sanitizeLogMetadataString(entry.callerServiceTier);
|
|
433
|
+
const responseServiceTier = sanitizeLogMetadataString(entry.responseServiceTier);
|
|
434
|
+
const shadowCallRewrittenFrom = sanitizeLogMetadataString(entry.shadowCallRewrittenFrom);
|
|
435
|
+
const routeDecision = entry.routeDecision
|
|
436
|
+
? normalizeRouteDecisionTrace(entry.routeDecision)
|
|
437
|
+
: undefined;
|
|
297
438
|
return {
|
|
298
439
|
requestId: entry.requestId,
|
|
299
440
|
timestamp: entry.timestamp,
|
|
@@ -309,11 +450,15 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
309
450
|
: {}),
|
|
310
451
|
...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
|
|
311
452
|
...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
|
|
453
|
+
...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
|
|
454
|
+
? { accountLogLabel: entry.accountLogLabel }
|
|
455
|
+
: {}),
|
|
312
456
|
...(typeof entry.conversationId === "string" && entry.conversationId.trim()
|
|
313
457
|
? { conversationId: entry.conversationId.trim().slice(0, 128) }
|
|
314
458
|
: {}),
|
|
315
459
|
...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
|
|
316
460
|
...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
|
|
461
|
+
...(shadowCallRewrittenFrom ? { shadowCallRewrittenFrom } : {}),
|
|
317
462
|
...(typeof entry.requestedEffort === "string" && entry.requestedEffort
|
|
318
463
|
? { requestedEffort: capMetadataString(entry.requestedEffort) }
|
|
319
464
|
: {}),
|
|
@@ -323,11 +468,12 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
323
468
|
...(typeof entry.reasoningWireField === "string" && entry.reasoningWireField
|
|
324
469
|
? { reasoningWireField: capMetadataString(entry.reasoningWireField) }
|
|
325
470
|
: {}),
|
|
326
|
-
...(
|
|
327
|
-
?
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
471
|
+
...(isValidReasoningWireValue(entry.reasoningWireField, entry.reasoningWireValue)
|
|
472
|
+
? typeof entry.reasoningWireValue === "string"
|
|
473
|
+
? { reasoningWireValue: capMetadataString(entry.reasoningWireValue) }
|
|
474
|
+
: { reasoningWireValue: entry.reasoningWireValue }
|
|
475
|
+
: {}),
|
|
476
|
+
...(callerServiceTier ? { callerServiceTier } : {}),
|
|
331
477
|
...(typeof entry.requestedServiceTier === "string" && entry.requestedServiceTier
|
|
332
478
|
? { requestedServiceTier: capMetadataString(entry.requestedServiceTier) }
|
|
333
479
|
: {}),
|
|
@@ -343,9 +489,8 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
343
489
|
...(typeof entry.modelSupportsServiceTier === "boolean"
|
|
344
490
|
? { modelSupportsServiceTier: entry.modelSupportsServiceTier }
|
|
345
491
|
: {}),
|
|
346
|
-
...(
|
|
347
|
-
|
|
348
|
-
: {}),
|
|
492
|
+
...(responseServiceTier ? { responseServiceTier } : {}),
|
|
493
|
+
...(tierOutcome ? { tierOutcome } : {}),
|
|
349
494
|
status: entry.status,
|
|
350
495
|
durationMs: entry.durationMs,
|
|
351
496
|
...(isNonNegativeFiniteNumber(entry.firstOutputMs)
|
|
@@ -354,11 +499,12 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
|
|
|
354
499
|
usageStatus: entry.usageStatus,
|
|
355
500
|
...(entry.usage ? { usage: normalizeUsageValue(entry.usage) } : {}),
|
|
356
501
|
...(typeof entry.totalTokens === "number" ? { totalTokens: entry.totalTokens } : {}),
|
|
357
|
-
...(attempts
|
|
502
|
+
...(Array.isArray(entry.attempts) ? { attempts } : {}),
|
|
358
503
|
...(entry.errorCode ? { errorCode: entry.errorCode } : {}),
|
|
359
504
|
...(entry.terminalStatus ? { terminalStatus: entry.terminalStatus } : {}),
|
|
360
505
|
...(entry.closeReason ? { closeReason: entry.closeReason } : {}),
|
|
361
506
|
...(entry.upstreamError ? { upstreamError: entry.upstreamError } : {}),
|
|
507
|
+
...(routeDecision ? { routeDecision } : {}),
|
|
362
508
|
};
|
|
363
509
|
}
|
|
364
510
|
|
|
@@ -388,8 +534,18 @@ export type UsageLogRevision = {
|
|
|
388
534
|
|
|
389
535
|
let usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
|
|
390
536
|
const MANAGEMENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
|
|
537
|
+
const RECENT_USAGE_MAX_READ_BYTES = 64 * 1024 * 1024;
|
|
391
538
|
const MANAGEMENT_USAGE_READ_CHUNK_BYTES = 1024 * 1024;
|
|
392
|
-
const
|
|
539
|
+
const MANAGEMENT_USAGE_MAX_ENTRIES_DEFAULT = 500_000;
|
|
540
|
+
/**
|
|
541
|
+
* Row cap for a management snapshot. Overridable only so tests can reach the cap without
|
|
542
|
+
* building a half-million-row fixture; production always uses the default.
|
|
543
|
+
*/
|
|
544
|
+
let MANAGEMENT_USAGE_MAX_ENTRIES = MANAGEMENT_USAGE_MAX_ENTRIES_DEFAULT;
|
|
545
|
+
|
|
546
|
+
export function setManagementUsageMaxEntriesForTests(value: number | null): void {
|
|
547
|
+
MANAGEMENT_USAGE_MAX_ENTRIES = value ?? MANAGEMENT_USAGE_MAX_ENTRIES_DEFAULT;
|
|
548
|
+
}
|
|
393
549
|
const MANAGEMENT_USAGE_FLIGHT_STALE_MS = 30_000;
|
|
394
550
|
export interface ManagementUsageSnapshot {
|
|
395
551
|
entries: PersistedUsageEntry[];
|
|
@@ -397,14 +553,156 @@ export interface ManagementUsageSnapshot {
|
|
|
397
553
|
truncatedPrefixBytes: number;
|
|
398
554
|
entriesTruncated: boolean;
|
|
399
555
|
entriesDropped: number;
|
|
556
|
+
/** Digest of the covered prefix, used to detect an in-place rewrite before reuse. */
|
|
557
|
+
prefixDigest: string;
|
|
558
|
+
/**
|
|
559
|
+
* Byte offset where the RETAINED ROWS begin.
|
|
560
|
+
*
|
|
561
|
+
* Distinct from `truncatedPrefixBytes`, which is the API-visible "bytes skipped by the
|
|
562
|
+
* byte window" signal and must stay independent of entry-count truncation. When the
|
|
563
|
+
* entry cap drops rows, those bytes are not window truncation, but the retained rows do
|
|
564
|
+
* start later -- this field tracks that so the byte accounting stays exact.
|
|
565
|
+
*/
|
|
566
|
+
rowsBeginAtBytes: number;
|
|
567
|
+
/**
|
|
568
|
+
* Byte length of each returned row, in order, including its newline.
|
|
569
|
+
*
|
|
570
|
+
* Lets a later read trim rows that have fallen out of the bounded window without
|
|
571
|
+
* re-reading or re-parsing them, which is what keeps the returned set equal to the
|
|
572
|
+
* window the caller asked for.
|
|
573
|
+
*/
|
|
574
|
+
entryLengths: number[];
|
|
575
|
+
/** Unparseable bytes after the last returned row; the next read folds them forward. */
|
|
576
|
+
trailingSkippedBytes: number;
|
|
400
577
|
}
|
|
401
578
|
let managementUsageReadInflight: {
|
|
402
579
|
key: string;
|
|
580
|
+
openedSize: number;
|
|
403
581
|
promise: Promise<ManagementUsageSnapshot>;
|
|
404
582
|
startedAt: number;
|
|
405
583
|
abort: AbortController;
|
|
406
584
|
} | null = null;
|
|
407
585
|
|
|
586
|
+
/**
|
|
587
|
+
* Append-tolerant snapshot of the last management read.
|
|
588
|
+
*
|
|
589
|
+
* The management reader parses a 64 MiB tail into ~53k objects, which costs roughly
|
|
590
|
+
* 640 MB of transient RSS per cold call. The JS objects are collected promptly, but
|
|
591
|
+
* the allocator does not return those pages, so every cold miss ratchets process RSS
|
|
592
|
+
* upward and never comes back down (observed: 7.9 GiB RSS against a 130 MB JS heap).
|
|
593
|
+
*
|
|
594
|
+
* Reparsing an unchanged prefix is what makes that transient recur. `usage.jsonl` is
|
|
595
|
+
* append-only under a stable identity, so when the file has only grown we keep the
|
|
596
|
+
* previously parsed rows and parse just the appended bytes. This is retained state, so
|
|
597
|
+
* it is registered with the app-owned memory budget and is evictable under pressure.
|
|
598
|
+
*/
|
|
599
|
+
interface RetainedUsageSnapshot {
|
|
600
|
+
identityKey: string;
|
|
601
|
+
maxReadBytes: number;
|
|
602
|
+
/** Absolute end offset in the file that `entries` already covers. */
|
|
603
|
+
coveredThroughBytes: number;
|
|
604
|
+
/**
|
|
605
|
+
* Digest of the last bytes of the covered prefix, re-verified before extending.
|
|
606
|
+
*
|
|
607
|
+
* Identity (path/dev/ino/birthtime) intentionally ignores size and mtime so appends
|
|
608
|
+
* can share work, which also means an in-place rewrite that keeps the inode is
|
|
609
|
+
* invisible to it. A hand-edit or external compaction can therefore replace history
|
|
610
|
+
* under a stable identity without shrinking the file. Re-reading this trailing window
|
|
611
|
+
* catches that: if the bytes behind `coveredThroughBytes` changed, the retained rows
|
|
612
|
+
* no longer describe the file and must not be extended.
|
|
613
|
+
*/
|
|
614
|
+
prefixDigest: string;
|
|
615
|
+
/** Bytes of the file skipped ahead of the retained window. */
|
|
616
|
+
truncatedPrefixBytes: number;
|
|
617
|
+
/** Byte length of each retained row, so out-of-window rows can be trimmed exactly. */
|
|
618
|
+
entryLengths: number[];
|
|
619
|
+
/** Unparseable bytes after the last retained row; folded into the next row's span. */
|
|
620
|
+
trailingSkippedBytes: number;
|
|
621
|
+
/** Byte offset where the retained rows begin; see ManagementUsageSnapshot. */
|
|
622
|
+
rowsBeginAtBytes: number;
|
|
623
|
+
entries: PersistedUsageEntry[];
|
|
624
|
+
entriesTruncated: boolean;
|
|
625
|
+
entriesDropped: number;
|
|
626
|
+
revision: UsageLogRevision;
|
|
627
|
+
retainedAt: number;
|
|
628
|
+
approxBytes: number;
|
|
629
|
+
}
|
|
630
|
+
let retainedUsageSnapshot: RetainedUsageSnapshot | null = null;
|
|
631
|
+
|
|
632
|
+
/** Rough per-row retained cost; exact sizing would cost another full serialization pass. */
|
|
633
|
+
const RETAINED_USAGE_ENTRY_BYTES = 512;
|
|
634
|
+
|
|
635
|
+
/** Chunk size used when digesting a retained region. */
|
|
636
|
+
const RETAINED_USAGE_DIGEST_CHUNK_BYTES = 1024 * 1024;
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Digest a byte range into `hash`; false when it cannot be read.
|
|
640
|
+
*
|
|
641
|
+
* Deliberately not sampled. A sampled digest covers a vanishing fraction of a large
|
|
642
|
+
* prefix (32 KiB of 64 MiB is 0.05%), so an ordinary fixed-width in-place edit -- a
|
|
643
|
+
* redaction script fixing one field, a compaction rewriting a middle region -- lands in
|
|
644
|
+
* a gap by default and the stale rows are served.
|
|
645
|
+
*
|
|
646
|
+
* Hashing from byte 0 on every call is also wrong: that is O(file) per poll while the
|
|
647
|
+
* read it protects is capped at maxReadBytes, so the ratio degrades as the ledger grows
|
|
648
|
+
* and becomes SLOWER than a full read past roughly 1-2 GB. Only the retained REGION
|
|
649
|
+
* (truncatedPrefixBytes..coveredThroughBytes) is hashed. It is never wider than the
|
|
650
|
+
* window, and bytes below the retained start describe no retained row, so reading them
|
|
651
|
+
* would prove nothing.
|
|
652
|
+
*/
|
|
653
|
+
function updateUsageDigest(hash: Hash, fd: number, from: number, to: number): boolean {
|
|
654
|
+
if (to <= from) return true;
|
|
655
|
+
const buffer = Buffer.allocUnsafe(Math.min(RETAINED_USAGE_DIGEST_CHUNK_BYTES, to - from));
|
|
656
|
+
for (let position = from; position < to;) {
|
|
657
|
+
const length = Math.min(buffer.byteLength, to - position);
|
|
658
|
+
let offset = 0;
|
|
659
|
+
while (offset < length) {
|
|
660
|
+
const read = readSync(fd, buffer, offset, length - offset, position + offset);
|
|
661
|
+
if (read === 0) return false;
|
|
662
|
+
offset += read;
|
|
663
|
+
}
|
|
664
|
+
hash.update(buffer.subarray(0, length));
|
|
665
|
+
position += length;
|
|
666
|
+
}
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Digest of `from`..`to`; null when it cannot be read.
|
|
672
|
+
*
|
|
673
|
+
* The range is bound into the digest so a region cannot be confused with an equal-length
|
|
674
|
+
* region at a different offset.
|
|
675
|
+
*/
|
|
676
|
+
function usageRegionDigest(fd: number, from: number, to: number): string | null {
|
|
677
|
+
if (to <= from) return `${from}:${to}:empty`;
|
|
678
|
+
const hash = createHash("sha256");
|
|
679
|
+
if (!updateUsageDigest(hash, fd, from, to)) return null;
|
|
680
|
+
return `${from}:${to}:${hash.digest("hex")}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function retainedUsageSnapshotBytes(entries: PersistedUsageEntry[]): number {
|
|
684
|
+
return entries.length * RETAINED_USAGE_ENTRY_BYTES;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function discardRetainedUsageSnapshot(): number {
|
|
688
|
+
const released = retainedUsageSnapshot?.approxBytes ?? 0;
|
|
689
|
+
retainedUsageSnapshot = null;
|
|
690
|
+
return released;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function retainedUsageSnapshotStats(): {
|
|
694
|
+
count: number;
|
|
695
|
+
bytes: number;
|
|
696
|
+
oldestAt: number | null;
|
|
697
|
+
} {
|
|
698
|
+
if (!retainedUsageSnapshot) return { count: 0, bytes: 0, oldestAt: null };
|
|
699
|
+
return {
|
|
700
|
+
count: 1,
|
|
701
|
+
bytes: retainedUsageSnapshot.approxBytes,
|
|
702
|
+
oldestAt: retainedUsageSnapshot.retainedAt,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
408
706
|
/** Test-only observability for proving that unchanged prefixes are not reparsed. */
|
|
409
707
|
export function usageReadCacheStatsForTests(): Readonly<typeof usageReadCacheStats> {
|
|
410
708
|
return { ...usageReadCacheStats };
|
|
@@ -414,6 +712,7 @@ export function resetUsageReadCacheForTests(): void {
|
|
|
414
712
|
usageReadCacheStats = { fullReads: 0, tailReads: 0, parsedLines: 0 };
|
|
415
713
|
managementUsageReadInflight?.abort.abort();
|
|
416
714
|
managementUsageReadInflight = null;
|
|
715
|
+
retainedUsageSnapshot = null;
|
|
417
716
|
}
|
|
418
717
|
|
|
419
718
|
function readExactly(fd: number, length: number, position: number): Buffer | null {
|
|
@@ -453,6 +752,12 @@ export function usageLogRevisionKey(revision: UsageLogRevision | null): string {
|
|
|
453
752
|
].join("\0");
|
|
454
753
|
}
|
|
455
754
|
|
|
755
|
+
/** Identity of the usage ledger file, excluding size/mtime/ctime so appends can share work. */
|
|
756
|
+
export function usageLogIdentityKey(revision: UsageLogRevision | null): string {
|
|
757
|
+
if (!revision) return "missing";
|
|
758
|
+
return [revision.path, revision.dev, revision.ino, revision.birthtimeMs].join("\0");
|
|
759
|
+
}
|
|
760
|
+
|
|
456
761
|
export function currentUsageLogRevision(): UsageLogRevision | null {
|
|
457
762
|
const path = usageLogPath();
|
|
458
763
|
if (!existsSync(path)) return null;
|
|
@@ -468,23 +773,71 @@ export function currentUsageLogRevision(): UsageLogRevision | null {
|
|
|
468
773
|
async function parseUsageTextCooperatively(text: string, signal: AbortSignal): Promise<{
|
|
469
774
|
entries: PersistedUsageEntry[];
|
|
470
775
|
entriesDropped: number;
|
|
776
|
+
entryLengths: number[];
|
|
777
|
+
/** Bytes of unparseable lines after the final accepted row. */
|
|
778
|
+
trailingSkippedBytes: number;
|
|
779
|
+
/** Bytes of rows removed by the entry cap, which move into the skipped prefix. */
|
|
780
|
+
cappedPrefixBytes: number;
|
|
471
781
|
}> {
|
|
472
|
-
|
|
782
|
+
// Split on "\n" only, so a CRLF line keeps its "\r" and its byte length stays exact.
|
|
783
|
+
// Splitting on /\r?\n/ consumes two bytes but leaves no way to tell that it did, which
|
|
784
|
+
// made the recorded lengths short by one byte per line on a CRLF ledger and failed the
|
|
785
|
+
// accounting self-check. JSON.parse tolerates the trailing "\r".
|
|
786
|
+
const lines = text.split("\n");
|
|
473
787
|
usageReadCacheStats.parsedLines += lines.filter(line => line.trim()).length;
|
|
474
788
|
const entries: PersistedUsageEntry[] = [];
|
|
789
|
+
// Byte length of each accepted row including its newline, so a later read can trim
|
|
790
|
+
// rows that fall out of the bounded window without re-reading the file.
|
|
791
|
+
const entryLengths: number[] = [];
|
|
475
792
|
const batchSize = 1_000;
|
|
793
|
+
// Bytes of lines that did not yield an entry (malformed JSON, missing requestId, a
|
|
794
|
+
// torn final write). They still occupy space in the file, so they are folded into the
|
|
795
|
+
// next accepted row's recorded length. Dropping them would make the recorded lengths
|
|
796
|
+
// sum to less than the real byte span, and the window trim -- which walks forward by
|
|
797
|
+
// summing those lengths -- would consume extra rows to reach the window start,
|
|
798
|
+
// silently hiding history and desynchronizing truncatedPrefixBytes.
|
|
799
|
+
let pendingSkippedBytes = 0;
|
|
476
800
|
for (let offset = 0; offset < lines.length; offset += batchSize) {
|
|
477
801
|
if (signal.aborted) throw signal.reason;
|
|
478
|
-
|
|
802
|
+
const batch = lines.slice(offset, offset + batchSize);
|
|
803
|
+
for (let index = 0; index < batch.length; index++) {
|
|
804
|
+
const line = batch[index]!;
|
|
805
|
+
// The split leaves a trailing "" after the final newline; it occupies no bytes.
|
|
806
|
+
const isLastLine = offset + index === lines.length - 1;
|
|
807
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (isLastLine && line === "" ? 0 : 1);
|
|
808
|
+
const parsed = parseUsageLines([line]);
|
|
809
|
+
if (parsed.length === 0) {
|
|
810
|
+
pendingSkippedBytes += lineBytes;
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
entries.push(parsed[0]!);
|
|
814
|
+
entryLengths.push(lineBytes + pendingSkippedBytes);
|
|
815
|
+
pendingSkippedBytes = 0;
|
|
816
|
+
}
|
|
479
817
|
if (offset + batchSize < lines.length) {
|
|
480
818
|
// JSON parsing dominates large-log startup. Yield between bounded batches so
|
|
481
819
|
// Bun can continue serving health and settings requests on the same thread.
|
|
482
820
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
483
821
|
}
|
|
484
822
|
}
|
|
485
|
-
|
|
823
|
+
// Skipped bytes AFTER the last accepted row belong to no entry length, so report them
|
|
824
|
+
// separately; the trim arithmetic adds them back to keep lengths summing to the span.
|
|
825
|
+
const trailingSkippedBytes = pendingSkippedBytes;
|
|
826
|
+
if (entries.length <= MANAGEMENT_USAGE_MAX_ENTRIES) {
|
|
827
|
+
return { entries, entriesDropped: 0, entryLengths, trailingSkippedBytes, cappedPrefixBytes: 0 };
|
|
828
|
+
}
|
|
486
829
|
const entriesDropped = entries.length - MANAGEMENT_USAGE_MAX_ENTRIES;
|
|
487
|
-
return {
|
|
830
|
+
return {
|
|
831
|
+
entries: entries.slice(-MANAGEMENT_USAGE_MAX_ENTRIES),
|
|
832
|
+
entriesDropped,
|
|
833
|
+
entryLengths: entryLengths.slice(-MANAGEMENT_USAGE_MAX_ENTRIES),
|
|
834
|
+
trailingSkippedBytes,
|
|
835
|
+
// Bytes of the rows the cap removed. The caller adds them to its skipped prefix so
|
|
836
|
+
// the recorded lengths keep summing to the byte span they describe.
|
|
837
|
+
cappedPrefixBytes: entryLengths
|
|
838
|
+
.slice(0, entryLengths.length - MANAGEMENT_USAGE_MAX_ENTRIES)
|
|
839
|
+
.reduce((total, length) => total + length, 0),
|
|
840
|
+
};
|
|
488
841
|
}
|
|
489
842
|
|
|
490
843
|
async function readUsageEntriesFullCooperatively(
|
|
@@ -525,12 +878,181 @@ async function readUsageEntriesFullCooperatively(
|
|
|
525
878
|
}
|
|
526
879
|
const parsed = await parseUsageTextCooperatively(bytes.toString("utf-8"), signal);
|
|
527
880
|
usageReadCacheStats.fullReads += 1;
|
|
881
|
+
// Rows removed by the entry cap start the retained rows later in the file. That is
|
|
882
|
+
// NOT byte-window truncation, so it must not move truncatedPrefixBytes -- the two
|
|
883
|
+
// signals are independent in the API. It is tracked separately for the byte
|
|
884
|
+
// accounting the incremental reader relies on.
|
|
885
|
+
const rowsBeginAtBytes = truncatedPrefixBytes + parsed.cappedPrefixBytes;
|
|
886
|
+
// Digest the exact prefix these rows describe, so a later incremental read can
|
|
887
|
+
// prove the file was appended to rather than rewritten under the same inode.
|
|
888
|
+
const prefixDigest = usageRegionDigest(fd, rowsBeginAtBytes, Number(stat.size));
|
|
889
|
+
if (prefixDigest === null) throw new Error("usage log changed while it was being read");
|
|
528
890
|
return {
|
|
529
891
|
entries: parsed.entries,
|
|
530
892
|
revision: usageLogRevision(path, stat),
|
|
531
893
|
truncatedPrefixBytes,
|
|
532
894
|
entriesTruncated: parsed.entriesDropped > 0,
|
|
533
895
|
entriesDropped: parsed.entriesDropped,
|
|
896
|
+
prefixDigest,
|
|
897
|
+
entryLengths: parsed.entryLengths,
|
|
898
|
+
trailingSkippedBytes: parsed.trailingSkippedBytes,
|
|
899
|
+
rowsBeginAtBytes,
|
|
900
|
+
};
|
|
901
|
+
} finally {
|
|
902
|
+
if (fd !== undefined) closeSync(fd);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Parse only the bytes appended since the retained snapshot's covered offset.
|
|
908
|
+
*
|
|
909
|
+
* Returns null when the retained snapshot cannot be extended safely — a different
|
|
910
|
+
* identity or read window, a file that shrank (replacement/truncation), or a covered
|
|
911
|
+
* offset that no longer sits on a record boundary. Callers then fall back to a full
|
|
912
|
+
* bounded read.
|
|
913
|
+
*/
|
|
914
|
+
async function readUsageEntriesIncrementally(
|
|
915
|
+
path: string,
|
|
916
|
+
signal: AbortSignal,
|
|
917
|
+
maxReadBytes: number,
|
|
918
|
+
retained: RetainedUsageSnapshot,
|
|
919
|
+
): Promise<ManagementUsageSnapshot | null> {
|
|
920
|
+
let fd: number | undefined;
|
|
921
|
+
try {
|
|
922
|
+
fd = openSync(path, "r");
|
|
923
|
+
const stat = fstatSync(fd);
|
|
924
|
+
const revision = usageLogRevision(path, stat);
|
|
925
|
+
if (usageLogIdentityKey(revision) !== retained.identityKey) return null;
|
|
926
|
+
const size = Number(stat.size);
|
|
927
|
+
// A shrink means truncation or replacement-in-place; the retained rows may no
|
|
928
|
+
// longer correspond to file contents, so refuse to extend them.
|
|
929
|
+
if (size < retained.coveredThroughBytes) return null;
|
|
930
|
+
// Verify the retained REGION is unchanged before anything is reused. Identity keeps
|
|
931
|
+
// dev/ino/birthtime, and an append and an in-place rewrite both move mtime/ctime
|
|
932
|
+
// forward, so only the bytes themselves settle it.
|
|
933
|
+
//
|
|
934
|
+
// Only `truncatedPrefixBytes..coveredThroughBytes` is hashed: that is exactly the
|
|
935
|
+
// span the retained rows were parsed from, and after trimming it is never wider than
|
|
936
|
+
// maxReadBytes. Hashing from byte 0 instead would make every poll O(file) -- cheaper
|
|
937
|
+
// than a reparse on a 245 MB ledger but MORE expensive past roughly 1-2 GB, turning
|
|
938
|
+
// this optimization into a pessimization on exactly the growth curve an append-only
|
|
939
|
+
// ledger follows. Bytes before the retained start are not described by any retained
|
|
940
|
+
// row, so re-reading them proves nothing.
|
|
941
|
+
const covered = usageRegionDigest(fd, retained.rowsBeginAtBytes, retained.coveredThroughBytes);
|
|
942
|
+
if (covered === null || covered !== retained.prefixDigest) return null;
|
|
943
|
+
// Read and parse ONLY the appended bytes.
|
|
944
|
+
let appendedEntries: PersistedUsageEntry[] = [];
|
|
945
|
+
let appendedLengths: number[] = [];
|
|
946
|
+
let appendedDropped = 0;
|
|
947
|
+
let appendedTrailingSkipped = retained.trailingSkippedBytes;
|
|
948
|
+
if (size > retained.coveredThroughBytes) {
|
|
949
|
+
// The covered offset must land immediately after a newline, or the retained rows
|
|
950
|
+
// and the appended text do not join on a record boundary.
|
|
951
|
+
if (retained.coveredThroughBytes > 0) {
|
|
952
|
+
const preceding = readExactly(fd, 1, retained.coveredThroughBytes - 1);
|
|
953
|
+
if (preceding === null || preceding[0] !== 0x0a) return null;
|
|
954
|
+
}
|
|
955
|
+
const chunks: Buffer[] = [];
|
|
956
|
+
for (let position = retained.coveredThroughBytes; position < size;) {
|
|
957
|
+
if (signal.aborted) throw signal.reason;
|
|
958
|
+
const length = Math.min(MANAGEMENT_USAGE_READ_CHUNK_BYTES, size - position);
|
|
959
|
+
const chunk = readExactly(fd, length, position);
|
|
960
|
+
if (chunk === null) throw new Error("usage log changed while it was being read");
|
|
961
|
+
chunks.push(chunk);
|
|
962
|
+
position += length;
|
|
963
|
+
}
|
|
964
|
+
const appended = await parseUsageTextCooperatively(Buffer.concat(chunks).toString("utf-8"), signal);
|
|
965
|
+
appendedEntries = appended.entries;
|
|
966
|
+
appendedLengths = appended.entryLengths;
|
|
967
|
+
appendedDropped = appended.entriesDropped;
|
|
968
|
+
// A capped appended chunk is not joinable: its dropped rows sit between the
|
|
969
|
+
// retained rows and the kept ones, so the lengths no longer describe a contiguous
|
|
970
|
+
// span. Fall back to a full read.
|
|
971
|
+
if (appended.cappedPrefixBytes > 0) return null;
|
|
972
|
+
// If the appended chunk produced rows, its own trailing skipped bytes become the
|
|
973
|
+
// new trailing remainder; otherwise the earlier remainder still stands and the new
|
|
974
|
+
// skipped bytes add to it.
|
|
975
|
+
appendedTrailingSkipped = appended.entries.length > 0
|
|
976
|
+
? appended.trailingSkippedBytes
|
|
977
|
+
: retained.trailingSkippedBytes + appended.trailingSkippedBytes;
|
|
978
|
+
}
|
|
979
|
+
// Re-anchor the window in place. Rows that have fallen outside `size - maxReadBytes`
|
|
980
|
+
// are dropped using their recorded byte lengths, so the result is exactly the rows a
|
|
981
|
+
// fresh bounded read would load -- no superset, and truncatedPrefixBytes and
|
|
982
|
+
// snapshotWindow keep describing the read honestly. Refusing here instead would make
|
|
983
|
+
// this path dead code on any ledger past the window, which is precisely the case it
|
|
984
|
+
// exists for.
|
|
985
|
+
const windowStart = Math.max(0, size - maxReadBytes);
|
|
986
|
+
let entries = retained.entries.concat(appendedEntries);
|
|
987
|
+
// Skipped bytes trailing the retained rows sit BETWEEN them and the appended rows, so
|
|
988
|
+
// they belong to the first appended row's span. Folding them in keeps the recorded
|
|
989
|
+
// lengths summing to the true byte distance, which is what the trim walk relies on.
|
|
990
|
+
const joinedLengths = appendedLengths.slice();
|
|
991
|
+
if (retained.trailingSkippedBytes > 0 && joinedLengths.length > 0) {
|
|
992
|
+
joinedLengths[0] = joinedLengths[0]! + retained.trailingSkippedBytes;
|
|
993
|
+
}
|
|
994
|
+
let lengths = retained.entryLengths.concat(joinedLengths);
|
|
995
|
+
let rowsBeginAtBytes = retained.rowsBeginAtBytes;
|
|
996
|
+
// Byte-window truncation advances ONLY here, so it stays exactly what a cold read of
|
|
997
|
+
// this window reports. The entry cap below is entry-count truncation and must not
|
|
998
|
+
// move it -- the two are independent signals in the API.
|
|
999
|
+
let windowTruncatedBytes = retained.truncatedPrefixBytes;
|
|
1000
|
+
let dropIndex = 0;
|
|
1001
|
+
while (dropIndex < lengths.length && rowsBeginAtBytes < windowStart) {
|
|
1002
|
+
rowsBeginAtBytes += lengths[dropIndex]!;
|
|
1003
|
+
windowTruncatedBytes += lengths[dropIndex]!;
|
|
1004
|
+
dropIndex += 1;
|
|
1005
|
+
}
|
|
1006
|
+
// If every row is gone and a trailing unparseable remainder still sits before the
|
|
1007
|
+
// window start, nothing is left to advance the offset with: the retained span would
|
|
1008
|
+
// keep growing past maxReadBytes on each malformed-only append while the accounting
|
|
1009
|
+
// still balanced. Re-anchor with a full read instead.
|
|
1010
|
+
if (rowsBeginAtBytes < windowStart) return null;
|
|
1011
|
+
if (dropIndex > 0) {
|
|
1012
|
+
entries = entries.slice(dropIndex);
|
|
1013
|
+
lengths = lengths.slice(dropIndex);
|
|
1014
|
+
}
|
|
1015
|
+
let entriesDropped = retained.entriesDropped + appendedDropped;
|
|
1016
|
+
if (entries.length > MANAGEMENT_USAGE_MAX_ENTRIES) {
|
|
1017
|
+
// A cold read applies the entry cap to the whole window and reports byte
|
|
1018
|
+
// truncation for the window boundary alone. An incremental read arrives at the cap
|
|
1019
|
+
// by a different route and cannot reconstruct that ordering from retained state, so
|
|
1020
|
+
// continuing here would report a truncatedPrefixBytes that disagrees with a fresh
|
|
1021
|
+
// read of the same window. Re-anchor instead.
|
|
1022
|
+
//
|
|
1023
|
+
// This is reachable in production, not a theoretical branch: rows average ~118
|
|
1024
|
+
// bytes on a real ledger, so 500,000 of them occupy ~56 MiB and fit inside the
|
|
1025
|
+
// 64 MiB window. Both truncations can therefore apply at once.
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
// The recorded lengths plus the trailing remainder must account for every byte from
|
|
1029
|
+
// the retained rows' start to EOF; if they do not, the lengths and the file have
|
|
1030
|
+
// diverged and the retained rows cannot be trusted.
|
|
1031
|
+
let accounted = appendedTrailingSkipped;
|
|
1032
|
+
for (const length of lengths) accounted += length;
|
|
1033
|
+
if (rowsBeginAtBytes + accounted !== size) return null;
|
|
1034
|
+
usageReadCacheStats.tailReads += 1;
|
|
1035
|
+
// Byte-window truncation is what the API reports, and it stays independent of
|
|
1036
|
+
// entry-count truncation. A cold read reports the record boundary it actually landed
|
|
1037
|
+
// on, which is where the rows begin MINUS whatever the entry cap removed -- the cap
|
|
1038
|
+
// is not window truncation. When nothing was skipped by the window at all, a cold
|
|
1039
|
+
// read reports 0.
|
|
1040
|
+
const truncatedPrefixBytes = windowTruncatedBytes;
|
|
1041
|
+
return {
|
|
1042
|
+
entries,
|
|
1043
|
+
revision,
|
|
1044
|
+
truncatedPrefixBytes,
|
|
1045
|
+
// ENTRY-count truncation only. Byte-window truncation is reported by
|
|
1046
|
+
// truncatedPrefixBytes, and the route ORs the two itself; folding bytes in here
|
|
1047
|
+
// would make a byte-truncated read claim rows were dropped when none were.
|
|
1048
|
+
entriesTruncated: entriesDropped > 0,
|
|
1049
|
+
entriesDropped,
|
|
1050
|
+
// The digest must describe exactly the region the returned rows came from, which
|
|
1051
|
+
// is the post-trim window, not the pre-trim one.
|
|
1052
|
+
prefixDigest: usageRegionDigest(fd, rowsBeginAtBytes, size) ?? "",
|
|
1053
|
+
entryLengths: lengths,
|
|
1054
|
+
trailingSkippedBytes: appendedTrailingSkipped,
|
|
1055
|
+
rowsBeginAtBytes,
|
|
534
1056
|
};
|
|
535
1057
|
} finally {
|
|
536
1058
|
if (fd !== undefined) closeSync(fd);
|
|
@@ -539,8 +1061,10 @@ async function readUsageEntriesFullCooperatively(
|
|
|
539
1061
|
|
|
540
1062
|
/**
|
|
541
1063
|
* Management API reader: full parses yield between bounded batches and concurrent
|
|
542
|
-
* callers share work
|
|
543
|
-
*
|
|
1064
|
+
* callers share work when they observe the same ledger identity and byte window.
|
|
1065
|
+
* Appends keep that identity; replacements (inode/birthtime change) start a new flight.
|
|
1066
|
+
* The parsed tail is retained under the app-owned memory budget so an append reparses
|
|
1067
|
+
* only the appended bytes; the retained rows are evictable and are copied per caller.
|
|
544
1068
|
*/
|
|
545
1069
|
export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_USAGE_MAX_READ_BYTES): Promise<{
|
|
546
1070
|
entries: PersistedUsageEntry[];
|
|
@@ -553,18 +1077,57 @@ export async function readUsageSnapshotForManagement(maxReadBytes = MANAGEMENT_U
|
|
|
553
1077
|
const path = usageLogPath();
|
|
554
1078
|
if (!existsSync(path)) return { entries: [], revision: null, truncatedPrefixBytes: 0, entriesTruncated: false, entriesDropped: 0 };
|
|
555
1079
|
const observed = currentUsageLogRevision();
|
|
556
|
-
const key = `${
|
|
1080
|
+
const key = `${usageLogIdentityKey(observed)}\0${maxReadBytes}`;
|
|
1081
|
+
const observedSize = observed?.size ?? 0;
|
|
557
1082
|
const existing = managementUsageReadInflight;
|
|
558
|
-
|
|
1083
|
+
const replacement = Boolean(existing && observedSize < existing.openedSize);
|
|
1084
|
+
if (!replacement && existing?.key === key && Date.now() - existing.startedAt <= MANAGEMENT_USAGE_FLIGHT_STALE_MS) {
|
|
1085
|
+
const shared = await existing.promise;
|
|
1086
|
+
return { ...shared, entries: shared.entries.slice() };
|
|
1087
|
+
}
|
|
1088
|
+
if (existing && (existing.key !== key || replacement || Date.now() - existing.startedAt > MANAGEMENT_USAGE_FLIGHT_STALE_MS)) {
|
|
1089
|
+
existing.abort.abort(new Error("management usage read superseded"));
|
|
1090
|
+
} else if (existing) {
|
|
559
1091
|
const shared = await existing.promise;
|
|
560
1092
|
return { ...shared, entries: shared.entries.slice() };
|
|
561
1093
|
}
|
|
562
|
-
existing?.abort.abort(new Error("management usage read superseded"));
|
|
563
1094
|
const abort = new AbortController();
|
|
564
|
-
const
|
|
565
|
-
|
|
1095
|
+
const retained = retainedUsageSnapshot;
|
|
1096
|
+
const reusable = retained
|
|
1097
|
+
&& retained.identityKey === usageLogIdentityKey(observed)
|
|
1098
|
+
&& retained.maxReadBytes === maxReadBytes
|
|
1099
|
+
? retained
|
|
1100
|
+
: null;
|
|
1101
|
+
const promise = (async (): Promise<ManagementUsageSnapshot> => {
|
|
1102
|
+
if (reusable) {
|
|
1103
|
+
const incremental = await readUsageEntriesIncrementally(path, abort.signal, maxReadBytes, reusable);
|
|
1104
|
+
if (incremental) return incremental;
|
|
1105
|
+
// The retained rows could not be extended safely; drop them before the full read
|
|
1106
|
+
// so a stale window is never combined with freshly parsed bytes.
|
|
1107
|
+
discardRetainedUsageSnapshot();
|
|
1108
|
+
}
|
|
1109
|
+
return readUsageEntriesFullCooperatively(path, abort.signal, maxReadBytes);
|
|
1110
|
+
})();
|
|
1111
|
+
managementUsageReadInflight = { key, openedSize: observedSize, promise, startedAt: Date.now(), abort };
|
|
566
1112
|
try {
|
|
567
1113
|
const snapshot = await promise;
|
|
1114
|
+
retainedUsageSnapshot = {
|
|
1115
|
+
identityKey: usageLogIdentityKey(snapshot.revision),
|
|
1116
|
+
maxReadBytes,
|
|
1117
|
+
coveredThroughBytes: snapshot.revision.size,
|
|
1118
|
+
prefixDigest: snapshot.prefixDigest,
|
|
1119
|
+
truncatedPrefixBytes: snapshot.truncatedPrefixBytes,
|
|
1120
|
+
entryLengths: snapshot.entryLengths,
|
|
1121
|
+
trailingSkippedBytes: snapshot.trailingSkippedBytes,
|
|
1122
|
+
rowsBeginAtBytes: snapshot.rowsBeginAtBytes,
|
|
1123
|
+
entries: snapshot.entries,
|
|
1124
|
+
entriesTruncated: snapshot.entriesTruncated,
|
|
1125
|
+
entriesDropped: snapshot.entriesDropped,
|
|
1126
|
+
revision: snapshot.revision,
|
|
1127
|
+
retainedAt: Date.now(),
|
|
1128
|
+
approxBytes: retainedUsageSnapshotBytes(snapshot.entries),
|
|
1129
|
+
};
|
|
1130
|
+
enforceAppOwnedMemoryBudget();
|
|
568
1131
|
return { ...snapshot, entries: snapshot.entries.slice() };
|
|
569
1132
|
} finally {
|
|
570
1133
|
if (managementUsageReadInflight?.promise === promise) managementUsageReadInflight = null;
|
|
@@ -614,18 +1177,20 @@ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
|
|
|
614
1177
|
* Read only the newest `limit` usage.jsonl rows without loading the whole append-only
|
|
615
1178
|
* file into memory. Used by request-log hydration on `ocx start`.
|
|
616
1179
|
*/
|
|
617
|
-
export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
|
|
1180
|
+
export function readRecentUsageEntries(limit: number, configDir?: string): PersistedUsageEntry[] {
|
|
618
1181
|
if (!Number.isFinite(limit) || limit <= 0) return [];
|
|
619
|
-
const path = usageLogPath();
|
|
1182
|
+
const path = usageLogPath(configDir);
|
|
620
1183
|
if (!existsSync(path)) return [];
|
|
621
1184
|
let fd: number | undefined;
|
|
622
1185
|
try {
|
|
623
1186
|
fd = openSync(path, "r");
|
|
624
1187
|
const size = fstatSync(fd).size;
|
|
625
1188
|
if (size <= 0) return [];
|
|
626
|
-
//
|
|
627
|
-
|
|
628
|
-
|
|
1189
|
+
// Trace-sized rows need a larger per-row budget than the pre-trace ledger,
|
|
1190
|
+
// but startup hydration must never grow into an unbounded whole-file read.
|
|
1191
|
+
const maxWindowBytes = Math.min(size, RECENT_USAGE_MAX_READ_BYTES);
|
|
1192
|
+
let windowBytes = Math.min(maxWindowBytes, Math.max(64 * 1024, Math.ceil(limit) * 20 * 1024));
|
|
1193
|
+
while (true) {
|
|
629
1194
|
const start = Math.max(0, size - windowBytes);
|
|
630
1195
|
const buf = Buffer.alloc(size - start);
|
|
631
1196
|
readSync(fd, buf, 0, buf.length, start);
|
|
@@ -633,8 +1198,8 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
|
|
|
633
1198
|
if (start > 0) {
|
|
634
1199
|
const nl = text.indexOf("\n");
|
|
635
1200
|
if (nl < 0) {
|
|
636
|
-
if (start === 0) break;
|
|
637
|
-
windowBytes = Math.min(
|
|
1201
|
+
if (start === 0 || windowBytes >= maxWindowBytes) break;
|
|
1202
|
+
windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
|
|
638
1203
|
continue;
|
|
639
1204
|
}
|
|
640
1205
|
text = text.slice(nl + 1);
|
|
@@ -644,8 +1209,8 @@ export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
|
|
|
644
1209
|
// or partial lines are filtered out during parsing and we always return the
|
|
645
1210
|
// most recent N valid rows (not N physical lines minus corrupt ones).
|
|
646
1211
|
const entries = parseUsageLines(lines);
|
|
647
|
-
if (entries.length >= limit || start === 0 || windowBytes >=
|
|
648
|
-
windowBytes = Math.min(
|
|
1212
|
+
if (entries.length >= limit || start === 0 || windowBytes >= maxWindowBytes) return entries.slice(-limit);
|
|
1213
|
+
windowBytes = Math.min(maxWindowBytes, windowBytes * 4);
|
|
649
1214
|
}
|
|
650
1215
|
return [];
|
|
651
1216
|
} catch {
|