@bitkyc08/opencodex 2.80.0 → 2.82.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/README.md +3 -3
- package/bin/ocx.mjs +166 -73
- package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
- package/gui/dist/assets/Tray-BqHF5Trd.js +1 -0
- package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
- package/gui/dist/assets/index-D3FBCNiP.js +94 -0
- package/gui/dist/assets/quota-summary-DTBC8n7-.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/claude-green.svg +1 -0
- package/package.json +3 -2
- package/src/adapters/anthropic-model-contract.ts +8 -2
- package/src/adapters/anthropic.ts +8 -3
- package/src/adapters/codebuddy/mcp-server.ts +2 -25
- package/src/adapters/codebuddy/tool-bridge.ts +1 -7
- package/src/adapters/coding-agent/mcp-server.ts +33 -0
- package/src/adapters/coding-agent/protocol.ts +122 -17
- package/src/adapters/coding-agent/tool-catalog.ts +75 -0
- package/src/adapters/coding-agent/turn.ts +101 -44
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +2 -0
- package/src/adapters/devin/live-models.ts +2 -0
- package/src/adapters/kiro-retry.ts +9 -4
- package/src/adapters/openai-chat/messages.ts +7 -6
- package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -4
- package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
- package/src/adapters/physical-send.ts +8 -5
- package/src/adapters/qoder/adapter.ts +41 -6
- package/src/adapters/qoder/scaffold-guard.ts +2 -3
- package/src/adapters/zed.ts +16 -1
- package/src/claude/agents-inject.ts +14 -3
- package/src/claude/context-windows.ts +19 -17
- package/src/claude/desktop-3p.ts +88 -12
- package/src/claude/desktop-profile.ts +2 -1
- package/src/claude/gateway-cache.ts +18 -1
- package/src/claude/inbound-cache-stabilize.ts +4 -9
- package/src/claude/inbound.ts +78 -3
- package/src/claude/intercept/cli-catalog.ts +3 -0
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/claude/intercept/picker-ca-persistence.ts +7 -6
- package/src/claude/intercept/picker-listener.ts +71 -4
- package/src/claude/intercept/picker-models.ts +14 -6
- package/src/claude/intercept/settings.ts +3 -0
- package/src/claude/long-context.ts +66 -0
- package/src/claude/model-info.ts +19 -16
- package/src/claude/outbound.ts +94 -9
- package/src/claude/subagent-model.ts +40 -11
- package/src/cli/account-anthropic-threshold.ts +14 -5
- package/src/cli/account-api.ts +3 -1
- package/src/cli/account-extended.ts +13 -9
- package/src/cli/account-policy-dto.ts +1 -1
- package/src/cli/account-policy.ts +13 -4
- package/src/cli/account.ts +3 -3
- package/src/cli/agent.ts +9 -5
- package/src/cli/aside-profile-recovery.ts +33 -0
- package/src/cli/capabilities-accounts.ts +2 -2
- package/src/cli/capabilities-agents-routing.ts +24 -0
- package/src/cli/capabilities-base.ts +5 -5
- package/src/cli/capabilities-integrations.ts +18 -2
- package/src/cli/cli-path-diagnostics.ts +132 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/desktop-runtime-guidance.ts +65 -0
- package/src/cli/dispatch.ts +15 -1
- package/src/cli/doctor.ts +16 -3
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +25 -14
- package/src/cli/integration-aside-sync.ts +2 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/integration-preview.ts +8 -1
- package/src/cli/integrations.ts +48 -6
- package/src/cli/message-args.ts +34 -0
- package/src/cli/message-command.ts +43 -0
- package/src/cli/message-runtime.ts +6 -0
- package/src/cli/observe.ts +3 -1
- package/src/cli/provider.ts +29 -6
- package/src/cli/registry.ts +24 -2
- package/src/cli/resolve.ts +25 -2
- package/src/cli/root.ts +4 -2
- package/src/cli/status.ts +64 -12
- package/src/cli/update-restart-child.ts +4 -2
- package/src/cli/update-restart-home.ts +28 -10
- package/src/cli/update-restart-service-record.ts +116 -0
- package/src/cli/update-restart-supervision.ts +96 -0
- package/src/cli/update-restart.ts +59 -13
- package/src/cli/v2.ts +27 -20
- package/src/cli/version-skew-notice.ts +77 -0
- package/src/clients/config-export/commandcode.ts +131 -0
- package/src/clients/config-export/contracts.ts +12 -0
- package/src/clients/config-export.ts +64 -1
- package/src/clients/lazycodex.ts +1 -1
- package/src/clients/omo-role-models.ts +32 -7
- package/src/codex/auth-api/account-list.ts +11 -3
- package/src/codex/auth-api/account-selection.ts +107 -0
- package/src/codex/auth-api/main-account-probe.ts +25 -8
- package/src/codex/auth-api/pool-mode-gate.ts +33 -9
- package/src/codex/auth-api/pool-quota-probe.ts +15 -1
- package/src/codex/auth-api/routes.ts +11 -49
- package/src/codex/auth-context.ts +20 -1
- package/src/codex/autostart-health.ts +12 -3
- package/src/codex/catalog/auto-review.ts +13 -6
- package/src/codex/catalog/build-entries.ts +33 -7
- package/src/codex/catalog/bundled.ts +155 -26
- package/src/codex/catalog/combo-member.ts +3 -2
- package/src/codex/catalog/control-plane.ts +45 -0
- package/src/codex/catalog/effort.ts +37 -6
- package/src/codex/catalog/gather-capture.ts +10 -3
- package/src/codex/catalog/model-hints.ts +15 -8
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/operator-tiers.ts +38 -0
- package/src/codex/catalog/parsing.ts +2 -1
- package/src/codex/catalog/provider-models.ts +58 -44
- package/src/codex/catalog/retained-sync.ts +7 -5
- package/src/codex/catalog/routed-gather.ts +15 -1
- package/src/codex/catalog-auto-refresh-sources.ts +3 -4
- package/src/codex/catalog-auto-refresh.ts +19 -0
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-home-owner.ts +3 -0
- package/src/codex/config-write-lock.ts +333 -0
- package/src/codex/control-plane-models.ts +6 -0
- package/src/codex/convergence.ts +2 -1
- package/src/codex/features.ts +257 -200
- package/src/codex/inject/config-write-section.ts +34 -0
- package/src/codex/inject/multi-agent-v2.ts +14 -6
- package/src/codex/inject/remove.ts +36 -11
- package/src/codex/inject/restore.ts +85 -20
- package/src/codex/inject-coordination.ts +15 -6
- package/src/codex/inject.ts +62 -20
- package/src/codex/journal.ts +62 -17
- package/src/codex/main-account-cache.ts +43 -0
- package/src/codex/paths.ts +2 -0
- package/src/codex/prepared-home.ts +18 -0
- package/src/codex/prompt-journal.ts +25 -12
- package/src/codex/prompt-layers.ts +49 -26
- package/src/codex/prompt-lock-claim.ts +134 -0
- package/src/codex/prompt-lock-evidence.ts +26 -0
- package/src/codex/prompt-lock-io.ts +16 -0
- package/src/codex/prompt-lock-owner.ts +130 -0
- package/src/codex/prompt-lock.ts +84 -53
- package/src/codex/routing/failure-window.ts +146 -0
- package/src/codex/routing/selection.ts +4 -1
- package/src/codex/routing.ts +13 -17
- package/src/codex/runtime.ts +106 -18
- package/src/codex/shim.ts +45 -19
- package/src/combos/jev-decision-contract.ts +25 -2
- package/src/combos/jev-service-exchange.ts +281 -0
- package/src/combos/jev.ts +34 -219
- package/src/combos/types.ts +1 -1
- package/src/config/atomic-write.ts +1 -1
- package/src/config/codex-ws-reuse-setting.ts +10 -0
- package/src/config/diagnostics.ts +27 -16
- package/src/config/live-reconcile.ts +17 -0
- package/src/config/load-degrade.ts +42 -0
- package/src/config/rebase-provenance.ts +11 -0
- package/src/config/schema/anthropic-account-pool.ts +143 -0
- package/src/config/schema/config-schema.ts +11 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +566 -298
- package/src/generated/model-metadata.ts +7 -6
- package/src/grok/inject.ts +50 -4
- package/src/grok/sync.ts +2 -1
- package/src/images/loop.ts +30 -11
- package/src/integrations/aside-profiles.ts +10 -3
- package/src/integrations/catalog-refresh.ts +19 -3
- package/src/integrations/mutation-plan.ts +7 -0
- package/src/integrations/registry.ts +8 -0
- package/src/integrations/state.ts +9 -2
- package/src/integrations/writer.ts +17 -5
- package/src/lib/bun-path-runtime.d.mts +32 -0
- package/src/lib/bun-path-runtime.mjs +95 -0
- package/src/lib/bun-runtime-preflight.ts +140 -0
- package/src/lib/desktop-cli-handoff.d.mts +20 -0
- package/src/lib/desktop-cli-handoff.mjs +92 -0
- package/src/lib/desktop-cli-record.d.mts +29 -0
- package/src/lib/desktop-cli-record.mjs +102 -0
- package/src/lib/lab-live-route-production.ts +9 -1
- package/src/lib/request-execution-budget.ts +244 -58
- package/src/lib/spend-pool-alias-validation.ts +79 -0
- package/src/lib/spend-pool-continuity.ts +34 -0
- package/src/lib/spend-reservation-ledger.ts +412 -50
- package/src/lib/state-store-registrations.ts +20 -3
- package/src/lib/synced-state-location.ts +46 -6
- package/src/lib/test-home-guard.ts +87 -1
- package/src/lib/translator-budget.ts +17 -4
- package/src/lib/upstream-retry.ts +17 -5
- package/src/lib/windows-atomic-replace.ts +38 -16
- package/src/lib/windows-elevation.ts +6 -0
- package/src/lib/windows-secret-acl.ts +92 -25
- package/src/lib/winsw.ts +2 -3
- package/src/lib/workflow-budget.ts +53 -11
- package/src/messaging/budget.ts +45 -0
- package/src/messaging/discovery.ts +57 -0
- package/src/messaging/envelope.ts +49 -0
- package/src/messaging/input.ts +37 -0
- package/src/messaging/rpc.ts +196 -0
- package/src/messaging/send.ts +58 -0
- package/src/messaging/socket.ts +72 -0
- package/src/messaging/types.ts +29 -0
- package/src/oauth/anthropic-account-refusal.ts +90 -18
- package/src/oauth/anthropic-account-threshold.ts +8 -2
- package/src/oauth/anthropic-continuity.ts +15 -1
- package/src/oauth/anthropic-model-quota.ts +120 -48
- package/src/oauth/anthropic-model-routes.ts +11 -3
- package/src/oauth/anthropic-oauth-definitions.ts +98 -0
- package/src/oauth/anthropic-pool-config.ts +30 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +64 -9
- package/src/oauth/anthropic-routing.ts +1202 -866
- package/src/oauth/anthropic-send-ownership.ts +41 -0
- package/src/oauth/anthropic.ts +4 -1
- package/src/oauth/chatgpt.ts +113 -41
- package/src/oauth/generic-account-failover.ts +3 -1
- package/src/oauth/health.ts +8 -6
- package/src/oauth/index.ts +96 -175
- package/src/oauth/kiro.ts +22 -9
- package/src/oauth/login-cli.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +43 -0
- package/src/oauth/pool-kernel.ts +11 -5
- package/src/oauth/pool-settings-capability.ts +5 -3
- package/src/oauth/store-anthropic-instance.ts +87 -0
- package/src/oauth/store.ts +68 -10
- package/src/oauth/token-guardian.ts +2 -0
- package/src/oauth/types.ts +2 -0
- package/src/protocols/plan-snapshot.ts +5 -0
- package/src/protocols/settings.ts +20 -5
- package/src/providers/anthropic-instance-id.ts +47 -0
- package/src/providers/anthropic-instance.ts +48 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +11 -1
- package/src/providers/azure-model-metadata.ts +115 -0
- package/src/providers/derive.ts +23 -0
- package/src/providers/kiro-models.ts +3 -0
- package/src/providers/label.ts +1 -1
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/quota/account-cache.ts +37 -11
- package/src/providers/quota/anthropic-account-quota.ts +93 -0
- package/src/providers/quota/anthropic-cooldown-recovery.ts +222 -111
- package/src/providers/quota/report-cache.ts +24 -1
- package/src/providers/quota/vendor-probes-key.ts +127 -17
- package/src/providers/quota/vendor-probes-oauth.ts +46 -14
- package/src/providers/quota.ts +23 -33
- package/src/providers/registry/entries-core.ts +35 -30
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +7 -4
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/registry.ts +3 -3
- package/src/responses/reasoning-envelope.ts +29 -1
- package/src/responses/reasoning-replay-cache.ts +17 -1
- package/src/responses/spill-store.ts +20 -3
- package/src/responses/state/metrics.ts +95 -0
- package/src/responses/state/snapshot-codec.ts +6 -0
- package/src/responses/state/spill-failure.ts +12 -0
- package/src/responses/state/spill-queue.ts +13 -5
- package/src/responses/state/unforced-store-false.ts +55 -0
- package/src/responses/state.ts +68 -76
- package/src/responses/task-input.ts +17 -0
- package/src/router.ts +28 -4
- package/src/routing/compatibility/assemble.ts +1 -1
- package/src/routing/quota.ts +7 -3
- package/src/server/auth-cors.ts +5 -0
- package/src/server/background-lifecycle.ts +27 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native.ts +64 -28
- package/src/server/claude-messages.ts +59 -8
- package/src/server/gui-static.ts +38 -4
- package/src/server/inbound-body-admission.ts +1 -0
- package/src/server/index/claude-intercept-lifecycle.ts +3 -1
- package/src/server/index/serve-options.ts +10 -2
- package/src/server/index/spend-ledger-lifecycle.ts +12 -5
- package/src/server/index/startup-warnings.ts +19 -0
- package/src/server/index.ts +5 -2
- package/src/server/inference/context.ts +1 -1
- package/src/server/lifecycle.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +123 -38
- package/src/server/management/anthropic-account-threshold.ts +12 -9
- package/src/server/management/anthropic-pool-settings.ts +72 -0
- package/src/server/management/anthropic-reset-grant-routes.ts +98 -39
- package/src/server/management/codex-agent-role-routes.ts +4 -2
- package/src/server/management/codex-prompt-routes.ts +1 -0
- package/src/server/management/codex-role-auto-assign.ts +13 -2
- package/src/server/management/config-routes.ts +85 -14
- package/src/server/management/context.ts +2 -2
- package/src/server/management/decision-routes.ts +2 -1
- package/src/server/management/integration-routes.ts +25 -2
- package/src/server/management/oauth-account-routes.ts +79 -98
- package/src/server/management/provider-instance-ownership.ts +44 -0
- package/src/server/management/provider-routes.ts +21 -28
- package/src/server/management/route-registry.ts +3 -1
- package/src/server/management/routing-profile-routes.ts +4 -4
- package/src/server/management/shared.ts +3 -3
- package/src/server/management/system-restart.ts +44 -14
- package/src/server/management/system-routes.ts +24 -2
- package/src/server/management/vision-sidecar-options.ts +9 -6
- package/src/server/management/web-search-sidecar-options.ts +51 -2
- package/src/server/management-auth.ts +6 -0
- package/src/server/messages-native-eligibility.ts +3 -2
- package/src/server/messages-native-oauth.ts +91 -45
- package/src/server/messages-native-selector.ts +39 -0
- package/src/server/messages-native.ts +133 -38
- package/src/server/messages-response-headers.ts +14 -0
- package/src/server/request-log-conversation.ts +48 -5
- package/src/server/request-log.ts +36 -2
- package/src/server/responses/adapter-continuation.ts +31 -19
- package/src/server/responses/adapter-dispatch.ts +75 -24
- package/src/server/responses/antigravity-validation-refusal.ts +20 -6
- package/src/server/responses/codex-ws-exchange.ts +17 -2
- package/src/server/responses/codex-ws-pool.ts +100 -13
- package/src/server/responses/codex-ws-wire.ts +9 -0
- package/src/server/responses/combo-requested-effort.ts +3 -1
- package/src/server/responses/compact.ts +25 -8
- package/src/server/responses/compaction-recovery.ts +1 -1
- package/src/server/responses/core-codex-account.ts +22 -1
- package/src/server/responses/core-combo-native.ts +24 -4
- package/src/server/responses/core-combo.ts +38 -25
- package/src/server/responses/core-lifetime.ts +3 -36
- package/src/server/responses/core-normalize.ts +1 -0
- package/src/server/responses/core-options.ts +8 -1
- package/src/server/responses/core-replay.ts +78 -0
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/fetch-helpers.ts +26 -3
- package/src/server/responses/passthrough-delivery.ts +30 -4
- package/src/server/responses/passthrough-dispatch.ts +39 -9
- package/src/server/responses/request-prepare.ts +41 -5
- package/src/server/responses/request-send-budget.ts +150 -50
- package/src/server/responses/request-spend.ts +325 -33
- package/src/server/responses/request-transport.ts +132 -52
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +13 -3
- package/src/server/responses/sidecar-execution.ts +113 -22
- package/src/server/responses/sidecar-send-budget.ts +58 -0
- package/src/server/responses/ws-upstream.ts +19 -6
- package/src/server/responses-undeclared-tool-guard.ts +5 -1
- package/src/server/startup-health-cache.ts +2 -1
- package/src/server/workflow-refusal.ts +33 -0
- package/src/service/cli.ts +20 -4
- package/src/service/desktop-command-guard.ts +32 -0
- package/src/service/desktop-startup.ts +66 -40
- package/src/service/desktop-supervision.d.mts +26 -0
- package/src/service/desktop-supervision.mjs +140 -0
- package/src/service/managing-cli.ts +1 -0
- package/src/service/orchestration.ts +46 -20
- package/src/service/ownership-mutation-lease.d.mts +1 -1
- package/src/service/ownership-mutation-lease.mjs +5 -7
- package/src/service/repair.ts +25 -9
- package/src/service/state.ts +4 -3
- package/src/service/systemd.ts +17 -8
- package/src/service/windows-ops.ts +61 -22
- package/src/sidecar/anthropic-binding.ts +79 -0
- package/src/sidecar/auth.ts +76 -2
- package/src/storage/policy-job.ts +5 -1
- package/src/storage/policy-worker.ts +3 -1
- package/src/storage/policy.ts +9 -0
- package/src/types/anthropic-account-pool.ts +33 -0
- package/src/types/config.ts +33 -28
- package/src/types/provider.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/types/tools.ts +7 -0
- package/src/types/wire.ts +1 -0
- package/src/types.ts +4 -0
- package/src/update/index.ts +57 -9
- package/src/update/restart-ownership.ts +10 -3
- package/src/update/runtime-ownership.d.mts +6 -0
- package/src/update/runtime-ownership.mjs +23 -7
- package/src/usage/cost.ts +3 -2
- package/src/usage/expected-prices.ts +66 -14
- package/src/vision/anthropic-describe.ts +16 -3
- package/src/vision/index.ts +3 -0
- package/src/vision/plan.ts +19 -6
- package/src/web-search/alpha-search.ts +7 -1
- package/src/web-search/anthropic-executor.ts +16 -4
- package/src/web-search/index.ts +7 -3
- package/src/web-search/loop.ts +32 -7
- package/src/web-search/passthrough-bridge.ts +4 -1
- package/src/web-search/sidecar-providers.ts +4 -3
- package/gui/dist/assets/App-MbDVfUeC.js +0 -54
- package/gui/dist/assets/Tray-CzkxK82A.js +0 -1
- package/gui/dist/assets/index-UJt6npXk.js +0 -94
- package/gui/dist/assets/quota-summary-DU3sD6Z-.js +0 -1
|
@@ -32,7 +32,11 @@ import {
|
|
|
32
32
|
reconcileOAuthFlowState,
|
|
33
33
|
sweepExpiredXaiPermanentFailureVerdicts,
|
|
34
34
|
} from "../oauth";
|
|
35
|
-
import {
|
|
35
|
+
import { sweepAllAnthropicRoutingHealth, reconcileAnthropicRoutingState } from "../oauth/anthropic-routing";
|
|
36
|
+
import { reconcileAllAnthropicFamilyQuota } from "../oauth/anthropic-model-quota";
|
|
37
|
+
import { reconcileAllAnthropicRatePauses, sweepExpiredAllAnthropicRatePauses } from "../oauth/anthropic-rate-limit-policy";
|
|
38
|
+
import { reconcileAllAnthropicCooldownGenerations } from "../providers/quota/anthropic-cooldown-recovery";
|
|
39
|
+
import { configuredAnthropicInstance } from "../providers/anthropic-instance";
|
|
36
40
|
import { listLiveOAuthAccountKeys, reconcileOAuthReauthState } from "../oauth/store";
|
|
37
41
|
import { reconcileGuardianBackoff } from "../oauth/token-guardian";
|
|
38
42
|
import { sweepExpiredApiKeyCooldowns } from "../providers/key-failover";
|
|
@@ -41,6 +45,7 @@ import { sweepAbandonedResponseStateTemps, sweepExpiredResponseStates, sweepOrph
|
|
|
41
45
|
import { sweepExpiredAntigravityReplay } from "../adapters/google-antigravity-replay";
|
|
42
46
|
import { reconcileProviderAccountQuotaRows } from "../providers/quota";
|
|
43
47
|
import { reconcileRouterWarningMemos } from "../router";
|
|
48
|
+
import { configureSharedSpendLedger, spendPolicyFromConfig } from "./spend-reservation-ledger";
|
|
44
49
|
import type { OcxConfig } from "../types";
|
|
45
50
|
import {
|
|
46
51
|
type GenerationContext,
|
|
@@ -58,19 +63,27 @@ export function setLiveStateStoreConfig(config: OcxConfig): void {
|
|
|
58
63
|
|
|
59
64
|
export function reconcileLiveStateStores() {
|
|
60
65
|
if (!liveServerConfig) return { storesVisited: 0, rowsRemoved: 0 };
|
|
66
|
+
// Only adopted live providers own canonical pools; detached disk snapshots may
|
|
67
|
+
// contain providers the routing instance deliberately has not activated yet.
|
|
68
|
+
configureSharedSpendLedger(spendPolicyFromConfig(liveServerConfig.spend,
|
|
69
|
+
liveServerConfig.spendPoolAliases, Object.keys(liveServerConfig.providers)));
|
|
61
70
|
return reconcileStateGeneration(buildGenerationContext());
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
export function buildGenerationContext(): GenerationContext {
|
|
65
74
|
if (!liveServerConfig) throw new Error("live server config is not installed");
|
|
66
75
|
const providerNames = new Set(Object.keys(liveServerConfig.providers));
|
|
76
|
+
const oauthAccountKeys = new Set(listLiveOAuthAccountKeys(providerNames));
|
|
77
|
+
if (configuredAnthropicInstance(liveServerConfig, "anthropic2") !== "anthropic2") {
|
|
78
|
+
for (const key of oauthAccountKeys) if (key.startsWith("anthropic2\0")) oauthAccountKeys.delete(key);
|
|
79
|
+
}
|
|
67
80
|
return {
|
|
68
81
|
generation: 0,
|
|
69
82
|
providerNames,
|
|
70
83
|
comboIds: new Set(Object.keys(liveServerConfig.combos ?? {})),
|
|
71
84
|
comboTargets: listLiveComboTargetKeys(liveServerConfig),
|
|
72
85
|
codexAccountIds: listLiveCodexAccountIds(liveServerConfig),
|
|
73
|
-
oauthAccountKeys
|
|
86
|
+
oauthAccountKeys,
|
|
74
87
|
configRoots: listLiveConfigOwnershipRoots(getConfigDir()),
|
|
75
88
|
};
|
|
76
89
|
}
|
|
@@ -84,7 +97,11 @@ export const STATE_STORE_REGISTRATIONS = [
|
|
|
84
97
|
sweepExpired: sweepExpiredComboTargetCooldowns,
|
|
85
98
|
reconcileGeneration: reconcileComboTargetCooldowns,
|
|
86
99
|
},
|
|
87
|
-
{ name: "anthropic-routing-health", sweepExpired:
|
|
100
|
+
{ name: "anthropic-routing-health", sweepExpired: sweepAllAnthropicRoutingHealth,
|
|
101
|
+
reconcileGeneration: context => reconcileAnthropicRoutingState(context, liveServerConfig ?? undefined) },
|
|
102
|
+
{ name: "anthropic-family-quota", reconcileGeneration: reconcileAllAnthropicFamilyQuota },
|
|
103
|
+
{ name: "anthropic-rate-pauses", sweepExpired: sweepExpiredAllAnthropicRatePauses, reconcileGeneration: reconcileAllAnthropicRatePauses },
|
|
104
|
+
{ name: "anthropic-cooldown-generations", reconcileGeneration: reconcileAllAnthropicCooldownGenerations },
|
|
88
105
|
{ name: "xai-refresh-verdicts", sweepExpired: sweepExpiredXaiPermanentFailureVerdicts },
|
|
89
106
|
{
|
|
90
107
|
name: "codex-quota-401-recovery",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstatSync, realpathSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
// POSIX path operations on purpose: every path this module classifies is a macOS path. The
|
|
4
4
|
// host's own path module would build backslash or drive-letter paths on Windows and make the
|
|
@@ -15,7 +15,11 @@ const { join, resolve } = posix;
|
|
|
15
15
|
* synced folder turns ordinary requests into intermittent 502s (#6314). This only answers
|
|
16
16
|
* "is it likely synced"; the guard itself stays strict.
|
|
17
17
|
*/
|
|
18
|
-
export type SyncedStateLocation =
|
|
18
|
+
export type SyncedStateLocation =
|
|
19
|
+
| "icloud-drive"
|
|
20
|
+
| "file-provider"
|
|
21
|
+
| "icloud-desktop-documents"
|
|
22
|
+
| "google-drive-desktop-documents";
|
|
19
23
|
|
|
20
24
|
export interface SyncedStateLocationProbe {
|
|
21
25
|
readonly platform?: NodeJS.Platform;
|
|
@@ -24,23 +28,34 @@ export interface SyncedStateLocationProbe {
|
|
|
24
28
|
readonly realpath?: (path: string) => string;
|
|
25
29
|
/** Whether a directory entry exists at the path, without following it. */
|
|
26
30
|
readonly entryExists?: (path: string) => boolean;
|
|
31
|
+
/** Entry names in a directory. An unreadable directory is empty, not an error. */
|
|
32
|
+
readonly listEntries?: (dir: string) => readonly string[];
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
const defaultEntryExists = (path: string): boolean => {
|
|
30
36
|
try { lstatSync(path); return true; } catch { return false; }
|
|
31
37
|
};
|
|
32
38
|
|
|
39
|
+
const defaultListEntries = (dir: string): readonly string[] => {
|
|
40
|
+
try { return readdirSync(dir); } catch { return []; }
|
|
41
|
+
};
|
|
42
|
+
|
|
33
43
|
/**
|
|
34
44
|
* Advisory only. Apple publishes no API a CLI can ask, so this reads the observed layout:
|
|
35
45
|
* iCloud Drive lives under ~/Library/Mobile Documents, File Provider clients (OneDrive, Dropbox,
|
|
36
46
|
* Google Drive) under ~/Library/CloudStorage, and with "Desktop & Documents Folders" turned on
|
|
37
|
-
* iCloud Drive holds a Desktop/Documents entry of its own.
|
|
38
|
-
*
|
|
47
|
+
* iCloud Drive holds a Desktop/Documents entry of its own. Google Drive for desktop can also
|
|
48
|
+
* sync the native Desktop and Documents folders without moving them under CloudStorage (#6314).
|
|
49
|
+
* The cheap signals for that are a DriveFS directory or a `GoogleDrive-*` CloudStorage folder.
|
|
50
|
+
* They do not read which folders the app was told to sync, so Drive being installed can warn
|
|
51
|
+
* even when Documents is not selected. A false positive costs one warning line; nothing is
|
|
52
|
+
* refused on the strength of it.
|
|
39
53
|
*/
|
|
40
54
|
export function syncedStateLocation(dir: string, probe: SyncedStateLocationProbe = {}): SyncedStateLocation | undefined {
|
|
41
55
|
if ((probe.platform ?? process.platform) !== "darwin") return undefined;
|
|
42
56
|
const realpath = probe.realpath ?? ((path: string) => realpathSync.native(path));
|
|
43
57
|
const entryExists = probe.entryExists ?? defaultEntryExists;
|
|
58
|
+
const listEntries = probe.listEntries ?? defaultListEntries;
|
|
44
59
|
const canonical = (path: string): string => {
|
|
45
60
|
try { return realpath(path); } catch { return resolve(path); }
|
|
46
61
|
};
|
|
@@ -56,17 +71,42 @@ export function syncedStateLocation(dir: string, probe: SyncedStateLocationProbe
|
|
|
56
71
|
if (within(mobileDocuments)) return "icloud-drive";
|
|
57
72
|
if (within(join(home, "Library", "CloudStorage"))) return "file-provider";
|
|
58
73
|
for (const folder of ["Desktop", "Documents"]) {
|
|
59
|
-
if (within(join(home, folder))
|
|
60
|
-
|
|
74
|
+
if (!within(join(home, folder))) continue;
|
|
75
|
+
if (entryExists(join(mobileDocuments, "com~apple~CloudDocs", folder))) return "icloud-desktop-documents";
|
|
76
|
+
if (googleDriveAppearsToSyncDesktopDocuments(home, entryExists, listEntries)) {
|
|
77
|
+
return "google-drive-desktop-documents";
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
return undefined;
|
|
64
81
|
}
|
|
65
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Presence only. Drive for desktop keeps its config under DriveFS and, on current macOS,
|
|
85
|
+
* a `GoogleDrive-*` directory in CloudStorage. Either one means the app is in use. Missing
|
|
86
|
+
* or unreadable markers are "not detected", never a startup failure.
|
|
87
|
+
*/
|
|
88
|
+
function googleDriveAppearsToSyncDesktopDocuments(
|
|
89
|
+
home: string,
|
|
90
|
+
entryExists: (path: string) => boolean,
|
|
91
|
+
listEntries: (dir: string) => readonly string[],
|
|
92
|
+
): boolean {
|
|
93
|
+
const driveFs = join(home, "Library", "Application Support", "Google", "DriveFS");
|
|
94
|
+
try {
|
|
95
|
+
if (entryExists(driveFs)) return true;
|
|
96
|
+
} catch { /* unreadable marker */ }
|
|
97
|
+
try {
|
|
98
|
+
return listEntries(join(home, "Library", "CloudStorage"))
|
|
99
|
+
.some((name) => name.toLowerCase().startsWith("googledrive-"));
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
66
105
|
const LOCATION_LABEL: Record<SyncedStateLocation, string> = {
|
|
67
106
|
"icloud-drive": "inside iCloud Drive",
|
|
68
107
|
"file-provider": "inside a cloud-storage (File Provider) folder",
|
|
69
108
|
"icloud-desktop-documents": "in Desktop or Documents, which iCloud Drive appears to sync",
|
|
109
|
+
"google-drive-desktop-documents": "in Desktop or Documents, which Google Drive appears to sync",
|
|
70
110
|
};
|
|
71
111
|
|
|
72
112
|
/** The startup warning. Names the location kind, never the path. */
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* how this incident happened.
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
23
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
24
24
|
import { realpathSync } from "node:fs";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
|
|
@@ -31,6 +31,8 @@ const GUARD_ENV = "OCX_TEST_HOME_GUARD";
|
|
|
31
31
|
* loads, so the true home is only knowable from this hand-off.
|
|
32
32
|
*/
|
|
33
33
|
const REAL_HOME_ENV = "OCX_REAL_HOME";
|
|
34
|
+
/** Set by `scripts/test.ts` to the developer's own Claude config directory before it sandboxes one. */
|
|
35
|
+
const REAL_CLAUDE_CONFIG_DIR_ENV = "OCX_REAL_CLAUDE_CONFIG_DIR";
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Resolve symlinks so two spellings of one location compare equal — macOS hands out
|
|
@@ -62,6 +64,25 @@ function canonicalize(path: string): string {
|
|
|
62
64
|
const REAL_HOME = process.env[REAL_HOME_ENV]?.trim() || homedir();
|
|
63
65
|
const PROTECTED_HOME = canonicalize(join(REAL_HOME, ".opencodex"));
|
|
64
66
|
const PROTECTED_CODEX_HOME = canonicalize(join(REAL_HOME, ".codex"));
|
|
67
|
+
/**
|
|
68
|
+
* The developer's Claude config directory: the default `<real home>/.claude`, plus their own
|
|
69
|
+
* `CLAUDE_CONFIG_DIR` when they set one. `scripts/test.ts` hands the original over as
|
|
70
|
+
* `OCX_REAL_CLAUDE_CONFIG_DIR` because its child already starts with a sandboxed
|
|
71
|
+
* `CLAUDE_CONFIG_DIR`; a bare run imports this module before the preload rewrites the
|
|
72
|
+
* environment, so `CLAUDE_CONFIG_DIR` still holds the developer's value here.
|
|
73
|
+
*
|
|
74
|
+
* Claude agent sync prunes generated `agents/ocx-*.md`, and a bare `bun test` once did that
|
|
75
|
+
* to a live directory: Bun's `os.homedir()` ignores a HOME rewritten after startup (#6775).
|
|
76
|
+
*/
|
|
77
|
+
const REAL_CLAUDE_CONFIG_DIRS = [...new Set([
|
|
78
|
+
join(REAL_HOME, ".claude"),
|
|
79
|
+
process.env[REAL_CLAUDE_CONFIG_DIR_ENV]?.trim() || process.env.CLAUDE_CONFIG_DIR?.trim() || join(REAL_HOME, ".claude"),
|
|
80
|
+
])];
|
|
81
|
+
const PROTECTED_CLAUDE_TREES = REAL_CLAUDE_CONFIG_DIRS.map(path => ({
|
|
82
|
+
path: canonicalize(path),
|
|
83
|
+
lexical: resolve(path),
|
|
84
|
+
label: "the real Claude config directory",
|
|
85
|
+
}));
|
|
65
86
|
/**
|
|
66
87
|
* `~/Library/LaunchAgents` needs its own entry because HOME isolation does not reach it:
|
|
67
88
|
* `os.homedir()` reads the password database, not `$HOME`, so a macOS test that rewrites
|
|
@@ -83,6 +104,11 @@ export function protectedCodexHomeForTests(): string {
|
|
|
83
104
|
return PROTECTED_CODEX_HOME;
|
|
84
105
|
}
|
|
85
106
|
|
|
107
|
+
/** The Claude config directories this process protects. Exported for the guard's own tests. */
|
|
108
|
+
export function protectedClaudeConfigDirsForTests(): readonly string[] {
|
|
109
|
+
return PROTECTED_CLAUDE_TREES.map(tree => tree.path);
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
export function isTestHomeGuardArmed(): boolean {
|
|
87
113
|
return process.env[GUARD_ENV] === "1";
|
|
88
114
|
}
|
|
@@ -155,6 +181,54 @@ export function assertNotRealCodexHomeUnderTest(dir: string): void {
|
|
|
155
181
|
);
|
|
156
182
|
}
|
|
157
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Throw when an armed test process is about to write or prune inside the real Claude config
|
|
186
|
+
* directory. Unlike the OpenCodex and Codex homes this refuses descendants too, and callers pass
|
|
187
|
+
* every directory they are about to touch (the config root AND its `agents`/`cache` child), so a
|
|
188
|
+
* sandbox child that is a link into the real directory is judged by where it resolves.
|
|
189
|
+
*
|
|
190
|
+
* The protected roots are resolved at check time rather than at import, so a custom directory
|
|
191
|
+
* created after this module loaded still compares by its current identity. On macOS and Windows,
|
|
192
|
+
* whose default filesystems ignore case, paths compare case-insensitively: `.CLAUDE` is the same
|
|
193
|
+
* directory there. Comparing too broadly only refuses more, which is the safe direction here.
|
|
194
|
+
*
|
|
195
|
+
* Call FIRST, outside any best-effort catch, so a refusal is visible and nothing is touched.
|
|
196
|
+
*/
|
|
197
|
+
export function assertNotRealClaudeConfigUnderTest(...paths: string[]): void {
|
|
198
|
+
if (!isTestHomeGuardArmed()) return;
|
|
199
|
+
const roots = currentClaudeConfigRoots();
|
|
200
|
+
for (const path of paths) {
|
|
201
|
+
for (const candidate of [canonicalize(path), resolve(path)]) {
|
|
202
|
+
for (const root of roots) {
|
|
203
|
+
if (!isSameOrInsideIgnoringPlatformCase(root, candidate)) continue;
|
|
204
|
+
throw new Error(
|
|
205
|
+
`refusing to write the real Claude config directory (${root}) from a test process. `
|
|
206
|
+
+ "Set CLAUDE_CONFIG_DIR to a temp directory, or pass an explicit config directory "
|
|
207
|
+
+ "(for a server, startServer's managementApi.claudeAgentConfigDir).",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function currentClaudeConfigRoots(): string[] {
|
|
215
|
+
return REAL_CLAUDE_CONFIG_DIRS.flatMap(path => [canonicalize(path), resolve(path)]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function foldPlatformCase(path: string): string {
|
|
219
|
+
return process.platform === "darwin" || process.platform === "win32" ? path.toLowerCase() : path;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isSameOrInsideIgnoringPlatformCase(root: string, candidate: string): boolean {
|
|
223
|
+
const parent = foldPlatformCase(root);
|
|
224
|
+
const child = foldPlatformCase(candidate);
|
|
225
|
+
if (parent === child) return true;
|
|
226
|
+
const rel = relative(parent, child);
|
|
227
|
+
// Only a leading ".." COMPONENT leaves the root, split on this platform's separator: a child
|
|
228
|
+
// named "..fixture", or "..\\fixture" on POSIX where a backslash is a filename character, is inside.
|
|
229
|
+
return rel !== "" && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
158
232
|
/**
|
|
159
233
|
* The trees a removal must never reach, and the reason each one is named.
|
|
160
234
|
*
|
|
@@ -172,6 +246,7 @@ const PROTECTED_TREES: ReadonlyArray<{ path: string; lexical: string; label: str
|
|
|
172
246
|
lexical: resolve(join(REAL_HOME, "Library", "LaunchAgents")),
|
|
173
247
|
label: "the real LaunchAgents directory",
|
|
174
248
|
},
|
|
249
|
+
...PROTECTED_CLAUDE_TREES,
|
|
175
250
|
];
|
|
176
251
|
const PROTECTED_REAL_HOME = canonicalize(REAL_HOME);
|
|
177
252
|
const LEXICAL_REAL_HOME = resolve(REAL_HOME);
|
|
@@ -258,6 +333,17 @@ export function protectedRemovalReason(target: string): string | null {
|
|
|
258
333
|
if (isInside(candidate, protectedPath)) return `an ancestor of ${tree.label} (${protectedPath})`;
|
|
259
334
|
}
|
|
260
335
|
}
|
|
336
|
+
// The Claude directory is judged like its writer guard: roots resolved now, case folded
|
|
337
|
+
// where the filesystem folds it, so an alias created after import is still refused.
|
|
338
|
+
for (const root of currentClaudeConfigRoots()) {
|
|
339
|
+
if (isSameOrInsideIgnoringPlatformCase(root, candidate)) {
|
|
340
|
+
// Same narrow lift as the trees above: content of the running checkout when that
|
|
341
|
+
// checkout itself lives inside the directory. Never the root itself.
|
|
342
|
+
if (foldPlatformCase(root) !== foldPlatformCase(candidate) && isOwnCheckoutContent(root, candidate)) continue;
|
|
343
|
+
return `the real Claude config directory or a path inside it (${root})`;
|
|
344
|
+
}
|
|
345
|
+
if (isSameOrInsideIgnoringPlatformCase(candidate, root)) return `an ancestor of the real Claude config directory (${root})`;
|
|
346
|
+
}
|
|
261
347
|
}
|
|
262
348
|
return null;
|
|
263
349
|
}
|
|
@@ -191,6 +191,7 @@ class Budget implements TranslatorBudget {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
openCall(id: string): void {
|
|
194
|
+
if (this.disposed) return;
|
|
194
195
|
if (!this.calls.has(id)) {
|
|
195
196
|
this.calls.set(id, { logicalBytes: 0, physicalBytes: 0 });
|
|
196
197
|
aggregateActiveCalls += 1;
|
|
@@ -210,6 +211,7 @@ class Budget implements TranslatorBudget {
|
|
|
210
211
|
bytes: number,
|
|
211
212
|
scope: { kind: TranslatorBufferKind; callId?: string },
|
|
212
213
|
): TranslatorTransientReservation {
|
|
214
|
+
if (this.disposed) return { commitRetained() {}, release() {} };
|
|
213
215
|
const admitted = normalizeBytes(bytes);
|
|
214
216
|
if (scope.callId && admitted > this.maxCallArgumentBytes) {
|
|
215
217
|
this.reject("tool_args", this.maxCallArgumentBytes);
|
|
@@ -222,6 +224,7 @@ class Budget implements TranslatorBudget {
|
|
|
222
224
|
return {
|
|
223
225
|
commitRetained: () => {
|
|
224
226
|
if (state !== "reserved") return;
|
|
227
|
+
if (this.disposed) { state = "released"; return; }
|
|
225
228
|
state = "committed";
|
|
226
229
|
if (!scope.callId) return;
|
|
227
230
|
const call = this.calls.get(scope.callId) ?? { logicalBytes: 0, physicalBytes: 0 };
|
|
@@ -234,6 +237,7 @@ class Budget implements TranslatorBudget {
|
|
|
234
237
|
release: () => {
|
|
235
238
|
if (state !== "reserved") return;
|
|
236
239
|
state = "released";
|
|
240
|
+
if (this.disposed) return;
|
|
237
241
|
this.charged.set(scope.kind, Math.max(0, (this.charged.get(scope.kind) ?? 0) - admitted));
|
|
238
242
|
this.hardChargedBytes = Math.max(0, this.hardChargedBytes - admitted);
|
|
239
243
|
this.changed();
|
|
@@ -242,6 +246,7 @@ class Budget implements TranslatorBudget {
|
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
chargeRetained(delta: number, scope: { kind: TranslatorBufferKind; callId?: string }): void {
|
|
249
|
+
if (this.disposed) return;
|
|
245
250
|
const admitted = normalizeBytes(delta);
|
|
246
251
|
if (scope.callId) {
|
|
247
252
|
const call = this.calls.get(scope.callId) ?? { logicalBytes: 0, physicalBytes: 0 };
|
|
@@ -279,6 +284,7 @@ class Budget implements TranslatorBudget {
|
|
|
279
284
|
}
|
|
280
285
|
|
|
281
286
|
private observe(bytes: number): () => void {
|
|
287
|
+
if (this.disposed) return () => {};
|
|
282
288
|
const observed = normalizeBytes(bytes);
|
|
283
289
|
this.observedBytes += observed;
|
|
284
290
|
this.changed();
|
|
@@ -369,7 +375,7 @@ export function resetTranslatorAggregateForTests(): void {
|
|
|
369
375
|
aggregateOverflows = 0;
|
|
370
376
|
}
|
|
371
377
|
|
|
372
|
-
export function finalizeTranslatorBudgetResponse(response: Response, budget: TranslatorBudget): Response {
|
|
378
|
+
export function finalizeTranslatorBudgetResponse(response: Response, budget: TranslatorBudget, signal?: AbortSignal): Response {
|
|
373
379
|
if (!response.body) {
|
|
374
380
|
budget.dispose();
|
|
375
381
|
return response;
|
|
@@ -379,9 +385,12 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
|
|
|
379
385
|
const finalize = () => {
|
|
380
386
|
if (finalized) return;
|
|
381
387
|
finalized = true;
|
|
388
|
+
signal?.removeEventListener("abort", finalize);
|
|
382
389
|
budget.dispose();
|
|
383
390
|
};
|
|
384
|
-
|
|
391
|
+
// The transport owns cancellation and terminal precedence. Abort releases only
|
|
392
|
+
// accounting here, including when no consumer ever pulls or cancels this body.
|
|
393
|
+
const body = new ReadableStream<Uint8Array>({
|
|
385
394
|
async pull(controller) {
|
|
386
395
|
try {
|
|
387
396
|
const result = await reader.read();
|
|
@@ -395,7 +404,11 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
|
|
|
395
404
|
}
|
|
396
405
|
},
|
|
397
406
|
async cancel(reason) {
|
|
398
|
-
|
|
407
|
+
finalize();
|
|
408
|
+
await reader.cancel(reason);
|
|
399
409
|
},
|
|
400
|
-
})
|
|
410
|
+
});
|
|
411
|
+
if (signal?.aborted) finalize();
|
|
412
|
+
else signal?.addEventListener("abort", finalize, { once: true });
|
|
413
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
401
414
|
}
|
|
@@ -508,7 +508,11 @@ export interface ResetRetryOptions {
|
|
|
508
508
|
* `rebuildAndRefetch` recovery kind whose provider has no transient policy -- was not merely
|
|
509
509
|
* uncounted but UNCOUNTABLE: the callback existed on a type those call sites never reach.
|
|
510
510
|
*/
|
|
511
|
-
onSendsConsumed?: (sends: number) => void
|
|
511
|
+
onSendsConsumed?: ((sends: number) => void) & {
|
|
512
|
+
beforeSend?: () => boolean;
|
|
513
|
+
close?: () => void;
|
|
514
|
+
execute?: <T>(run: () => Promise<T>) => Promise<T>;
|
|
515
|
+
};
|
|
512
516
|
/**
|
|
513
517
|
* Spend one operator-granted replacement for a pre-header reset this helper would otherwise
|
|
514
518
|
* refuse. Absent means no operator policy, which is the fail-closed answer.
|
|
@@ -629,6 +633,7 @@ export async function fetchWithResetRetry(
|
|
|
629
633
|
// more send on every recovery leg, which is most of what made a bounded per-layer retry
|
|
630
634
|
// compose into an unbounded per-request count.
|
|
631
635
|
if (attempts === 0) throw new SendBudgetExhaustedError(opts.label);
|
|
636
|
+
try {
|
|
632
637
|
let lastError: unknown;
|
|
633
638
|
let sawReset = false;
|
|
634
639
|
// True once this leg has spent the request's operator allowance. From that point the leg
|
|
@@ -640,9 +645,13 @@ export async function fetchWithResetRetry(
|
|
|
640
645
|
// Reported before the await, one physical send at a time: a send that rejects has still
|
|
641
646
|
// been made, and this helper leaves through four exits (return, reset give-up, non-reset
|
|
642
647
|
// rethrow, abort), so a per-send report is the only shape that is correct on all of them.
|
|
643
|
-
opts.onSendsConsumed?.(
|
|
648
|
+
if (opts.onSendsConsumed?.beforeSend && !opts.onSendsConsumed.beforeSend()) throw new SendBudgetExhaustedError(opts.label);
|
|
649
|
+
if (!opts.onSendsConsumed?.beforeSend) opts.onSendsConsumed?.(1);
|
|
644
650
|
try {
|
|
645
|
-
|
|
651
|
+
let response: Response;
|
|
652
|
+
try { const run = () => doFetch(attempt === 0 ? firstRecovery : "connection-reset");
|
|
653
|
+
response = await (opts.onSendsConsumed?.execute ? opts.onSendsConsumed.execute(run) : run()); }
|
|
654
|
+
finally { if (opts.onSendsConsumed?.beforeSend) opts.onSendsConsumed(1); }
|
|
646
655
|
return spentOperatorReplacement ? settleOperatorReplacement(response) : response;
|
|
647
656
|
} catch (err) {
|
|
648
657
|
if (opts.abortSignal?.aborted) throw err;
|
|
@@ -686,6 +695,7 @@ export async function fetchWithResetRetry(
|
|
|
686
695
|
}
|
|
687
696
|
}
|
|
688
697
|
throw lastError ?? new Error("upstream fetch failed");
|
|
698
|
+
} finally { opts.onSendsConsumed?.close?.(); }
|
|
689
699
|
}
|
|
690
700
|
|
|
691
701
|
/**
|
|
@@ -717,8 +727,10 @@ export async function fetchWithTransientRetry(
|
|
|
717
727
|
const countedFetch: ReplayableFetch = (recovery) => {
|
|
718
728
|
// Incremented BEFORE the await so a rejected send still consumes budget; counting only
|
|
719
729
|
// successes would let a reset storm loop without bound.
|
|
730
|
+
if (opts.onSendsConsumed?.beforeSend && !opts.onSendsConsumed.beforeSend()) throw new SendBudgetExhaustedError(opts.label);
|
|
720
731
|
sent += 1;
|
|
721
|
-
|
|
732
|
+
const run = () => doFetch(recovery);
|
|
733
|
+
return opts.onSendsConsumed?.execute ? opts.onSendsConsumed.execute(run) : run();
|
|
722
734
|
};
|
|
723
735
|
// No floor. A spent budget hands the inner helper 0, which refuses rather than buying one
|
|
724
736
|
// more send -- the loop condition alone was never enough, because every later recovery leg
|
|
@@ -789,7 +801,7 @@ export async function fetchWithTransientRetry(
|
|
|
789
801
|
// Budget exhausted: the last response is returned with its body intact.
|
|
790
802
|
return res;
|
|
791
803
|
} finally {
|
|
792
|
-
opts.onSendsConsumed?.(sent);
|
|
804
|
+
try { opts.onSendsConsumed?.(sent); } finally { opts.onSendsConsumed?.close?.(); }
|
|
793
805
|
}
|
|
794
806
|
}
|
|
795
807
|
|
|
@@ -37,6 +37,9 @@ export type ReplacePublisher =
|
|
|
37
37
|
| "storage-cleanup"
|
|
38
38
|
| "tray";
|
|
39
39
|
|
|
40
|
+
/** Called immediately before every rename attempt, including each Windows retry. */
|
|
41
|
+
export interface RenameValidationHooks { validateBeforeRename?: (destination: string) => void }
|
|
42
|
+
|
|
40
43
|
/** The Windows error codes this module treats as a momentary hold. */
|
|
41
44
|
export type ReplaceRetryCode = "EBUSY" | "EPERM" | "EACCES";
|
|
42
45
|
|
|
@@ -97,7 +100,7 @@ const MAX_RETRIES = 2;
|
|
|
97
100
|
* Windows sharing violations only, returning the code so the caller can record
|
|
98
101
|
* which one. Any other error is the caller's to see, immediately.
|
|
99
102
|
*/
|
|
100
|
-
function transientWindowsReplaceCode(
|
|
103
|
+
export function transientWindowsReplaceCode(
|
|
101
104
|
platform: NodeJS.Platform,
|
|
102
105
|
error: unknown,
|
|
103
106
|
): ReplaceRetryCode | null {
|
|
@@ -106,6 +109,32 @@ function transientWindowsReplaceCode(
|
|
|
106
109
|
return code === "EBUSY" || code === "EPERM" || code === "EACCES" ? code : null;
|
|
107
110
|
}
|
|
108
111
|
|
|
112
|
+
/** One filesystem operation plus its retry observers, carried as members (not free callbacks). */
|
|
113
|
+
export interface WindowsFileOperationStep<T> {
|
|
114
|
+
run: () => T;
|
|
115
|
+
onRetry?: (code: ReplaceRetryCode, exhausted: boolean) => void;
|
|
116
|
+
beforeAttempt?: () => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Bounded sharing-violation retries for one filesystem operation, never a transaction. */
|
|
120
|
+
export function retryWindowsFileOperation<T>(
|
|
121
|
+
step: WindowsFileOperationStep<T>,
|
|
122
|
+
io: Pick<AtomicRenameIO, "platform" | "sleep"> = { platform: process.platform, sleep: Bun.sleepSync },
|
|
123
|
+
): T {
|
|
124
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
125
|
+
step.beforeAttempt?.();
|
|
126
|
+
try { return step.run(); }
|
|
127
|
+
catch (error) {
|
|
128
|
+
const code = transientWindowsReplaceCode(io.platform, error);
|
|
129
|
+
if (!code) throw error;
|
|
130
|
+
const exhausted = attempt >= MAX_RETRIES;
|
|
131
|
+
step.onRetry?.(code, exhausted);
|
|
132
|
+
if (exhausted) throw error;
|
|
133
|
+
io.sleep(25 * (attempt + 1));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
109
138
|
export function renameAtomicFile(
|
|
110
139
|
source: string,
|
|
111
140
|
destination: string,
|
|
@@ -115,30 +144,23 @@ export function renameAtomicFile(
|
|
|
115
144
|
sleep: Bun.sleepSync,
|
|
116
145
|
},
|
|
117
146
|
publisher: ReplacePublisher = "config",
|
|
147
|
+
hooks: RenameValidationHooks = {},
|
|
118
148
|
): void {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const code = transientWindowsReplaceCode(io.platform, error);
|
|
125
|
-
if (!code) throw error;
|
|
126
|
-
if (attempt >= MAX_RETRIES) {
|
|
127
|
-
bump(publisher, code, "exhausted");
|
|
128
|
-
throw error;
|
|
129
|
-
}
|
|
130
|
-
bump(publisher, code, "retried");
|
|
131
|
-
io.sleep(25 * (attempt + 1));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
149
|
+
retryWindowsFileOperation({
|
|
150
|
+
run: () => io.rename(source, destination),
|
|
151
|
+
onRetry: (code, exhausted) => bump(publisher, code, exhausted ? "exhausted" : "retried"),
|
|
152
|
+
beforeAttempt: () => hooks.validateBeforeRename?.(destination),
|
|
153
|
+
}, io);
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
export async function renameAtomicFileAsync(
|
|
137
157
|
source: string,
|
|
138
158
|
destination: string,
|
|
139
159
|
publisher: ReplacePublisher = "config",
|
|
160
|
+
hooks: RenameValidationHooks = {},
|
|
140
161
|
): Promise<void> {
|
|
141
162
|
for (let attempt = 0; ; attempt += 1) {
|
|
163
|
+
hooks.validateBeforeRename?.(destination);
|
|
142
164
|
try {
|
|
143
165
|
renameSync(source, destination);
|
|
144
166
|
return;
|
|
@@ -203,6 +203,12 @@ export function resolveTrustedWindowsPowerShellExe(): string {
|
|
|
203
203
|
return assertTrustedSystemExecutable(candidate, "PowerShell");
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/** Absolute System32\\reg.exe path; PATH, SystemRoot and WINDIR cannot select it. */
|
|
207
|
+
export function resolveTrustedWindowsRegExe(): string {
|
|
208
|
+
const candidate = join(resolveTrustedWindowsSystemDirectory(), "reg.exe");
|
|
209
|
+
return assertTrustedSystemExecutable(candidate, "reg.exe");
|
|
210
|
+
}
|
|
211
|
+
|
|
206
212
|
/** Absolute path to System32\\schtasks.exe from a trusted system directory. */
|
|
207
213
|
export function resolveTrustedWindowsSchtasksExe(): string {
|
|
208
214
|
if (elevationExeOverridesForTests?.schtasks) {
|