@bitkyc08/opencodex 2.86.0 → 2.87.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/gui/dist/assets/App-ZfYbLHkB.js +58 -0
- package/gui/dist/assets/App-wT3mXqFz.css +1 -0
- package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
- package/gui/dist/assets/index-CfYDMj34.js +94 -0
- package/gui/dist/assets/index-DPeB9HpV.css +1 -0
- package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +9 -8
- package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
- package/src/adapters/anthropic/passthrough.ts +7 -23
- package/src/adapters/anthropic-progress.ts +84 -0
- package/src/adapters/anthropic.ts +108 -21
- package/src/adapters/base.ts +2 -0
- package/src/adapters/claude-cli/adapter.ts +111 -23
- package/src/adapters/claude-cli/profiles.ts +2 -1
- package/src/adapters/claude-cli/stable-replay.ts +108 -0
- package/src/adapters/claude-cli/usage-admission.ts +230 -0
- package/src/adapters/coding-agent/protocol.ts +1 -1
- package/src/adapters/coding-agent/turn.ts +18 -2
- package/src/adapters/dsh-account.ts +97 -0
- package/src/adapters/google.ts +35 -8
- package/src/adapters/openai-chat/messages.ts +4 -3
- package/src/adapters/openai-chat/passthrough.ts +8 -2
- package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
- package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
- package/src/adapters/openai-responses/passthrough.ts +57 -125
- package/src/adapters/openai-responses/request-strips.ts +25 -3
- package/src/adapters/openai-responses/transport.ts +81 -0
- package/src/adapters/qoder/adapter.ts +4 -2
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/zed.ts +26 -1
- package/src/advisor/activity.ts +55 -0
- package/src/advisor/budget.ts +62 -0
- package/src/advisor/consult.ts +4 -1
- package/src/advisor/context.ts +16 -4
- package/src/advisor/evidence-tool.ts +21 -0
- package/src/advisor/evidence.ts +117 -0
- package/src/advisor/runtime.ts +79 -31
- package/src/advisor/settings.ts +11 -2
- package/src/bridge/errors.ts +19 -1
- package/src/bridge/internal.ts +3 -1
- package/src/bridge/response-json.ts +18 -3
- package/src/bridge/sse.ts +58 -8
- package/src/bridge/terminal-refusal.ts +59 -0
- package/src/claude/desktop-3p-library.ts +32 -7
- package/src/claude/desktop-3p.ts +55 -13
- package/src/claude/desktop-first-party.ts +1 -1
- package/src/claude/desktop-picker-platform.ts +4 -0
- package/src/claude/desktop-picker.ts +15 -8
- package/src/claude/desktop-system-proxy.ts +50 -4
- package/src/claude/inbound-content-options.ts +11 -0
- package/src/claude/inbound.ts +33 -0
- package/src/claude/intercept/picker-ca-startup.ts +2 -1
- package/src/claude/intercept/picker-runtime.ts +6 -5
- package/src/claude/intercept/picker-trust-windows.ts +338 -0
- package/src/claude/intercept/picker-trust.ts +19 -0
- package/src/claude/intercept/port-policy.ts +30 -0
- package/src/claude/intercept/runtime.ts +97 -94
- package/src/claude/outbound.ts +38 -13
- package/src/cli/account-api.ts +1 -0
- package/src/cli/account-policy-dto.ts +6 -0
- package/src/cli/account-policy.ts +34 -5
- package/src/cli/account-preservation.ts +44 -0
- package/src/cli/account.ts +8 -2
- package/src/cli/advisor.ts +17 -3
- package/src/cli/capabilities-accounts.ts +20 -1
- package/src/cli/capabilities-agents-routing.ts +5 -2
- package/src/cli/capabilities-base.ts +38 -10
- package/src/cli/capabilities-observe-system.ts +63 -3
- package/src/cli/capabilities-provider-models.ts +11 -0
- package/src/cli/codex-cli-update.ts +151 -41
- package/src/cli/codex-provider.ts +44 -0
- package/src/cli/combo-input.ts +10 -1
- package/src/cli/combo-result.ts +1 -1
- package/src/cli/combo.ts +2 -1
- package/src/cli/cross-home-owner.ts +17 -14
- package/src/cli/dispatch.ts +25 -4
- package/src/cli/doctor.ts +6 -1
- package/src/cli/ensure-desktop-cold-start.ts +60 -7
- package/src/cli/help.ts +3 -2
- package/src/cli/index.ts +1 -1
- package/src/cli/integration-plan-dto.ts +1 -0
- package/src/cli/log-history.ts +172 -0
- package/src/cli/models-runtime-subcommands.ts +1 -0
- package/src/cli/models-runtime.ts +72 -8
- package/src/cli/observe-stream.ts +2 -0
- package/src/cli/observe.ts +16 -4
- package/src/cli/provider.ts +27 -5
- package/src/cli/registry.ts +28 -8
- package/src/cli/runtime-api.ts +5 -3
- package/src/cli/system-command.ts +40 -8
- package/src/cli/uninstall-client-state.ts +32 -0
- package/src/cli/usage-compare.ts +309 -0
- package/src/cli/usage-model-search.ts +4 -4
- package/src/cli/usage-report.ts +2 -2
- package/src/cli/v2.ts +1 -1
- package/src/client/machine-listener.ts +1 -3
- package/src/client/runtime.ts +1 -0
- package/src/clients/client-path.ts +51 -0
- package/src/clients/config-export/contracts.ts +2 -1
- package/src/clients/config-export.ts +18 -44
- package/src/clients/gentle-shell-path.ts +87 -0
- package/src/codex/account-auto-switch.ts +2 -0
- package/src/codex/account-identity.ts +29 -0
- package/src/codex/account-label.ts +1 -1
- package/src/codex/account-lifecycle.ts +21 -5
- package/src/codex/account-preservation.ts +28 -0
- package/src/codex/account-use-remaining.ts +23 -0
- package/src/codex/app-server-processes.ts +97 -7
- package/src/codex/app-server-restart-service.ts +55 -4
- package/src/codex/auth-api/account-list.ts +4 -0
- package/src/codex/auth-api/account-pause-group.ts +6 -50
- package/src/codex/auth-api/account-preservation.ts +57 -0
- package/src/codex/auth-api/routes.ts +8 -0
- package/src/codex/auth-api/use-remaining-route.ts +36 -0
- package/src/codex/auth-api.ts +0 -1
- package/src/codex/auth-context.ts +2 -2
- package/src/codex/catalog/auto-review.ts +20 -3
- package/src/codex/catalog/build-entries.ts +9 -1
- package/src/codex/catalog/combo-member.ts +14 -5
- package/src/codex/catalog/configured-tiers.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +11 -9
- package/src/codex/catalog/model-hints.ts +21 -16
- package/src/codex/catalog/model-visibility.ts +3 -1
- package/src/codex/catalog/parsing.ts +30 -86
- package/src/codex/catalog/provider-models.ts +36 -34
- package/src/codex/catalog/routed-gather.ts +12 -13
- package/src/codex/cli-install-provenance.ts +105 -17
- package/src/codex/cli-update-plan.ts +545 -0
- package/src/codex/config-write-lock.ts +23 -2
- package/src/codex/desktop-app/picker-freshness.ts +96 -0
- package/src/codex/desktop-app-restart.ts +21 -0
- package/src/codex/desktop-compatibility/installed-build.ts +24 -6
- package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
- package/src/codex/desktop-compatibility/runtime.ts +12 -3
- package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
- package/src/codex/features.ts +85 -45
- package/src/codex/inject/history-failure.ts +27 -0
- package/src/codex/inject/restore.ts +24 -2
- package/src/codex/inject/routing-classify.ts +4 -3
- package/src/codex/inject.ts +18 -27
- package/src/codex/main-account-hard-lock.ts +30 -1
- package/src/codex/native-profile-startup.ts +1 -1
- package/src/codex/ocx-compaction-history.ts +12 -10
- package/src/codex/paths.ts +86 -1
- package/src/codex/provider-routing.ts +174 -0
- package/src/codex/refresh.ts +2 -2
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/selection.ts +4 -3
- package/src/codex/routing/thread-affinity.ts +1 -1
- package/src/codex/signed-in-identity.ts +43 -0
- package/src/codex/subagent-model-fallback.ts +1 -1
- package/src/codex/sync.ts +6 -1
- package/src/codex/user-identity.ts +16 -5
- package/src/codex/windows-local-appdata.ts +84 -0
- package/src/combos/failover.ts +6 -1
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +46 -10
- package/src/combos/types.ts +15 -0
- package/src/config/diagnostics.ts +33 -6
- package/src/config/live-reconcile.ts +38 -2
- package/src/config/load-degrade.ts +17 -0
- package/src/config/opencode-wire-upgrade.ts +54 -0
- package/src/config/opencode-zen-upgrade.ts +52 -0
- package/src/config/owner-registry.ts +22 -17
- package/src/config/provider-validation.ts +45 -2
- package/src/config/schema/config-schema.ts +22 -0
- package/src/config/schema/leaf-validators.ts +38 -9
- package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
- package/src/config/schema/native-reasoning-retention.ts +26 -0
- package/src/config.ts +2 -0
- package/src/generated/compatibility-version.json +608 -284
- package/src/grok/sync.ts +1 -1
- package/src/images/loop.ts +46 -8
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +8 -0
- package/src/lib/codex-restart-contract.ts +15 -0
- package/src/lib/desktop-upstream-tunnel.ts +18 -53
- package/src/lib/empty-tool-catalog.ts +62 -0
- package/src/lib/errors.ts +19 -3
- package/src/lib/gcp-adc.ts +96 -9
- package/src/lib/gcp-credential-input.ts +12 -0
- package/src/lib/lab-live-route-production.ts +5 -0
- package/src/lib/opaque-proxy-tunnel.ts +217 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/account-quota-rank.ts +69 -2
- package/src/oauth/anthropic-account-refusal.ts +20 -2
- package/src/oauth/anthropic-oauth-definitions.ts +3 -1
- package/src/oauth/anthropic-routing.ts +1 -21
- package/src/oauth/anthropic.ts +38 -7
- package/src/oauth/callback-server.ts +27 -1
- package/src/oauth/chatgpt-device.ts +1 -1
- package/src/oauth/chatgpt-shared.ts +83 -0
- package/src/oauth/chatgpt.ts +8 -72
- package/src/oauth/dsh.ts +203 -0
- package/src/oauth/generic-account-failover.ts +33 -7
- package/src/oauth/generic-session-affinity.ts +125 -0
- package/src/oauth/index.ts +184 -109
- package/src/oauth/local-token-detect.ts +21 -1
- package/src/oauth/login-cli-errors.ts +44 -0
- package/src/oauth/login-cli.ts +69 -7
- package/src/oauth/login-flow-state.ts +371 -23
- package/src/oauth/meta-muse-errors.ts +41 -0
- package/src/oauth/meta-muse-keychain.ts +52 -0
- package/src/oauth/meta-muse.ts +51 -84
- package/src/oauth/model-discovery-auth.ts +3 -0
- package/src/oauth/pool-settings-capability.ts +4 -2
- package/src/oauth/refresh-flight-retirement.ts +48 -0
- package/src/oauth/session-affinity-key.ts +23 -0
- package/src/oauth/store-anthropic-instance.ts +9 -2
- package/src/oauth/store.ts +12 -5
- package/src/protocols/encoders/adapter-events.ts +5 -4
- package/src/protocols/encoders/messages.ts +9 -6
- package/src/protocols/provider-summary.ts +2 -1
- package/src/providers/api-key-cooldowns.ts +49 -0
- package/src/providers/api-key-resolve.ts +11 -6
- package/src/providers/api-key-response-ownership.ts +57 -0
- package/src/providers/api-key-selection.ts +31 -6
- package/src/providers/api-keys.ts +214 -3
- package/src/providers/claude-cli-usage.ts +96 -0
- package/src/providers/dsh-account-destination.ts +31 -0
- package/src/providers/dsh-account-models.ts +41 -0
- package/src/providers/gcp-credential-cleanup.ts +59 -0
- package/src/providers/key-failover.ts +54 -50
- package/src/providers/key-store.ts +7 -2
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/model-rename-migration.ts +2 -2
- package/src/providers/openai-tiers-destination.ts +11 -2
- package/src/providers/openai-tiers.ts +5 -12
- package/src/providers/opencode-go-transport.ts +3 -1
- package/src/providers/quota/account-cache.ts +6 -5
- package/src/providers/quota/account-row.ts +34 -0
- package/src/providers/quota/antigravity.ts +28 -7
- package/src/providers/quota/dsh-account.ts +70 -0
- package/src/providers/quota/vendor-probes-oauth.ts +1 -93
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +14 -20
- package/src/providers/registry/entries-core.ts +37 -8
- package/src/providers/registry/types.ts +1 -1
- package/src/providers/request-pacing.ts +77 -19
- package/src/providers/routed-config.ts +339 -0
- package/src/providers/service-tier.ts +2 -1
- package/src/quota/window-mapping.ts +5 -2
- package/src/responses/anthropic-progress.ts +34 -0
- package/src/responses/assistant-replay.ts +35 -0
- package/src/responses/compaction-history-framing.ts +65 -0
- package/src/responses/compaction.ts +6 -1
- package/src/responses/incomplete-reason.ts +31 -0
- package/src/responses/parser.ts +50 -5
- package/src/responses/reasoning-envelope.ts +4 -0
- package/src/responses/reasoning-replay-cache.ts +24 -2
- package/src/responses/reasoning-retention.ts +173 -0
- package/src/responses/retained-compaction.ts +46 -0
- package/src/responses/schema.ts +23 -0
- package/src/router.ts +8 -321
- package/src/routing/health.ts +37 -22
- package/src/routing/history/indexer.ts +6 -5
- package/src/routing/history/ingest.ts +6 -2
- package/src/routing/profile.ts +1 -1
- package/src/server/admission-model-scope.ts +17 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +7 -1
- package/src/server/chat-native-reasoning.ts +123 -0
- package/src/server/chat-native-sse.ts +110 -7
- package/src/server/chat-native.ts +76 -17
- package/src/server/claude-messages.ts +55 -414
- package/src/server/effort-policy.ts +1 -1
- package/src/server/images.ts +15 -2
- package/src/server/index/model-alias-id.ts +36 -0
- package/src/server/index/optional-listeners.ts +2 -0
- package/src/server/index/serve-options.ts +9 -1
- package/src/server/index/startup-warnings.ts +2 -0
- package/src/server/index.ts +1 -1
- package/src/server/inference/advisor-diagnostics.ts +31 -0
- package/src/server/management/advisor-routes.ts +15 -4
- package/src/server/management/claude-desktop-picker-routes.ts +1 -1
- package/src/server/management/combo-routes.ts +176 -50
- package/src/server/management/companion-routes.ts +1 -1
- package/src/server/management/config-routes.ts +75 -4
- package/src/server/management/cursor-integration-routes.ts +1 -1
- package/src/server/management/model-routes.ts +11 -6
- package/src/server/management/model-rows.ts +14 -0
- package/src/server/management/native-integration-routes.ts +2 -7
- package/src/server/management/native-reasoning-retention-routes.ts +41 -0
- package/src/server/management/oauth-account-routes.ts +87 -14
- package/src/server/management/provider-gcp-credential-routes.ts +29 -0
- package/src/server/management/provider-key-pacing-routes.ts +31 -0
- package/src/server/management/provider-overwrite-carry.ts +7 -0
- package/src/server/management/provider-routes.ts +25 -30
- package/src/server/management/route-registry.ts +10 -1
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native-tool-guard.ts +185 -0
- package/src/server/messages-native.ts +101 -21
- package/src/server/messages-shared.ts +431 -0
- package/src/server/relay.ts +1 -1
- package/src/server/request-log-terminal-status.ts +1 -1
- package/src/server/request-log.ts +6 -1
- package/src/server/responses/account-change-state.ts +8 -2
- package/src/server/responses/adapter-continuation.ts +13 -1
- package/src/server/responses/adapter-delivery.ts +19 -6
- package/src/server/responses/adapter-dispatch.ts +19 -2
- package/src/server/responses/advisor-retention.ts +117 -0
- package/src/server/responses/advisor-slot.ts +73 -41
- package/src/server/responses/codex-ws-wire.ts +15 -8
- package/src/server/responses/compact.ts +303 -125
- package/src/server/responses/compaction-recovery.ts +18 -10
- package/src/server/responses/compaction-retention.ts +61 -0
- package/src/server/responses/context-overflow.ts +11 -6
- package/src/server/responses/core-auth.ts +6 -0
- package/src/server/responses/core-codex-account.ts +11 -1
- package/src/server/responses/core-combo-failure.ts +2 -0
- package/src/server/responses/core-combo.ts +120 -36
- package/src/server/responses/core-errors.ts +3 -3
- package/src/server/responses/core-lifetime.ts +5 -2
- package/src/server/responses/core-opaque-recovery.ts +35 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-pipeline.ts +141 -0
- package/src/server/responses/core-replay.ts +18 -2
- package/src/server/responses/core.ts +2 -115
- package/src/server/responses/fetch-helpers.ts +73 -19
- package/src/server/responses/main-hard-lock-routing.ts +109 -0
- package/src/server/responses/native-response-control.ts +4 -2
- package/src/server/responses/passthrough-delivery.ts +3 -1
- package/src/server/responses/passthrough-dispatch.ts +15 -13
- package/src/server/responses/request-prepare.ts +76 -12
- package/src/server/responses/request-sidecar-auth.ts +2 -2
- package/src/server/responses/request-transport.ts +104 -27
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/run-turn-execution.ts +96 -30
- package/src/server/responses/sidecar-execution.ts +20 -2
- package/src/server/responses/terminal-guard.ts +2 -16
- package/src/service/auth-preflight.ts +71 -0
- package/src/service/cli.ts +4 -2
- package/src/service/definition.ts +97 -0
- package/src/service/guards.ts +1 -61
- package/src/service/health-sentinel-linux.ts +112 -0
- package/src/service/health-sentinel-main.ts +76 -57
- package/src/service/health.ts +5 -89
- package/src/service/launchd.ts +27 -7
- package/src/service/logging.ts +72 -9
- package/src/service/repair.ts +2 -1
- package/src/service/state.ts +7 -8
- package/src/service/systemd.ts +1 -1
- package/src/service/windows-guarded-manager.ts +2 -1
- package/src/service/windows-ops.ts +1 -1
- package/src/service/windows-scheduler.ts +2 -7
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service/windows-wscript.ts +14 -0
- package/src/service.ts +4 -2
- package/src/sidecar/auth.ts +2 -0
- package/src/types/config.ts +64 -3
- package/src/types/opencode-go.ts +61 -0
- package/src/types/opencode-zen.ts +29 -0
- package/src/types/provider.ts +32 -6
- package/src/types/request.ts +23 -2
- package/src/types/wire.ts +29 -37
- package/src/types.ts +1 -0
- package/src/update/npm-invocation.mjs +12 -2
- package/src/usage/cost.ts +48 -12
- package/src/usage/expected-prices.ts +3 -0
- package/src/usage/ledger-retention.ts +2 -1
- package/src/usage/ledger-scanner.ts +6 -5
- package/src/usage/log.ts +17 -8
- package/src/usage/summary.ts +22 -9
- package/src/usage/user-cost-overlays.ts +14 -19
- package/src/usage/user-cost-validation.ts +38 -0
- package/src/vision/eligibility.ts +13 -0
- package/src/web-search/loop.ts +55 -5
- package/src/web-search/progress-stream.ts +4 -1
- package/src/web-search/run-turn-loop.ts +9 -1
- package/gui/dist/assets/App-BjlttVLy.js +0 -58
- package/gui/dist/assets/App-CSJvmjNs.css +0 -1
- package/gui/dist/assets/index-BB6uK6tU.css +0 -1
- package/gui/dist/assets/index-BMssP4k3.js +0 -94
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* targeting it is the caller's explicit act.
|
|
21
21
|
*/
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
|
+
import { gentleShellPaths } from "./gentle-shell-path";
|
|
24
|
+
import { absoluteClientPath, ClientPathError, piAgentDir } from "./client-path";
|
|
25
|
+
export { ClientPathError, piAgentDir } from "./client-path";
|
|
23
26
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
24
27
|
import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
|
|
25
28
|
import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
|
|
@@ -213,21 +216,6 @@ function ompProfileName(env: OpencodeLaunchEnv): string | undefined {
|
|
|
213
216
|
return profile;
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
/**
|
|
217
|
-
* Pi resolves its agent directory from `PI_CODING_AGENT_DIR`, falling back to
|
|
218
|
-
* `~/.pi/agent`. `ompAgentDir` below already reads that variable — OMP is a Pi
|
|
219
|
-
* derivative — so Pi's own resolver honoring it is what makes the two agree
|
|
220
|
-
* rather than a new claim about Pi's contract.
|
|
221
|
-
*
|
|
222
|
-
* A relative override is refused for the same reason MCode's and ZCode's are: a
|
|
223
|
-
* background proxy and a foreground client can have different working
|
|
224
|
-
* directories, and would otherwise disagree about which file is named.
|
|
225
|
-
*/
|
|
226
|
-
export function piAgentDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
227
|
-
const override = env.PI_CODING_AGENT_DIR?.trim();
|
|
228
|
-
if (override) return absoluteClientPath(override, home, "PI_CODING_AGENT_DIR");
|
|
229
|
-
return join(home, ".pi", "agent");
|
|
230
|
-
}
|
|
231
219
|
|
|
232
220
|
/** Pi's canonical custom-provider catalog. */
|
|
233
221
|
export function piConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
@@ -289,35 +277,6 @@ export function hermesConfigPath(env: OpencodeLaunchEnv = process.env, home: str
|
|
|
289
277
|
return join(hermesHomeDir(env, home), "config.yaml");
|
|
290
278
|
}
|
|
291
279
|
|
|
292
|
-
/**
|
|
293
|
-
* Expand `~` against the effective home; REFUSE anything still relative.
|
|
294
|
-
*
|
|
295
|
-
* A first attempt at this called `resolve()` and claimed the path would "mean
|
|
296
|
-
* the same thing next time". It does not: `resolve()` only anchors the current
|
|
297
|
-
* invocation, so applying from one directory and disabling from another still
|
|
298
|
-
* resolved two different files — the second reported "not applied" and left
|
|
299
|
-
* the managed block behind with nothing claiming it.
|
|
300
|
-
*
|
|
301
|
-
* There is no honest way to recover the other process's cwd, so a relative
|
|
302
|
-
* selector is refused at the boundary instead of being silently anchored to
|
|
303
|
-
* whichever directory we happened to start in. A `~` path IS stable, because
|
|
304
|
-
* it anchors to the effective home rather than the cwd.
|
|
305
|
-
*/
|
|
306
|
-
export class ClientPathError extends Error {}
|
|
307
|
-
|
|
308
|
-
function absoluteClientPath(raw: string, home: string, variable: string): string {
|
|
309
|
-
const trimmed = raw.trim();
|
|
310
|
-
if (trimmed === "~") return home;
|
|
311
|
-
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return join(home, trimmed.slice(2));
|
|
312
|
-
if (!isAbsolute(trimmed)) {
|
|
313
|
-
throw new ClientPathError(
|
|
314
|
-
`${variable} must be an absolute path or start with ~; "${trimmed}" depends on the working directory, `
|
|
315
|
-
+ "so opencodex and the client would disagree about which file it names.",
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
return trimmed;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
280
|
/**
|
|
322
281
|
* OpenClaw's EFFECTIVE home: `OPENCLAW_HOME` outranks the OS home.
|
|
323
282
|
*
|
|
@@ -1711,6 +1670,21 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1711
1670
|
buildContribution: buildDroidContribution,
|
|
1712
1671
|
loopbackOnly: true,
|
|
1713
1672
|
},
|
|
1673
|
+
"gentle-shell": {
|
|
1674
|
+
id: "gentle-shell",
|
|
1675
|
+
filename: "gentle-shell-models.json",
|
|
1676
|
+
destination: env => gentleShellPaths(env).configPath,
|
|
1677
|
+
apiKeyEnv: "",
|
|
1678
|
+
exportHint: "Gentle Shell uses its persistent selected home; restart the shell to load the catalog. Link/shared homes use the Pi integration.",
|
|
1679
|
+
build: ctx => buildPiClientConfig(ctx, PI_EXPORT_OPTIONS),
|
|
1680
|
+
format: "json",
|
|
1681
|
+
summarize: summarizePi,
|
|
1682
|
+
buildContribution: ctx => {
|
|
1683
|
+
const doc = buildPiClientConfig(ctx, PI_EXPORT_OPTIONS);
|
|
1684
|
+
return singleFragment("gentle-shell", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1685
|
+
},
|
|
1686
|
+
loopbackOnly: true,
|
|
1687
|
+
},
|
|
1714
1688
|
};
|
|
1715
1689
|
|
|
1716
1690
|
export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
|
|
5
|
+
import { ClientPathError, piAgentDir } from "./client-path";
|
|
6
|
+
|
|
7
|
+
interface BootstrapIO {
|
|
8
|
+
readText(path: string): { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed" };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function absolute(raw: string, label: string): string {
|
|
12
|
+
// Unlike Pi's resolver, the Shell launcher does not expand a literal '~'.
|
|
13
|
+
if (!isAbsolute(raw)) throw new ClientPathError(`${label} must be an absolute path; the proxy cannot infer the launcher's working directory.`);
|
|
14
|
+
return resolve(raw);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readBootstrap(path: string, io?: BootstrapIO): string | undefined {
|
|
18
|
+
try {
|
|
19
|
+
if (!io) return readBoundedRegularFileSync(path, 64 * 1024).toString("utf8");
|
|
20
|
+
const read = io.readText(path);
|
|
21
|
+
if (read.kind === "missing") return undefined;
|
|
22
|
+
if (read.kind === "text" && Buffer.byteLength(read.text, "utf8") <= 64 * 1024) return read.text;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
25
|
+
}
|
|
26
|
+
throw new ClientPathError(`Cannot read Gentle Shell launcher config at ${path}; no catalog was selected.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolve existing ancestors too, so a not-yet-created catalog cannot hide a directory alias. */
|
|
30
|
+
function canonical(path: string): string {
|
|
31
|
+
const missing: string[] = [];
|
|
32
|
+
let current = resolve(path);
|
|
33
|
+
for (;;) {
|
|
34
|
+
try {
|
|
35
|
+
const value = join(realpathSync.native(current), ...missing);
|
|
36
|
+
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT" || dirname(current) === current) {
|
|
39
|
+
throw new ClientPathError(`Cannot verify Gentle Shell destination ${path}.`);
|
|
40
|
+
}
|
|
41
|
+
missing.unshift(basename(current));
|
|
42
|
+
current = dirname(current);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameFile(left: string, right: string): boolean {
|
|
48
|
+
if (canonical(left) === canonical(right)) return true;
|
|
49
|
+
// Hard-linked catalogs can have different canonical names but the same identity.
|
|
50
|
+
try {
|
|
51
|
+
const a = statSync(left, { bigint: true });
|
|
52
|
+
const b = statSync(right, { bigint: true });
|
|
53
|
+
return a.ino !== 0n && a.dev === b.dev && a.ino === b.ino;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
56
|
+
throw new ClientPathError("Cannot verify Gentle Shell and Pi catalog identities.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Persistent selectors only: a proxy cannot observe one-off `gentle-shell --home` flags. */
|
|
61
|
+
export function gentleShellPaths(
|
|
62
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
63
|
+
home: string = homedir(),
|
|
64
|
+
io?: BootstrapIO,
|
|
65
|
+
): { configPath: string; detectDir: string } {
|
|
66
|
+
const config = env.GENTLE_SHELL_CONFIG
|
|
67
|
+
? absolute(env.GENTLE_SHELL_CONFIG, "GENTLE_SHELL_CONFIG")
|
|
68
|
+
: join(home, ".gentle-shell", "config.json");
|
|
69
|
+
const text = readBootstrap(config, io);
|
|
70
|
+
let selector: unknown;
|
|
71
|
+
try {
|
|
72
|
+
const parsed: unknown = text === undefined ? undefined : JSON.parse(text);
|
|
73
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) selector = (parsed as Record<string, unknown>).home;
|
|
74
|
+
} catch { /* The launcher also falls back to isolated for malformed JSON. */ }
|
|
75
|
+
const shared = "Gentle Shell uses a Pi destination; use the Pi integration for link/shared homes.";
|
|
76
|
+
if (selector === "link") throw new ClientPathError(shared);
|
|
77
|
+
const dir = typeof selector === "string" && selector.length > 0 && selector !== "isolated"
|
|
78
|
+
? absolute(selector, "Gentle Shell saved home")
|
|
79
|
+
: env.GENTLE_SHELL_HOME
|
|
80
|
+
? absolute(env.GENTLE_SHELL_HOME, "GENTLE_SHELL_HOME")
|
|
81
|
+
: join(home, ".gentle-shell", "agent");
|
|
82
|
+
const configPath = join(dir, "models.json");
|
|
83
|
+
const piHomes = [join(home, ".pi", "agent"), piAgentDir(env, home)];
|
|
84
|
+
if (env.GENTLE_SHELL_USER_PI_HOME) piHomes.push(absolute(env.GENTLE_SHELL_USER_PI_HOME, "GENTLE_SHELL_USER_PI_HOME"));
|
|
85
|
+
if (piHomes.some(pi => sameFile(configPath, join(pi, "models.json")))) throw new ClientPathError(shared);
|
|
86
|
+
return { configPath, detectDir: dir };
|
|
87
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
2
|
import { deleteConfigObjectChildKey } from "../config/rebase-provenance";
|
|
3
3
|
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
4
|
+
import { codexAccountUsesRemainingQuota } from "./account-use-remaining";
|
|
4
5
|
|
|
5
6
|
export const DEFAULT_CODEX_AUTO_SWITCH_THRESHOLD = 80;
|
|
6
7
|
export const MIN_CODEX_AUTO_SWITCH_THRESHOLD = 0;
|
|
@@ -34,6 +35,7 @@ export function getEffectiveCodexAutoSwitchThreshold(
|
|
|
34
35
|
config: OcxConfig,
|
|
35
36
|
accountId: string,
|
|
36
37
|
): number {
|
|
38
|
+
if (codexAccountUsesRemainingQuota(config, accountId)) return 0;
|
|
37
39
|
const override = getCodexAccountAutoSwitchThresholdOverride(config, accountId);
|
|
38
40
|
if (override !== null) return override;
|
|
39
41
|
return config.autoSwitchThreshold ?? DEFAULT_CODEX_AUTO_SWITCH_THRESHOLD;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { codexLoginIdentity as identity, codexTokenIdentity, type CodexLoginIdentity } from "./signed-in-identity";
|
|
3
|
+
import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "./account-id";
|
|
4
|
+
import type { CodexTokenReadResult } from "./auth-collision";
|
|
5
|
+
import { getCodexAccountCredential } from "./account-store";
|
|
6
|
+
|
|
7
|
+
/** Match existing entries from an already-owned main observation, without exposing identity fields. */
|
|
8
|
+
export function linkedCodexAccountIds(config: OcxConfig, selectedId: string, main: CodexTokenReadResult | { status: "api-key-only" }): string[] | undefined {
|
|
9
|
+
// Unknown main identity must not produce a successful but incomplete pause.
|
|
10
|
+
if (main.status === "unreadable" || main.status === "invalid") return undefined;
|
|
11
|
+
const identities = new Map<string, CodexLoginIdentity | undefined>();
|
|
12
|
+
if (main.status === "ok") {
|
|
13
|
+
const selected = codexTokenIdentity(main.tokens);
|
|
14
|
+
if (selected === null) return undefined;
|
|
15
|
+
identities.set(MAIN_CODEX_ACCOUNT_ID, selected);
|
|
16
|
+
}
|
|
17
|
+
for (const account of config.codexAccounts ?? []) {
|
|
18
|
+
if (!isSelectableCodexPoolAccount(account)) continue;
|
|
19
|
+
const credential = getCodexAccountCredential(account.id);
|
|
20
|
+
identities.set(account.id, identity(credential?.chatgptAccountId, account.email));
|
|
21
|
+
}
|
|
22
|
+
const selected = identities.get(selectedId);
|
|
23
|
+
// Missing identity evidence never links unrelated logins, including members of one workspace.
|
|
24
|
+
if (!selected) return [selectedId];
|
|
25
|
+
return [selectedId, ...[...identities].flatMap(([id, candidate]) => (
|
|
26
|
+
id !== selectedId && candidate?.accountId === selected.accountId && candidate.email === selected.email
|
|
27
|
+
? [id] : []
|
|
28
|
+
))];
|
|
29
|
+
}
|
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import type { CodexAccount, OcxConfig } from "../types";
|
|
3
3
|
import type { CodexAuthContext } from "./auth-context";
|
|
4
4
|
import type { ProviderApiKeySelection } from "../types/provider";
|
|
5
|
-
import { MAIN_CODEX_ACCOUNT_ID } from "./
|
|
5
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
6
6
|
|
|
7
7
|
export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
|
|
8
8
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { observeSignedInCodexIdentity } from "./signed-in-identity";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import {
|
|
3
4
|
deleteConfigTopLevelKey,
|
|
@@ -8,17 +9,20 @@ import {
|
|
|
8
9
|
import { captureConfigTopLevelRollback } from "../config/rebase-provenance";
|
|
9
10
|
import { removeCodexAccountCredential } from "./account-store";
|
|
10
11
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
11
|
-
import {
|
|
12
|
+
import { readCodexTokensResult } from "./auth-collision";
|
|
12
13
|
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
13
14
|
import { clearAccountQuota } from "./quota";
|
|
14
|
-
import {
|
|
15
|
+
import { forgetManualPreference } from "./routing/active-account";
|
|
16
|
+
import { deleteAllHealthForAccount } from "./routing/health-store";
|
|
17
|
+
import { clearThreadAccountMapForAccount } from "./routing/thread-affinity";
|
|
15
18
|
|
|
16
19
|
import { clearCodexPoolRefreshFailure } from "./pool-refresh-backoff";
|
|
17
20
|
import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
18
21
|
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaCredential, observeMainQuotaIdentity } from "./main-account-cache";
|
|
19
|
-
import { extractAccountIdClaims } from "../oauth/chatgpt";
|
|
22
|
+
import { extractAccountId, extractAccountIdClaims } from "../oauth/chatgpt";
|
|
20
23
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
21
24
|
import { forgetCodexAccountCreditUse } from "./account-credit-use";
|
|
25
|
+
import { setCodexAccountUseRemainingQuota } from "./account-use-remaining";
|
|
22
26
|
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
23
27
|
import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
|
|
24
28
|
import { forgetCodexAccountAutoSwitchThreshold } from "./account-auto-switch";
|
|
@@ -45,7 +49,11 @@ export function purgeCodexAccountRuntimeState(accountId: string): void {
|
|
|
45
49
|
clearAccountNeedsReauth(accountId);
|
|
46
50
|
clearAccountQuota(accountId);
|
|
47
51
|
clearThreadAccountMapForAccount(accountId);
|
|
48
|
-
|
|
52
|
+
// Deliberately not via `./routing`: the account-delete path only needs the health rows and
|
|
53
|
+
// the manual preference gone, and importing the router here closed a cycle through
|
|
54
|
+
// routing/health-store and routing/active-account.
|
|
55
|
+
deleteAllHealthForAccount(accountId);
|
|
56
|
+
forgetManualPreference(accountId);
|
|
49
57
|
clearCodexPoolRefreshFailure(accountId);
|
|
50
58
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
51
59
|
clearMainAccountInfoCache();
|
|
@@ -66,7 +74,11 @@ function purgeMainCodexAccountRuntimeState(): void {
|
|
|
66
74
|
* cooldown, quota, reauth, and thread affinity do not leak across accounts.
|
|
67
75
|
*/
|
|
68
76
|
export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
69
|
-
const
|
|
77
|
+
const read = readCodexTokensResult();
|
|
78
|
+
const currentAccountId = read.status === "ok"
|
|
79
|
+
? extractAccountId(read.tokens.id_token, read.tokens.access_token) ?? (read.tokens.account_id || null)
|
|
80
|
+
: null;
|
|
81
|
+
if (read.status === "ok") observeSignedInCodexIdentity(read.tokens);
|
|
70
82
|
// A missing/malformed auth.json is an unknown identity, not a confirmed account switch. Keep the
|
|
71
83
|
// prior observation and its safety state until a real account id can be read again.
|
|
72
84
|
if (currentAccountId === null) return false;
|
|
@@ -110,6 +122,7 @@ export function initializeMainAccountPolicyBinding(authPath: string): boolean {
|
|
|
110
122
|
const previousAccountId = observedMainChatgptAccountId;
|
|
111
123
|
observedMainChatgptAccountId = accountId;
|
|
112
124
|
if (previousAccountId !== undefined && previousAccountId !== accountId) purgeMainCodexAccountRuntimeState();
|
|
125
|
+
observeSignedInCodexIdentity(tokens);
|
|
113
126
|
observeMainQuotaIdentity(accountId);
|
|
114
127
|
return observeMainQuotaCredential(tokens.access_token, accountId) !== undefined;
|
|
115
128
|
}
|
|
@@ -122,6 +135,7 @@ export function applyConfirmedMainCodexAccountTransition(
|
|
|
122
135
|
fromAccountId: string,
|
|
123
136
|
toAccountId: string,
|
|
124
137
|
): boolean {
|
|
138
|
+
observeSignedInCodexIdentity();
|
|
125
139
|
if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) {
|
|
126
140
|
if (toAccountId) {
|
|
127
141
|
observedMainChatgptAccountId = toAccountId;
|
|
@@ -137,6 +151,7 @@ export function applyConfirmedMainCodexAccountTransition(
|
|
|
137
151
|
|
|
138
152
|
export function resetMainCodexAccountIdentityTrackingForTests(): void {
|
|
139
153
|
observedMainChatgptAccountId = undefined;
|
|
154
|
+
observeSignedInCodexIdentity();
|
|
140
155
|
clearMainAccountCredentialPresence();
|
|
141
156
|
}
|
|
142
157
|
|
|
@@ -183,6 +198,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
183
198
|
forgetCodexAccountCreditUse(runtimeConfig, accountId);
|
|
184
199
|
forgetCodexAccountPriority(runtimeConfig, accountId);
|
|
185
200
|
forgetCodexAccountAutoSwitchThreshold(runtimeConfig, accountId);
|
|
201
|
+
setCodexAccountUseRemainingQuota(runtimeConfig, accountId, false);
|
|
186
202
|
if (runtimeConfig.codexQuotaAutoRefresh?.[accountId]) {
|
|
187
203
|
const retained = { ...runtimeConfig.codexQuotaAutoRefresh };
|
|
188
204
|
delete retained[accountId];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { MAIN_CODEX_ACCOUNT_ID, isSelectableCodexPoolAccount } from "./account-id";
|
|
3
|
+
import { getCodexAccountCredential } from "./account-store";
|
|
4
|
+
import { codexAccountPriorityLookup } from "./account-priority";
|
|
5
|
+
import { MIN_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY } from "./pool-rotation";
|
|
6
|
+
import { codexLoginIdentity, getSignedInCodexIdentity, sameCodexLogin } from "./signed-in-identity";
|
|
7
|
+
|
|
8
|
+
/** Resolve current rows afresh: a pool replacement must not inherit the prior login's role. */
|
|
9
|
+
export function signedInCodexAccountIds(config: OcxConfig): ReadonlySet<string> {
|
|
10
|
+
const ids = new Set([MAIN_CODEX_ACCOUNT_ID]);
|
|
11
|
+
const main = getSignedInCodexIdentity();
|
|
12
|
+
if (!main) return ids;
|
|
13
|
+
for (const account of config.codexAccounts ?? []) {
|
|
14
|
+
if (!isSelectableCodexPoolAccount(account)) continue;
|
|
15
|
+
const credential = getCodexAccountCredential(account.id);
|
|
16
|
+
if (sameCodexLogin(main, codexLoginIdentity(credential?.chatgptAccountId, account.email))) ids.add(account.id);
|
|
17
|
+
}
|
|
18
|
+
return ids;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Two disjoint bands, preserving custom order inside each; never writes the stored priorities. */
|
|
22
|
+
export function codexSelectionPriorityLookup(config: OcxConfig): (id: string) => number {
|
|
23
|
+
const priority = codexAccountPriorityLookup(config);
|
|
24
|
+
if (config.codexPreserveSignedInAccount !== true) return priority;
|
|
25
|
+
const reserved = signedInCodexAccountIds(config);
|
|
26
|
+
const band = MAX_ACCOUNT_PRIORITY - MIN_ACCOUNT_PRIORITY + 1;
|
|
27
|
+
return id => priority(id) - (reserved.has(id) ? band : 0);
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
3
|
+
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
4
|
+
|
|
5
|
+
type Policy = Pick<OcxConfig, "codexUseRemainingQuotaAccountIds">;
|
|
6
|
+
|
|
7
|
+
/** Included-quota preference only; never grants permission to spend credits. */
|
|
8
|
+
export function codexAccountUsesRemainingQuota(config: Policy | undefined, accountId: string): boolean {
|
|
9
|
+
return config?.codexUseRemainingQuotaAccountIds?.includes(accountId) ?? false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validRemainingQuotaAccountIds(value: unknown): value is string[] {
|
|
13
|
+
return Array.isArray(value) && value.every(id => id === MAIN_CODEX_ACCOUNT_ID || isValidCodexAccountId(id));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Preserve the underlying thresholds so turning this off restores their current values. */
|
|
17
|
+
export function setCodexAccountUseRemainingQuota(config: OcxConfig, accountId: string, enabled: boolean): void {
|
|
18
|
+
const ids = new Set(config.codexUseRemainingQuotaAccountIds ?? []);
|
|
19
|
+
if (enabled) ids.add(accountId);
|
|
20
|
+
else ids.delete(accountId);
|
|
21
|
+
if (ids.size) config.codexUseRemainingQuotaAccountIds = [...ids];
|
|
22
|
+
else deleteConfigTopLevelKey(config, "codexUseRemainingQuotaAccountIds");
|
|
23
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
resolveTrustedWindowsPowerShellExe,
|
|
15
15
|
resolveTrustedWindowsTaskkillExe,
|
|
16
16
|
} from "../lib/windows-elevation";
|
|
17
|
-
import { readCodexCatalogPath } from "./
|
|
17
|
+
import { readCodexCatalogPath } from "./paths";
|
|
18
18
|
|
|
19
19
|
export const STALE_CODEX_APP_SERVER_HINT =
|
|
20
20
|
"If Codex still shows an older model list, run `ocx sync --restart-codex`: it restarts the long-lived app-server "
|
|
@@ -332,6 +332,32 @@ export function isCodexAppServerCommandLine(commandLine: string, executable?: st
|
|
|
332
332
|
return false;
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
/**
|
|
336
|
+
* True when the command line is ANY live Codex session, not just an app-server.
|
|
337
|
+
*
|
|
338
|
+
* The update-blocking contract needs the broader reading: an interactive TUI, a
|
|
339
|
+
* one-shot `codex exec`, or an app-server all have the package's binaries in use,
|
|
340
|
+
* so every subcommand counts. The executable identity stays exactly as strict as
|
|
341
|
+
* {@link isCodexAppServerCommandLine} - only the argv discipline is dropped. A
|
|
342
|
+
* coincidental codex-shaped argument can never open this matcher.
|
|
343
|
+
*/
|
|
344
|
+
export function isCodexSessionCommandLine(commandLine: string, executable?: string): boolean {
|
|
345
|
+
const trimmed = commandLine.trim();
|
|
346
|
+
let tokens = tokenizeCommandLine(trimmed);
|
|
347
|
+
if (executable) {
|
|
348
|
+
if (isCodeModeHostToken(executable) || isCodexExecutableToken(executable)) return true;
|
|
349
|
+
}
|
|
350
|
+
if (tokens.length === 0) return false;
|
|
351
|
+
if (isCodeModeHostProcess(tokens)) return true;
|
|
352
|
+
// Same interpreter unwrap the app-server matcher uses: a codex-shaped token must
|
|
353
|
+
// immediately follow the interpreter so an interpreter flag value is never read
|
|
354
|
+
// as the entrypoint.
|
|
355
|
+
if (isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodexExecutableToken(tokens[1]!)) {
|
|
356
|
+
tokens = tokens.slice(1);
|
|
357
|
+
}
|
|
358
|
+
return isCodexExecutableToken(tokens[0]!);
|
|
359
|
+
}
|
|
360
|
+
|
|
335
361
|
function parseUnixProcStatusUid(status: string): number | undefined {
|
|
336
362
|
const match = /^Uid:\s+(\d+)/m.exec(status);
|
|
337
363
|
if (!match) return undefined;
|
|
@@ -339,7 +365,7 @@ function parseUnixProcStatusUid(status: string): number | undefined {
|
|
|
339
365
|
return Number.isSafeInteger(uid) ? uid : undefined;
|
|
340
366
|
}
|
|
341
367
|
|
|
342
|
-
function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
|
|
368
|
+
function listUnixProcSnapshots(uid: number | undefined, requireComplete = true): ProcessSnapshot[] {
|
|
343
369
|
// procfs missing on a Linux-shaped platform is an enumeration failure, not
|
|
344
370
|
// "no processes" — the staleness collector must not read it as not_running.
|
|
345
371
|
if (!existsSync("/proc")) throw new Error("procfs_unavailable");
|
|
@@ -359,8 +385,10 @@ function listUnixProcSnapshots(uid: number | undefined): ProcessSnapshot[] {
|
|
|
359
385
|
const commandLine = argv.join(" ").trim();
|
|
360
386
|
if (!commandLine) continue;
|
|
361
387
|
out.push({ pid, commandLine, executable: argv[0], uid: processUid });
|
|
362
|
-
} catch {
|
|
363
|
-
|
|
388
|
+
} catch (error) {
|
|
389
|
+
// Complete observations may ignore only disappearance. Restart target
|
|
390
|
+
// enumeration can skip an unreadable PID while retaining verified ones.
|
|
391
|
+
if (requireComplete && (error as NodeJS.ErrnoException)?.code !== "ENOENT") throw error;
|
|
364
392
|
}
|
|
365
393
|
}
|
|
366
394
|
return out;
|
|
@@ -518,10 +546,10 @@ async function listWindowsSnapshotsAsync(): Promise<ProcessSnapshot[]> {
|
|
|
518
546
|
return parseWindowsSnapshotOutput(output);
|
|
519
547
|
}
|
|
520
548
|
|
|
521
|
-
function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined): ProcessSnapshot[] {
|
|
549
|
+
function defaultListSnapshots(platform: NodeJS.Platform, getuid: () => number | undefined, requireComplete = true): ProcessSnapshot[] {
|
|
522
550
|
if (platform === "win32") return listWindowsSnapshots();
|
|
523
551
|
if (platform === "darwin") return listDarwinSnapshots(getuid());
|
|
524
|
-
return listUnixProcSnapshots(getuid());
|
|
552
|
+
return listUnixProcSnapshots(getuid(), requireComplete);
|
|
525
553
|
}
|
|
526
554
|
|
|
527
555
|
export interface ListProcessSnapshotsOptions {
|
|
@@ -565,7 +593,7 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
|
|
|
565
593
|
// Restart/kill contract (#476): enumeration failure means no targets —
|
|
566
594
|
// never signal a process we could not verify.
|
|
567
595
|
try {
|
|
568
|
-
snapshots = defaultListSnapshots(platform, getuid);
|
|
596
|
+
snapshots = defaultListSnapshots(platform, getuid, false);
|
|
569
597
|
} catch {
|
|
570
598
|
snapshots = [];
|
|
571
599
|
}
|
|
@@ -581,6 +609,68 @@ export function listCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): C
|
|
|
581
609
|
return matched;
|
|
582
610
|
}
|
|
583
611
|
|
|
612
|
+
/**
|
|
613
|
+
* Enumeration outcome for callers that must DEFER on an unreadable process table.
|
|
614
|
+
*
|
|
615
|
+
* {@link listCodexAppServerProcesses} maps enumeration failure to an empty list because its
|
|
616
|
+
* kill contract must never signal a process it could not verify. The Codex CLI update
|
|
617
|
+
* workflow needs the opposite reading: "no matches" and "could not look" lead to different
|
|
618
|
+
* decisions, and only the first one may allow an install to proceed.
|
|
619
|
+
*/
|
|
620
|
+
export type CodexAppServerProcessScan =
|
|
621
|
+
| Readonly<{ kind: "observed"; processes: readonly CodexAppServerProcess[] }>
|
|
622
|
+
| Readonly<{ kind: "unavailable" }>;
|
|
623
|
+
|
|
624
|
+
function scanProcessSnapshots(
|
|
625
|
+
io: CodexAppServerProcessIo,
|
|
626
|
+
matches: (commandLine: string, executable?: string) => boolean,
|
|
627
|
+
): CodexAppServerProcessScan {
|
|
628
|
+
const platform = io.platform ?? process.platform;
|
|
629
|
+
const getuid = io.getuid ?? (() => {
|
|
630
|
+
try {
|
|
631
|
+
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
632
|
+
} catch {
|
|
633
|
+
return undefined;
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
let snapshots: ProcessSnapshot[];
|
|
637
|
+
try {
|
|
638
|
+
snapshots = io.listSnapshots ? io.listSnapshots() : defaultListSnapshots(platform, getuid);
|
|
639
|
+
} catch {
|
|
640
|
+
return Object.freeze({ kind: "unavailable" as const });
|
|
641
|
+
}
|
|
642
|
+
const seen = new Set<number>();
|
|
643
|
+
const matched: CodexAppServerProcess[] = [];
|
|
644
|
+
for (const snapshot of snapshots) {
|
|
645
|
+
if (seen.has(snapshot.pid)) continue;
|
|
646
|
+
if (!matches(snapshot.commandLine, snapshot.executable)) continue;
|
|
647
|
+
seen.add(snapshot.pid);
|
|
648
|
+
matched.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
|
|
649
|
+
}
|
|
650
|
+
return Object.freeze({ kind: "observed" as const, processes: Object.freeze(matched) });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Same matcher and snapshot sources as {@link listCodexAppServerProcesses}, failing closed. */
|
|
654
|
+
export function scanCodexAppServerProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcessScan {
|
|
655
|
+
return scanProcessSnapshots(io, isCodexAppServerCommandLine);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* {@link isCodexSessionCommandLine} over the same snapshot sources, failing closed.
|
|
660
|
+
*
|
|
661
|
+
* Any live Codex session counts - TUI, exec or app-server all hold the installed
|
|
662
|
+
* package open while npm would be replacing it, and a missed TUI was the reported
|
|
663
|
+
* source of a half-written install on Linux. The kill path keeps the narrower
|
|
664
|
+
* {@link isCodexAppServerCommandLine} because a signal must never leave its
|
|
665
|
+
* verified contract; this scan decides whether it is SAFE to install, so it may
|
|
666
|
+
* only err toward deferring.
|
|
667
|
+
*/
|
|
668
|
+
export function scanCodexSessionProcesses(io: CodexAppServerProcessIo = {}): CodexAppServerProcessScan {
|
|
669
|
+
// Shared installs may be held open by another readable user's session. This
|
|
670
|
+
// read-only scan is broader than the unchanged same-user restart/kill path.
|
|
671
|
+
return scanProcessSnapshots({ ...io, getuid: io.getuid ?? (() => undefined) }, isCodexSessionCommandLine);
|
|
672
|
+
}
|
|
673
|
+
|
|
584
674
|
export function formatStaleCodexAppServerWarning(
|
|
585
675
|
processes: readonly { pid: number }[],
|
|
586
676
|
): string {
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
CodexRestartResponse,
|
|
35
35
|
} from "../lib/codex-restart-contract";
|
|
36
36
|
import { getServerListenPort } from "../server/lifecycle";
|
|
37
|
+
import type { DesktopPickerState } from "./desktop-app/picker-freshness";
|
|
37
38
|
|
|
38
39
|
async function defaultRestartDesktopApp(): Promise<CodexDesktopRestartSummary> {
|
|
39
40
|
const { restartCodexDesktopApp } = await import("./desktop-app-restart");
|
|
@@ -47,9 +48,40 @@ async function defaultRestartDesktopApp(): Promise<CodexDesktopRestartSummary> {
|
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
async function defaultDesktopPickerState(): Promise<DesktopPickerState> {
|
|
52
|
+
const { readCodexDesktopPickerState } = await import("./desktop-app/picker-freshness");
|
|
53
|
+
return readCodexDesktopPickerState();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function defaultCatalogBinding(): Promise<string | null> {
|
|
57
|
+
const { readCodexCatalogBinding } = await import("./desktop-app/picker-freshness");
|
|
58
|
+
return readCodexCatalogBinding();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readBinding(io: CodexRestartServiceIo): Promise<string | null | undefined> {
|
|
62
|
+
try {
|
|
63
|
+
return await (io.catalogBinding ?? defaultCatalogBinding)();
|
|
64
|
+
} catch {
|
|
65
|
+
// Distinct from null ("binds no catalog"): an unreadable binding cannot prove sameness.
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readPickerState(io: CodexRestartServiceIo): Promise<DesktopPickerState> {
|
|
71
|
+
try {
|
|
72
|
+
return await (io.desktopPickerState ?? defaultDesktopPickerState)();
|
|
73
|
+
} catch {
|
|
74
|
+
return "unknown";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
export interface CodexRestartServiceIo {
|
|
51
79
|
/** Desktop-restart seam, so a route test cannot terminate the developer's own Codex. */
|
|
52
80
|
restartDesktopApp?: () => Promise<CodexDesktopRestartSummary>;
|
|
81
|
+
/** Whether the desktop picker predates the catalog; decides if the shell must restart. */
|
|
82
|
+
desktopPickerState?: () => Promise<DesktopPickerState>;
|
|
83
|
+
/** The catalog config.toml binds, read before and after the sync to catch a rebinding. */
|
|
84
|
+
catalogBinding?: () => Promise<string | null>;
|
|
53
85
|
/** Process-layer seam, forwarded to every app-server-processes call. */
|
|
54
86
|
processIo?: CodexAppServerProcessIo;
|
|
55
87
|
/** Catalog refresh seam. Resolves to whether a catalog or cache write happened. */
|
|
@@ -133,6 +165,7 @@ async function runCodexRestart(io: CodexRestartServiceIo): Promise<CodexRestartR
|
|
|
133
165
|
// roster; stopping app-servers before the write would hand the replacement the
|
|
134
166
|
// same stale file it just lost.
|
|
135
167
|
let synced = false;
|
|
168
|
+
const bindingBefore = await readBinding(io);
|
|
136
169
|
try {
|
|
137
170
|
const port = (io.listenPort ?? getServerListenPort)();
|
|
138
171
|
synced = await (io.syncCatalog ?? defaultSyncCatalog)(port);
|
|
@@ -146,15 +179,33 @@ async function runCodexRestart(io: CodexRestartServiceIo): Promise<CodexRestartR
|
|
|
146
179
|
(io.resetStateCache ?? resetCodexAppServerCatalogStateCache)();
|
|
147
180
|
const before = (io.collectState ?? collectCodexAppServerCatalogState)(io.processIo ?? {});
|
|
148
181
|
|
|
149
|
-
//
|
|
150
|
-
// app: "no app-server is running" is not a reason to leave a stale roster on screen
|
|
151
|
-
//
|
|
182
|
+
// Decided here, before the early returns, because the model picker lives in the DESKTOP
|
|
183
|
+
// app: "no app-server is running" is not a reason to leave a stale roster on screen.
|
|
184
|
+
//
|
|
185
|
+
// The shell restarts only when its picker predates the catalog (#4761). Quitting it
|
|
186
|
+
// discards composer drafts, unsent picker selections and pending approvals, and when
|
|
187
|
+
// the picker is already current the app-server restart below is all the operator
|
|
188
|
+
// needs: the desktop respawns its app-server child, which rereads config and catalog.
|
|
189
|
+
// An unreadable picker state takes the full restart, the behaviour before #4761.
|
|
152
190
|
//
|
|
153
191
|
// allowHandoff is false and that is deliberate. The handoff waits for the CALLING
|
|
154
192
|
// process to exit, and this runs inside a long-lived proxy that does not, so every
|
|
155
193
|
// handoff started here would sit out its window and fail after the operator had
|
|
156
194
|
// already been told it was handed off. An honest refusal beats that.
|
|
157
|
-
|
|
195
|
+
//
|
|
196
|
+
// A sync that rebinds model_catalog_json without rewriting the catalog bytes leaves the
|
|
197
|
+
// catalog mtime older than the shell, yet the shell loaded the previous binding. Seen
|
|
198
|
+
// across this request's own sync, that is proof of staleness, so it skips the probe.
|
|
199
|
+
const bindingAfter = await readBinding(io);
|
|
200
|
+
const rebound = bindingBefore === undefined || bindingAfter === undefined || bindingBefore !== bindingAfter;
|
|
201
|
+
const picker: DesktopPickerState = rebound ? "stale" : await readPickerState(io);
|
|
202
|
+
const desktop: CodexDesktopRestartSummary = picker === "current" || picker === "not_running"
|
|
203
|
+
? {
|
|
204
|
+
attempted: false, stopped: [], surviving: [], relaunch: "skipped",
|
|
205
|
+
reason: picker === "current" ? "picker_current" : "no_targets",
|
|
206
|
+
pickerState: picker,
|
|
207
|
+
}
|
|
208
|
+
: { ...await (io.restartDesktopApp ?? defaultRestartDesktopApp)(), pickerState: picker };
|
|
158
209
|
|
|
159
210
|
const nothingToDo = (): CodexRestartResponse => ({
|
|
160
211
|
desktopApp: desktop,
|
|
@@ -10,6 +10,7 @@ import { ConfigMutationLockError, mutatePersistedConfig } from "../../config";
|
|
|
10
10
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
11
11
|
import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
|
|
12
12
|
import { codexAccountUsesCreditsAfterLimit } from "../account-credit-use";
|
|
13
|
+
import { codexAccountUsesRemainingQuota } from "../account-use-remaining";
|
|
13
14
|
import { getCodexAccountPriority } from "../account-priority";
|
|
14
15
|
import { getCodexAccountAutoSwitchThresholdOverride } from "../account-auto-switch";
|
|
15
16
|
import { clearThreadAccountMapForAccount, isCodexAccountPlanExcluded, reconcileCodexActiveAfterExclusion } from "../routing";
|
|
@@ -143,6 +144,7 @@ export function poolAccountDto(
|
|
|
143
144
|
priority,
|
|
144
145
|
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(config, account.id),
|
|
145
146
|
creditsAfterLimit: codexAccountUsesCreditsAfterLimit(config, account.id),
|
|
147
|
+
useRemainingQuota: codexAccountUsesRemainingQuota(config, account.id),
|
|
146
148
|
quota: quota ? { ...quota } : null,
|
|
147
149
|
...codexCreditsDtoField(config, account.id, poolQuotaHistoryIdentity(account.id) ?? null),
|
|
148
150
|
needsReauth: needsReauth || health.status === "reauth_required",
|
|
@@ -171,6 +173,7 @@ export interface CodexAuthAccountDto {
|
|
|
171
173
|
autoSwitchThresholdOverride: number | null;
|
|
172
174
|
/** False keeps the account out of selection while one of its usage windows is full. */
|
|
173
175
|
creditsAfterLimit?: boolean;
|
|
176
|
+
useRemainingQuota?: boolean;
|
|
174
177
|
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
175
178
|
credits?: CodexCredits;
|
|
176
179
|
needsReauth?: boolean;
|
|
@@ -405,6 +408,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
405
408
|
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
406
409
|
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
407
410
|
creditsAfterLimit: codexAccountUsesCreditsAfterLimit(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
411
|
+
useRemainingQuota: codexAccountUsesRemainingQuota(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
408
412
|
hasCredential: hasMainCredential,
|
|
409
413
|
needsReauth: mainNeedsReauth,
|
|
410
414
|
...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
|