claude-multiacc 2.0.64 → 2.0.66
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/bin/claude-accounts +15 -201
- package/bin/codex +25 -7
- package/bin/codex-accounts +16 -3
- package/docs/AUTORESUME.md +10 -1
- package/docs/CODEX.md +39 -0
- package/lib/__pycache__/audit.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
- package/lib/__pycache__/report.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
- package/lib/autoresume.py +28 -8
- package/lib/claude_oauth_lock.py +172 -0
- package/lib/claude_oauth_refresh.py +225 -0
- package/lib/codex_speed.py +351 -0
- package/lib/codex_speed.sh +39 -0
- package/lib/common.sh +13 -3
- package/lib/model_match.py +4 -1
- package/lib/session_settings.py +25 -6
- package/lib/shim_path.py +6 -0
- package/package.json +4 -3
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_oauth_lock.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
- package/tests/claude_oauth_fixture.py +100 -0
- package/tests/run-tests.sh +23 -7
- package/tests/test_claude_oauth_lock.py +138 -0
- package/tests/test_claude_oauth_refresh.py +106 -0
- package/tests/test_claude_oauth_refresh_failures.py +112 -0
- package/tests/test_codex_speed.py +236 -0
- package/tests/test_mutate_lock.py +107 -0
- package/tests/test_shim_path.py +44 -4
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_resume_cwd.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
- package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
- package/lib/__pycache__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
- package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_reader.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_exec_stdin.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
package/bin/claude-accounts
CHANGED
|
@@ -1708,7 +1708,7 @@ token_identity_receipt() { # Unknown results must never overwrite a peer's prove
|
|
|
1708
1708
|
limits_distribute_now() {
|
|
1709
1709
|
local lock="$ACC_ROOT/tmp/limits-push.lock"
|
|
1710
1710
|
mkdir -p "$ACC_ROOT/tmp" 2>/dev/null || return 0
|
|
1711
|
-
if !
|
|
1711
|
+
if ! atomic_mkdir "$lock" 2>/dev/null; then
|
|
1712
1712
|
# A DETACHED push that is killed (logout, reboot, pkill) never runs its EXIT trap,
|
|
1713
1713
|
# and mkdir can never take a lock dir nobody will remove: one stranded lock
|
|
1714
1714
|
# silently stopped ALL telemetry distribution on the live pool from 2026-09-03
|
|
@@ -1721,7 +1721,7 @@ limits_distribute_now() {
|
|
|
1721
1721
|
# exactly as before.
|
|
1722
1722
|
[ $(( $(epoch_now) - $(file_mtime "$lock") )) -gt 600 ] || return 0
|
|
1723
1723
|
rm -rf "$lock" 2>/dev/null
|
|
1724
|
-
|
|
1724
|
+
atomic_mkdir "$lock" 2>/dev/null || return 0
|
|
1725
1725
|
log_to sync.log "stale limits-push lock broken (older than 600s); distributing"
|
|
1726
1726
|
fi
|
|
1727
1727
|
trap 'rmdir "$lock" 2>/dev/null || true' EXIT
|
|
@@ -1792,7 +1792,7 @@ cmd_limits() {
|
|
|
1792
1792
|
done
|
|
1793
1793
|
local lock="$ACC_ROOT/.locks/limits"
|
|
1794
1794
|
mkdir -p "$ACC_ROOT/.locks"
|
|
1795
|
-
if !
|
|
1795
|
+
if ! atomic_mkdir "$lock" 2>/dev/null; then
|
|
1796
1796
|
local age=$(( $(epoch_now) - $(file_mtime "$lock") ))
|
|
1797
1797
|
if [ "$age" -lt 120 ]; then
|
|
1798
1798
|
# A --json caller still gets the document (built from the state on disk) —
|
|
@@ -1802,7 +1802,7 @@ cmd_limits() {
|
|
|
1802
1802
|
return 0
|
|
1803
1803
|
fi
|
|
1804
1804
|
rm -rf "$lock"
|
|
1805
|
-
if !
|
|
1805
|
+
if ! atomic_mkdir "$lock" 2>/dev/null; then
|
|
1806
1806
|
if [ "$json" = "1" ]; then emit_report_json limits; return $?; fi
|
|
1807
1807
|
return 0
|
|
1808
1808
|
fi
|
|
@@ -1846,12 +1846,6 @@ TOKEN_URL = os.environ.get('CLAUDE_MULTIACC_TOKEN_URL',
|
|
|
1846
1846
|
'https://console.anthropic.com/v1/oauth/token')
|
|
1847
1847
|
CLIENT_ID = os.environ.get('CLAUDE_MULTIACC_CLIENT_ID',
|
|
1848
1848
|
'9d1c250a-e61b-44d9-88ed-5944d1962f5e')
|
|
1849
|
-
# Only refresh a token that has been expired for a while: a LIVE session refreshes
|
|
1850
|
-
# its own credential within moments of expiry, so a long-expired one proves no
|
|
1851
|
-
# other writer is active (refresh tokens rotate; two racing refreshers strand one).
|
|
1852
|
-
REFRESH_MIN_EXPIRED = 300
|
|
1853
|
-
REFRESH_FAIL_BACKOFF = 600 # transient (network/5xx/429): retry in 10 min
|
|
1854
|
-
REFRESH_DENIED_BACKOFF = 21600 # 4xx = grant likely revoked: 6h; re-login needed anyway
|
|
1855
1849
|
# A usage fetch the server says will never succeed (x-should-retry: false — e.g. the
|
|
1856
1850
|
# 403 "does not meet scope requirement user:profile" that a setup token ALWAYS gets)
|
|
1857
1851
|
# is not a hiccup to retry every pass. Retrying it is what manufactured the 429s that
|
|
@@ -2008,39 +2002,12 @@ def park_dead_grant(aid, d, slug, detail):
|
|
|
2008
2002
|
say(f'{aid}: {detail} — re-login needed (claude-accounts relogin {aid})')
|
|
2009
2003
|
|
|
2010
2004
|
|
|
2011
|
-
#
|
|
2012
|
-
#
|
|
2013
|
-
|
|
2014
|
-
# document back — and its write FAILS after about 7.5 s of retries on a held lock. So the
|
|
2015
|
-
# refresher never holds it across the token POST: a lock held when a refresh would start
|
|
2016
|
-
# skips that refresh (nothing has rotated yet; the next pass retries), and the
|
|
2017
|
-
# check-and-set and the write take it after the POST, waiting longer, because a rotated
|
|
2018
|
-
# grant must land: past the wait (or with no helper) it is written as before, without it.
|
|
2019
|
-
try:
|
|
2020
|
-
from mcp_oauth import CLIENT_LOCK, CLIENT_LOCK_STALE_S, ClientLock # noqa: E402
|
|
2021
|
-
except Exception: # noqa: BLE001 — fail open: refresh exactly as before, unlocked
|
|
2022
|
-
ClientLock = None
|
|
2023
|
-
CLIENT_LOCK_PROBE_S = 1.0 # a client's write takes milliseconds: held this long, it is busy
|
|
2005
|
+
# Legacy Keychain migration needs only the client's short storage-write lock.
|
|
2006
|
+
# Refresh request locking and credential CAS live in claude_oauth_refresh.py.
|
|
2007
|
+
from mcp_oauth import ClientLock # noqa: E402
|
|
2024
2008
|
CLIENT_LOCK_WAIT_S = 10.0
|
|
2025
2009
|
|
|
2026
2010
|
|
|
2027
|
-
def client_lock_busy(d):
|
|
2028
|
-
"""A live writer holds <d>'s client lock, and still does CLIENT_LOCK_PROBE_S later."""
|
|
2029
|
-
if ClientLock is None:
|
|
2030
|
-
return False
|
|
2031
|
-
path = os.path.join(d, CLIENT_LOCK)
|
|
2032
|
-
deadline = time.monotonic() + CLIENT_LOCK_PROBE_S
|
|
2033
|
-
while True:
|
|
2034
|
-
try:
|
|
2035
|
-
if time.time() - os.stat(path).st_mtime > CLIENT_LOCK_STALE_S:
|
|
2036
|
-
return False # its holder is gone; the write takes it over
|
|
2037
|
-
except OSError:
|
|
2038
|
-
return False # no lock: nobody is writing
|
|
2039
|
-
if time.monotonic() >= deadline:
|
|
2040
|
-
return True
|
|
2041
|
-
time.sleep(0.05)
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
2011
|
class FileStore:
|
|
2045
2012
|
"""The OAuth credential as `.credentials.json`: atomic 0600 writes. The client keeps
|
|
2046
2013
|
its MCP servers' OAuth state (mcpOAuth, mcpOAuthClientConfig) in this same file, so
|
|
@@ -2141,166 +2108,13 @@ def oauth_store(d):
|
|
|
2141
2108
|
|
|
2142
2109
|
|
|
2143
2110
|
def refresh_oauth(aid, d, store):
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
spath = os.path.join(d, '.oauth-refresh.json')
|
|
2151
|
-
try:
|
|
2152
|
-
doc = store.read()
|
|
2153
|
-
o = doc.get('claudeAiOauth') if isinstance(doc, dict) else None
|
|
2154
|
-
# Present-but-null/non-object claudeAiOauth (interrupted or reset credential
|
|
2155
|
-
# write) must degrade THIS account only, like every other malformed input; and a
|
|
2156
|
-
# document with no login in it at all (MCP state alone) has nothing to refresh.
|
|
2157
|
-
if not isinstance(o, dict):
|
|
2158
|
-
return None
|
|
2159
|
-
except Exception:
|
|
2160
|
-
return None
|
|
2161
|
-
# NB: no "must have an accessToken" gate. A credential whose access token was
|
|
2162
|
-
# cleared but whose REFRESH token is alive is exactly the shape a grant is supposed
|
|
2163
|
-
# to recover from; requiring the dead half to be present meant such an account could
|
|
2164
|
-
# never come back, and (with a setup token beside it) went dark for telemetry
|
|
2165
|
-
# forever. The expiresAt gate below is what protects a live session's credential.
|
|
2166
|
-
if not o.get('refreshToken'):
|
|
2167
|
-
park_dead_grant(aid, d, 'no-refresh-token',
|
|
2168
|
-
'credential has no refresh token and its access token expired')
|
|
2169
|
-
return None
|
|
2170
|
-
if o.get('expiresAt', 0) / 1000.0 > now - REFRESH_MIN_EXPIRED:
|
|
2171
|
-
return None # not expired long enough to prove no live session owns it
|
|
2172
|
-
if o.get('refreshTokenExpiresAt', 0) / 1000.0 <= now:
|
|
2173
|
-
# Nothing can revive this account: park it so the shim stops selecting it
|
|
2174
|
-
# (every run under it would fail with "OAuth session expired").
|
|
2175
|
-
park_dead_grant(aid, d, 'refresh-token-expired',
|
|
2176
|
-
'the refresh token itself expired; only a re-login can fix it')
|
|
2177
|
-
return None
|
|
2178
|
-
if not force:
|
|
2179
|
-
try:
|
|
2180
|
-
if json.load(open(spath)).get('retry_after', 0) > now:
|
|
2181
|
-
return None # earlier refresh failure still backing off
|
|
2182
|
-
except Exception:
|
|
2183
|
-
pass
|
|
2184
|
-
|
|
2185
|
-
def back_off(wait, why, denials=0):
|
|
2186
|
-
try:
|
|
2187
|
-
with open(spath + '.tmp', 'w') as f:
|
|
2188
|
-
json.dump({'retry_after': int(now + wait), 'error': why,
|
|
2189
|
-
'denials': denials,
|
|
2190
|
-
'at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}, f)
|
|
2191
|
-
os.replace(spath + '.tmp', spath)
|
|
2192
|
-
except Exception:
|
|
2193
|
-
pass
|
|
2194
|
-
say(f'{aid}: oauth refresh failed ({why}); backing off {wait}s; limits left as-is')
|
|
2111
|
+
# Import failures skip this account through the caller's guard; never rotate
|
|
2112
|
+
# without the native request lock. The helper owns the full refresh transaction.
|
|
2113
|
+
from claude_oauth_refresh import OAuthRefresher
|
|
2114
|
+
return OAuthRefresher(now=now, force=force, token_url=TOKEN_URL, client_id=CLIENT_ID,
|
|
2115
|
+
say=say, park_dead_grant=park_dead_grant,
|
|
2116
|
+
clear_expired=clear_expired).refresh(aid, d, store)
|
|
2195
2117
|
|
|
2196
|
-
if client_lock_busy(d):
|
|
2197
|
-
# The client is writing this credential right now (it may be its own refresh of
|
|
2198
|
-
# the same grant): no request is sent, so nothing rotates, and no backoff is noted.
|
|
2199
|
-
say(f'{aid}: oauth refresh skipped — the client holds its credential lock '
|
|
2200
|
-
f'({CLIENT_LOCK}); the next pass retries')
|
|
2201
|
-
return None
|
|
2202
|
-
started_with = o['refreshToken']
|
|
2203
|
-
body = json.dumps({'grant_type': 'refresh_token',
|
|
2204
|
-
'refresh_token': o['refreshToken'],
|
|
2205
|
-
'client_id': CLIENT_ID}).encode()
|
|
2206
|
-
req = urllib.request.Request(TOKEN_URL, data=body, headers={
|
|
2207
|
-
'Content-Type': 'application/json',
|
|
2208
|
-
'User-Agent': 'claude-multiacc/1.0',
|
|
2209
|
-
})
|
|
2210
|
-
try:
|
|
2211
|
-
data = json.loads(urllib.request.urlopen(req, timeout=30).read().decode())
|
|
2212
|
-
except urllib.error.HTTPError as e:
|
|
2213
|
-
if e.code in (400, 401, 403):
|
|
2214
|
-
# A 4xx from the TOKEN endpoint is only proof of a dead grant when the
|
|
2215
|
-
# server says so (OAuth's invalid_grant). Everything else 4xx — a bad
|
|
2216
|
-
# client_id, an endpoint change, a WAF page, a provider incident — would
|
|
2217
|
-
# hit EVERY account at once, so it must never park the whole pool on the
|
|
2218
|
-
# first try: back off, and only park after this account has been refused
|
|
2219
|
-
# repeatedly.
|
|
2220
|
-
body = ''
|
|
2221
|
-
try:
|
|
2222
|
-
body = e.read().decode('utf-8', 'replace')[:400]
|
|
2223
|
-
except Exception:
|
|
2224
|
-
pass
|
|
2225
|
-
denials = 1
|
|
2226
|
-
try:
|
|
2227
|
-
denials = int(json.load(open(spath)).get('denials', 0)) + 1
|
|
2228
|
-
except Exception:
|
|
2229
|
-
pass
|
|
2230
|
-
if 'invalid_grant' in body:
|
|
2231
|
-
park_dead_grant(aid, d, f'refresh-denied-http-{e.code}',
|
|
2232
|
-
'the refresh grant was refused as invalid_grant (revoked or rotated away)')
|
|
2233
|
-
elif denials >= 3:
|
|
2234
|
-
park_dead_grant(aid, d, f'refresh-denied-http-{e.code}',
|
|
2235
|
-
f'the refresh grant was refused {denials} times in a row')
|
|
2236
|
-
back_off(REFRESH_DENIED_BACKOFF,
|
|
2237
|
-
f'HTTP {e.code} — refresh token may be revoked; re-login needed',
|
|
2238
|
-
denials=denials)
|
|
2239
|
-
else:
|
|
2240
|
-
back_off(REFRESH_FAIL_BACKOFF, f'HTTP {e.code}')
|
|
2241
|
-
return None
|
|
2242
|
-
except Exception as e:
|
|
2243
|
-
back_off(REFRESH_FAIL_BACKOFF, str(e)[:200])
|
|
2244
|
-
return None
|
|
2245
|
-
tok = data.get('access_token') if isinstance(data, dict) else None
|
|
2246
|
-
if not tok:
|
|
2247
|
-
back_off(REFRESH_DENIED_BACKOFF, 'no access_token in response')
|
|
2248
|
-
return None
|
|
2249
|
-
o['accessToken'] = tok
|
|
2250
|
-
# The grant ROTATES the refresh token: persist it (and both expiries) or the
|
|
2251
|
-
# account is stranded — hence atomic write, and a loud message if it fails.
|
|
2252
|
-
if data.get('refresh_token'):
|
|
2253
|
-
o['refreshToken'] = data['refresh_token']
|
|
2254
|
-
if data.get('expires_in'):
|
|
2255
|
-
o['expiresAt'] = int((now + float(data['expires_in'])) * 1000)
|
|
2256
|
-
else:
|
|
2257
|
-
# No expires_in in the response: assume a conservative 1h. Leaving the old
|
|
2258
|
-
# (past) expiresAt would make every later pass re-run the grant in a loop.
|
|
2259
|
-
o['expiresAt'] = int((now + 3600) * 1000)
|
|
2260
|
-
if data.get('refresh_token_expires_in'):
|
|
2261
|
-
o['refreshTokenExpiresAt'] = int((now + float(data['refresh_token_expires_in'])) * 1000)
|
|
2262
|
-
# CHECK-AND-SET. The grant rotates, and a live claude session refreshes the same
|
|
2263
|
-
# file. REFRESH_MIN_EXPIRED makes that unlikely, not impossible — and losing the
|
|
2264
|
-
# race by overwriting means the session's newer credential is destroyed. If the
|
|
2265
|
-
# on-disk refresh token is no longer the one this grant was issued against, the
|
|
2266
|
-
# other writer won: keep its result, discard ours. Both under the client's own lock,
|
|
2267
|
-
# so no client write lands between the check and the write.
|
|
2268
|
-
lock = ClientLock(d, CLIENT_LOCK_WAIT_S) if ClientLock is not None else None
|
|
2269
|
-
try:
|
|
2270
|
-
held = lock is not None and lock.acquire()
|
|
2271
|
-
except Exception: # noqa: BLE001 — the rotated grant lands either way
|
|
2272
|
-
held = False
|
|
2273
|
-
if lock is not None and not held and os.path.isdir(lock.path):
|
|
2274
|
-
say(f'{aid}: the client kept its credential lock ({CLIENT_LOCK}) past '
|
|
2275
|
-
f'{int(CLIENT_LOCK_WAIT_S)}s — writing the rotated login without it')
|
|
2276
|
-
try:
|
|
2277
|
-
try:
|
|
2278
|
-
disk = store.read().get('claudeAiOauth', {})
|
|
2279
|
-
if isinstance(disk, dict) and disk.get('refreshToken') != started_with:
|
|
2280
|
-
say(f'{aid}: credential was refreshed by something else mid-flight — '
|
|
2281
|
-
f'keeping the newer one in the {store.kind}')
|
|
2282
|
-
return None
|
|
2283
|
-
except Exception:
|
|
2284
|
-
pass
|
|
2285
|
-
try:
|
|
2286
|
-
# Only the login is ours: both stores write it INTO the document as it is now,
|
|
2287
|
-
# so the MCP state the client saved meanwhile (mcpOAuth) is never rolled back.
|
|
2288
|
-
store.write({'claudeAiOauth': o})
|
|
2289
|
-
except Exception as e:
|
|
2290
|
-
say(f'{aid}: token refreshed but credentials NOT persisted ({e}) — re-login may be needed')
|
|
2291
|
-
return None
|
|
2292
|
-
finally:
|
|
2293
|
-
if held:
|
|
2294
|
-
lock.release()
|
|
2295
|
-
try:
|
|
2296
|
-
os.remove(spath)
|
|
2297
|
-
except OSError:
|
|
2298
|
-
pass
|
|
2299
|
-
# The grant answered: whatever parked this account before, it authenticates now.
|
|
2300
|
-
if clear_expired(d):
|
|
2301
|
-
say(f'{aid}: dead-auth marker cleared (refresh grant works again)')
|
|
2302
|
-
say(f'{aid}: oauth access token refreshed via refresh-token grant')
|
|
2303
|
-
return tok
|
|
2304
2118
|
|
|
2305
2119
|
for acct in manifest.get('accounts', []):
|
|
2306
2120
|
aid = acct['id']
|
|
@@ -2400,8 +2214,8 @@ for acct in manifest.get('accounts', []):
|
|
|
2400
2214
|
bearer, source = tok, 'oauth'
|
|
2401
2215
|
# A setup token is a LAST RESORT and only for an account that has no OAuth
|
|
2402
2216
|
# credential at all. Where one exists but could not be used this pass — its
|
|
2403
|
-
# access token expired moments ago (refresh_oauth
|
|
2404
|
-
#
|
|
2217
|
+
# access token expired moments ago (refresh_oauth gives the native
|
|
2218
|
+
# client a five-minute grace period), a refresh backoff, or a Keychain this session
|
|
2405
2219
|
# cannot open — spending the token is guaranteed to earn a 403 it can never not
|
|
2406
2220
|
# earn, and that 403 used to park the whole ACCOUNT for six hours. Telemetry then
|
|
2407
2221
|
# froze for an account whose OAuth would have worked on the very next pass, and
|
package/bin/codex
CHANGED
|
@@ -156,6 +156,20 @@ cli_positional_at() { # argv -> prints the index of its first positional argumen
|
|
|
156
156
|
return 1
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
# Account/model capabilities are resolved only after the serving account is known.
|
|
160
|
+
# The helper is optional so a damaged install retains the shim's fail-open contract.
|
|
161
|
+
codex_native_argv() {
|
|
162
|
+
CODEX_SPEED_ARGV=("$@")
|
|
163
|
+
if [ -f "$SELF_DIR/../lib/codex_speed.sh" ]; then
|
|
164
|
+
. "$SELF_DIR/../lib/codex_speed.sh"
|
|
165
|
+
codex_speed_prepare "$@" || return $?
|
|
166
|
+
fi
|
|
167
|
+
}
|
|
168
|
+
codex_native_exec() {
|
|
169
|
+
codex_native_argv "$@" || exit $?
|
|
170
|
+
exec "$REAL" ${CODEX_SPEED_ARGV[@]+"${CODEX_SPEED_ARGV[@]}"}
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
# Whether argv is `codex [global options] mcp add|remove …`.
|
|
160
174
|
mcp_mutation_argv() {
|
|
161
175
|
local i j
|
|
@@ -252,7 +266,7 @@ if [ -n "${CODEX_HOME:-}" ] \
|
|
|
252
266
|
mcp_oauth_ensure "$CODEX_HOME"
|
|
253
267
|
fi
|
|
254
268
|
mcp_nested_mirror "$@" || true
|
|
255
|
-
|
|
269
|
+
codex_native_exec "$@"
|
|
256
270
|
fi
|
|
257
271
|
|
|
258
272
|
now="$(date +%s)"
|
|
@@ -1074,6 +1088,9 @@ ar_argv_ok() { # the argv codex is about to get
|
|
|
1074
1088
|
model:*) case "$a" in ''|-*) return 1 ;; esac ;;
|
|
1075
1089
|
effort:model_reasoning_effort=?*)
|
|
1076
1090
|
case "${a#model_reasoning_effort=}" in *[!$AR_LOWER]*) return 1 ;; esac ;;
|
|
1091
|
+
effort:service_tier=*)
|
|
1092
|
+
case "${a#service_tier=}" in default|fast|priority|ultrafast|'"default"'|'"fast"'|'"priority"'|'"ultrafast"'|"'default'"|"'fast'"|"'priority'"|"'ultrafast'") ;; *) return 1 ;; esac ;;
|
|
1093
|
+
effort:features.fast_mode=true) ;;
|
|
1077
1094
|
*) return 1 ;;
|
|
1078
1095
|
esac
|
|
1079
1096
|
want=""
|
|
@@ -1272,7 +1289,7 @@ if [ -n "${CODEX_ACCOUNT:-}" ] && [ "$AR_PROBE" != "1" ]; then
|
|
|
1272
1289
|
mcp_ensure "$d"
|
|
1273
1290
|
mcp_oauth_ensure "$d"
|
|
1274
1291
|
mcp_mirror_run "$d" "$@" || true
|
|
1275
|
-
|
|
1292
|
+
codex_native_exec "$@"
|
|
1276
1293
|
fi
|
|
1277
1294
|
sel_log "pin-invalid account=$CODEX_ACCOUNT (no such dir; random fallback)"
|
|
1278
1295
|
fi
|
|
@@ -1343,7 +1360,7 @@ if [ "${#valid[@]}" -eq 0 ]; then
|
|
|
1343
1360
|
[ -t 2 ] && printf 'codex-multiacc: no pool account is usable (%s) — see: codex-accounts expired, then: codex-accounts relogin\n' \
|
|
1344
1361
|
"${ids# }" >&2
|
|
1345
1362
|
fi
|
|
1346
|
-
|
|
1363
|
+
codex_native_exec "$@"
|
|
1347
1364
|
fi
|
|
1348
1365
|
|
|
1349
1366
|
# ---- rotation ----------------------------------------------------------------
|
|
@@ -1657,14 +1674,14 @@ if [ "$wants_retry" = "0" ]; then
|
|
|
1657
1674
|
fi
|
|
1658
1675
|
ar_scrub_env
|
|
1659
1676
|
[ "$AR_NO_DAEMON" != 1 ] || set -- --no-daemon "$@"
|
|
1660
|
-
|
|
1677
|
+
codex_native_exec "$@"
|
|
1661
1678
|
fi
|
|
1662
1679
|
|
|
1663
1680
|
# Retry path: buffer stdio so a retried call never double-emits partial output.
|
|
1664
1681
|
mkdir -p "$ACC_ROOT/tmp" 2>/dev/null || true
|
|
1665
1682
|
tmpd="$(mktemp -d "$ACC_ROOT/tmp/shim.XXXXXX" 2>/dev/null)" || {
|
|
1666
1683
|
export CODEX_HOME="$pick"
|
|
1667
|
-
|
|
1684
|
+
codex_native_exec "$@"
|
|
1668
1685
|
}
|
|
1669
1686
|
trap 'rm -rf "$tmpd"' EXIT
|
|
1670
1687
|
|
|
@@ -1673,7 +1690,7 @@ trap 'rm -rf "$tmpd"' EXIT
|
|
|
1673
1690
|
# nonzero — a hard failure. Verify now, fall back to plain exec if we cannot.
|
|
1674
1691
|
if ! : > "$tmpd/out" 2>/dev/null || ! : > "$tmpd/err" 2>/dev/null; then
|
|
1675
1692
|
export CODEX_HOME="$pick"
|
|
1676
|
-
|
|
1693
|
+
codex_native_exec "$@"
|
|
1677
1694
|
fi
|
|
1678
1695
|
|
|
1679
1696
|
stdin_file=""
|
|
@@ -1708,8 +1725,9 @@ cur="$pick"
|
|
|
1708
1725
|
rc=0
|
|
1709
1726
|
while :; do
|
|
1710
1727
|
if [ -n "$stdin_file" ]; then exec 3< "$stdin_file"; else exec 3< /dev/null; fi
|
|
1728
|
+
CODEX_HOME="$cur" codex_native_argv "$@" || exit $?
|
|
1711
1729
|
CODEX_HOME="$cur" CODEX_MULTIACC_SELECTED_HOME="$cur" \
|
|
1712
|
-
codex_retry_run "$REAL" "
|
|
1730
|
+
codex_retry_run "$REAL" ${CODEX_SPEED_ARGV[@]+"${CODEX_SPEED_ARGV[@]}"} <&3 > "$tmpd/out" 2> "$tmpd/err"
|
|
1713
1731
|
rc=$?
|
|
1714
1732
|
exec 3<&-
|
|
1715
1733
|
if [ "$rc" -ne 0 ] && [ "$attempt" -eq 1 ] \
|
package/bin/codex-accounts
CHANGED
|
@@ -106,6 +106,10 @@ USAGE
|
|
|
106
106
|
every install and self-update runs it, and each limits pass refreshes its own pool's
|
|
107
107
|
copy once it is an hour old. --if-stale leaves a younger copy alone; --quiet prints
|
|
108
108
|
nothing. show prints the merged catalog.
|
|
109
|
+
codex-accounts speed --account-dir DIR --model MODEL --speed normal|fast|ultrafast [--json]
|
|
110
|
+
resolve speed from this account's native model catalog. Ultrafast verifies
|
|
111
|
+
account access through native model/list without inference; unknown or
|
|
112
|
+
unavailable access falls back to Fast. Never changes account configuration.
|
|
109
113
|
codex-accounts post-sync (server side) seed dirs, fix perms, quick verify
|
|
110
114
|
codex-accounts mcp add <name> [--provider claude|codex|both] [--scope user]
|
|
111
115
|
[-e KEY=VAL]... [-H 'Header: v']... [--transport stdio|http]
|
|
@@ -144,6 +148,8 @@ ENV
|
|
|
144
148
|
'none' = local-only, nothing is pushed anywhere
|
|
145
149
|
CODEX_MULTIACC_SYNC_ROOT / _SYNC_REPO remote pool root / addon repo for it
|
|
146
150
|
CODEX_ACCOUNT pin the shim to one account
|
|
151
|
+
CODEX_SPEED normal|fast|ultrafast for this launch; unavailable Ultrafast uses Fast
|
|
152
|
+
CODEX_SPEED_MODEL model hint for app-server launches that choose their model over RPC
|
|
147
153
|
CODEX_SHIM_RETRY=0 disable the `codex exec` auto-retry
|
|
148
154
|
CODEX_MULTIACC_DISABLE=1 bypass the shim entirely
|
|
149
155
|
CODEX_MULTIACC_AUTO_RESET=0 disable automatic usage-reset redemption
|
|
@@ -156,6 +162,12 @@ EOF
|
|
|
156
162
|
|
|
157
163
|
require_manifest() { [ -f "$MANIFEST" ] || die "no manifest at $MANIFEST — run claude-multiacc install first"; }
|
|
158
164
|
|
|
165
|
+
cmd_speed() {
|
|
166
|
+
local native
|
|
167
|
+
native="$(find_real_codex "$_self" 2>/dev/null)" || native=""
|
|
168
|
+
"$PYBIN" "$LIB_DIR/codex_speed.py" --native-cli "$native" "$@"
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
cmd_configure() {
|
|
160
172
|
require_manifest
|
|
161
173
|
local apply=0 check=0 arg rc
|
|
@@ -257,11 +269,11 @@ PYEOF
|
|
|
257
269
|
# later contender legitimately re-took.
|
|
258
270
|
login_lock_acquire() { # $1 = lock dir; nonzero when someone else holds it
|
|
259
271
|
local L="$1"
|
|
260
|
-
if !
|
|
272
|
+
if ! atomic_mkdir "$L" 2>/dev/null; then
|
|
261
273
|
[ $(( $(epoch_now) - $(file_mtime "$L") )) -gt 1800 ] || return 1
|
|
262
274
|
mv "$L" "$L.stale.$$" 2>/dev/null || return 1
|
|
263
275
|
rm -rf "$L.stale.$$" 2>/dev/null
|
|
264
|
-
|
|
276
|
+
atomic_mkdir "$L" 2>/dev/null || return 1
|
|
265
277
|
fi
|
|
266
278
|
: > "$L/owner.$$" 2>/dev/null || true
|
|
267
279
|
return 0
|
|
@@ -2477,7 +2489,7 @@ PYEOF
|
|
|
2477
2489
|
# distribution for as long as the arg loops rejected the flag. An UNKNOWN verb must still
|
|
2478
2490
|
# FAIL, or the probe stops meaning what it says — so this list must hold exactly the verbs
|
|
2479
2491
|
# the dispatcher below implements, and a test pins that both ways.
|
|
2480
|
-
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp model-catalog"
|
|
2492
|
+
_KNOWN_VERBS="list status add import export-credential export-cred import-credential import-cred adopt dedupe remove login expired relogin re-login sync configure verify limits post-sync health self-update init-pool mcp model-catalog speed"
|
|
2481
2493
|
case " $_KNOWN_VERBS " in
|
|
2482
2494
|
*" ${1:-help} "*)
|
|
2483
2495
|
# ...but never past a literal `--`: from there on the words belong to an MCP
|
|
@@ -2510,6 +2522,7 @@ case "${1:-help}" in
|
|
|
2510
2522
|
init-pool) shift; cmd_init_pool "$@" ;;
|
|
2511
2523
|
mcp) shift; cmd_mcp "$@" ;;
|
|
2512
2524
|
model-catalog) shift; cmd_model_catalog "$@" ;;
|
|
2525
|
+
speed) shift; cmd_speed "$@" ;;
|
|
2513
2526
|
help|--help|-h) usage ;;
|
|
2514
2527
|
*) usage; exit 1 ;;
|
|
2515
2528
|
esac
|
package/docs/AUTORESUME.md
CHANGED
|
@@ -225,7 +225,16 @@ The prompt is submitted automatically:
|
|
|
225
225
|
- **To replace the whole text**, set `CLAUDE_MULTIACC_AUTORESUME_PROMPT` or
|
|
226
226
|
`CODEX_MULTIACC_AUTORESUME_PROMPT`.
|
|
227
227
|
|
|
228
|
-
### Model and
|
|
228
|
+
### Model, effort, and Codex speed
|
|
229
|
+
|
|
230
|
+
Codex relaunches also carry the most recently saved `service_tier` from the
|
|
231
|
+
thread's own settings or turn context. Normal, Fast, and Ultrafast remain the same
|
|
232
|
+
across account rotation; Ultrafast becomes Fast when the selected destination
|
|
233
|
+
account/model does not advertise it. Claude receives no speed flag. The shim
|
|
234
|
+
updates the named thread's tier through native `thread/resume` before launching
|
|
235
|
+
the CLI, because native resume restores a saved tier ahead of `-c` configuration.
|
|
236
|
+
See [Session speed](CODEX.md#session-speed) for capability discovery and the
|
|
237
|
+
`CODEX_SPEED` launch override.
|
|
229
238
|
|
|
230
239
|
The watcher reads the model and effort from the session's own history when it decides to
|
|
231
240
|
relaunch, so a `/model` or `/effort` switch made after the launch is honoured:
|
package/docs/CODEX.md
CHANGED
|
@@ -92,6 +92,45 @@ Codex-specific notes:
|
|
|
92
92
|
- **API-key logins are rejected** — ChatGPT subscription accounts only, matching the
|
|
93
93
|
addon's no-API-keys rule.
|
|
94
94
|
|
|
95
|
+
## Session speed
|
|
96
|
+
|
|
97
|
+
Set `CODEX_SPEED=normal`, `fast`, or `ultrafast` for a Codex launch. Normal explicitly
|
|
98
|
+
uses the standard tier, even when an account's configuration defaults to Fast.
|
|
99
|
+
The setting also works with `CODEX_ACCOUNT` and `CODEX_HOME` pins. Explicit native
|
|
100
|
+
`-c service_tier=...` overrides take precedence. Claude has no corresponding option.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
CODEX_SPEED=fast codex
|
|
104
|
+
CODEX_SPEED=ultrafast codex exec -m gpt-6.1-sol 'Review the changed code'
|
|
105
|
+
codex-accounts speed --account-dir ~/.codex-accounts/acct-18 --model gpt-6.1-sol --speed ultrafast --json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The shim reads **the selected account's** native model catalog, never an e-mail or
|
|
109
|
+
a plan-name guess. Ultrafast falls back to Fast when that account/model does not
|
|
110
|
+
advertise it. Every Ultrafast launch verifies the native account's `model/list`
|
|
111
|
+
with no inference, so even a copied cache cannot grant another account access;
|
|
112
|
+
an inconclusive read falls back to Fast.
|
|
113
|
+
Catalog probes use a temporary SQLite index, preserving the account's normal
|
|
114
|
+
session-index work. The inspection command above uses the same native check for
|
|
115
|
+
Ultrafast.
|
|
116
|
+
|
|
117
|
+
Quota retries resolve the speed again for the next account. Automatic quota
|
|
118
|
+
resumes carry the speed recorded in the session history. For those resumes and
|
|
119
|
+
explicit speed requests on a named session UUID, the shim applies the resolved
|
|
120
|
+
tier through native `thread/resume` before launching the resumed CLI: Codex
|
|
121
|
+
currently restores its saved tier ahead of process configuration. That
|
|
122
|
+
preparation sends no turn and preserves the conversation. A failed preparation
|
|
123
|
+
stops the launch instead of silently selecting another speed.
|
|
124
|
+
|
|
125
|
+
App-server callers may supply `CODEX_SPEED_MODEL` when their model is chosen over
|
|
126
|
+
RPC. Read the resolved `config.service_tier` with native `config/read` and pass it
|
|
127
|
+
as `serviceTier` on `thread/start`, `thread/resume`, and `turn/start`; otherwise
|
|
128
|
+
saved thread settings can override the process configuration.
|
|
129
|
+
|
|
130
|
+
The [official speed documentation](https://learn.chatgpt.com/docs/agent-configuration/speed)
|
|
131
|
+
describes Fast's higher usage and Ultrafast access for Pro $500 and eligible
|
|
132
|
+
Enterprise/Edu plans. Actual account/model capabilities remain authoritative.
|
|
133
|
+
|
|
95
134
|
## Spendable Codex credits
|
|
96
135
|
|
|
97
136
|
The existing authenticated usage GET also supplies the account's spendable Codex
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|