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/lib/autoresume.py
CHANGED
|
@@ -1214,6 +1214,14 @@ def codex_effort_ok(val):
|
|
|
1214
1214
|
and EFFORT_VALUE_RE.match(val[len(CODEX_EFFORT_KEY):]) is not None
|
|
1215
1215
|
|
|
1216
1216
|
|
|
1217
|
+
def codex_speed_ok(val):
|
|
1218
|
+
"""Only known speed choices, never arbitrary config, may cross a relaunch."""
|
|
1219
|
+
if val == "features.fast_mode=true":
|
|
1220
|
+
return True
|
|
1221
|
+
return isinstance(val, str) and re.fullmatch(
|
|
1222
|
+
r'''service_tier=(?:default|fast|priority|ultrafast|"(?:default|fast|priority|ultrafast)"|'(?:default|fast|priority|ultrafast)')''', val) is not None
|
|
1223
|
+
|
|
1224
|
+
|
|
1217
1225
|
def codex_argv_parse(argv):
|
|
1218
1226
|
"""The argv shapes a codex relaunch can rebuild (mirrors bin/codex's ar_argv_ok) ->
|
|
1219
1227
|
(kept options, resume target or None), or None for anything else."""
|
|
@@ -1231,7 +1239,7 @@ def codex_argv_parse(argv):
|
|
|
1231
1239
|
kept += [arg, expected]
|
|
1232
1240
|
i += 1
|
|
1233
1241
|
elif arg in ("-m", "--model", "-c"):
|
|
1234
|
-
value_ok = codex_effort_ok if arg == "-c" else _value_ok
|
|
1242
|
+
value_ok = (lambda value: codex_effort_ok(value) or codex_speed_ok(value)) if arg == "-c" else _value_ok
|
|
1235
1243
|
if i + 1 >= len(argv) or not value_ok(argv[i + 1]):
|
|
1236
1244
|
return None
|
|
1237
1245
|
kept += [arg, argv[i + 1]]
|
|
@@ -1315,7 +1323,11 @@ def settings_argv(provider, settings):
|
|
|
1315
1323
|
(anything else counts as unknown). [] when nothing is known."""
|
|
1316
1324
|
effort = settings.get("effort") if isinstance(settings, dict) else None
|
|
1317
1325
|
effort = effort if isinstance(effort, str) and EFFORT_VALUE_RE.match(effort) else None
|
|
1318
|
-
|
|
1326
|
+
flags = settings_flags(provider, history_model(settings), effort)
|
|
1327
|
+
if provider == "codex" and isinstance(settings, dict) and settings.get("speed") in ("normal", "fast", "ultrafast"):
|
|
1328
|
+
flags += ["-c", "service_tier=" + {"normal": "default", "fast": "fast",
|
|
1329
|
+
"ultrafast": "ultrafast"}[settings["speed"]]]
|
|
1330
|
+
return flags
|
|
1319
1331
|
|
|
1320
1332
|
|
|
1321
1333
|
def argv_settings(provider, argv):
|
|
@@ -1336,6 +1348,10 @@ def argv_settings(provider, argv):
|
|
|
1336
1348
|
elif provider == "codex" and arg == "-c" and codex_effort_ok(val):
|
|
1337
1349
|
found["effort"] = val[len(CODEX_EFFORT_KEY):]
|
|
1338
1350
|
i += 1
|
|
1351
|
+
elif provider == "codex" and arg == "-c" and val.startswith("service_tier=") and codex_speed_ok(val):
|
|
1352
|
+
from codex_speed import speed
|
|
1353
|
+
found["speed"] = speed(val.split("=", 1)[1].strip("\"'"))
|
|
1354
|
+
i += 1
|
|
1339
1355
|
i += 1
|
|
1340
1356
|
return found
|
|
1341
1357
|
|
|
@@ -1355,14 +1371,16 @@ def claude_model_left(launch, served):
|
|
|
1355
1371
|
return family is not None and family != launch
|
|
1356
1372
|
|
|
1357
1373
|
|
|
1358
|
-
def without_settings(kept, model, effort):
|
|
1374
|
+
def without_settings(kept, model, effort, speed=False):
|
|
1359
1375
|
"""``kept`` without its model options (-m V, --model V, --model=V) when ``model`` and its
|
|
1360
1376
|
effort options (--effort V, codex's -c model_reasoning_effort=V) when ``effort``. Both
|
|
1361
1377
|
parsers refuse a value that starts with "-", so no value is ever taken for an option."""
|
|
1362
1378
|
out, i = [], 0
|
|
1363
1379
|
while i < len(kept):
|
|
1364
1380
|
arg = kept[i]
|
|
1365
|
-
|
|
1381
|
+
val = kept[i + 1] if i + 1 < len(kept) else ""
|
|
1382
|
+
if ((model and arg in ("-m", "--model")) or (effort and arg == "--effort")
|
|
1383
|
+
or (arg == "-c" and ((effort and codex_effort_ok(val)) or (speed and codex_speed_ok(val))))):
|
|
1366
1384
|
i += 2
|
|
1367
1385
|
continue
|
|
1368
1386
|
if not (model and arg.startswith("--model=")):
|
|
@@ -1407,10 +1425,12 @@ def relaunch_argv(provider, argv, sid, cls, prompt=None, settings=None, detail=N
|
|
|
1407
1425
|
left = claude_model_left(argv_settings("claude", kept).get("model"), settings.get("model"))
|
|
1408
1426
|
settings = {"effort": settings.get("effort")}
|
|
1409
1427
|
flags = settings_argv(provider, settings)
|
|
1410
|
-
model
|
|
1411
|
-
|
|
1428
|
+
model = left or "--model" in flags or "-m" in flags
|
|
1429
|
+
effort = "--effort" in flags or any(codex_effort_ok(value) for value in flags)
|
|
1430
|
+
speed = provider == "codex" and settings.get("speed") in ("normal", "fast", "ultrafast")
|
|
1431
|
+
if provider == "codex" and not model and history_model(settings):
|
|
1412
1432
|
model = effort = True # no launch -m or -c alone: neither, and the thread restores both
|
|
1413
|
-
return head + without_settings(kept, model, effort) + flags + tail
|
|
1433
|
+
return head + without_settings(kept, model, effort, speed) + flags + tail
|
|
1414
1434
|
|
|
1415
1435
|
|
|
1416
1436
|
# ---- relaunch / state files ---------------------------------------------------------
|
|
@@ -2721,7 +2741,7 @@ class Watcher:
|
|
|
2721
2741
|
try:
|
|
2722
2742
|
found = session_settings(self.provider, self.transcript)
|
|
2723
2743
|
self.served_model = history_model(found)
|
|
2724
|
-
if not found.get("model") and not found.get("effort"):
|
|
2744
|
+
if not found.get("model") and not found.get("effort") and not found.get("speed"):
|
|
2725
2745
|
return None
|
|
2726
2746
|
# The catalog only clamps an effort to the model and supplies Codex's default
|
|
2727
2747
|
# one: this pool's copy and caches, the other pool's only when the chain named it.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Serialize OAuth token POSTs with Claude Code's native refresh locks.
|
|
2
|
+
|
|
3
|
+
Claude Code 2.1.310 Gnr/Vri use proper-lockfile directories: first
|
|
4
|
+
<config>/.oauth_refresh.lock, then <realpath(config)>.lock, with a 60-second
|
|
5
|
+
stale deadline and five-second mtime renewal. Ec changes callback scheduling,
|
|
6
|
+
not that on-disk protocol. The optional native owner sidecar only accelerates
|
|
7
|
+
proven-dead-holder recovery; without one, native clients retain the stale gate.
|
|
8
|
+
|
|
9
|
+
Hold this lock across credential reread, token exchange and rotated-token save.
|
|
10
|
+
The separate .storage-write.lock protects only short credential transactions.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import stat
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
STALE_SECONDS = 60.0
|
|
21
|
+
UPDATE_SECONDS = 5.0
|
|
22
|
+
RETRY_SECONDS = 0.05
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _snapshot(path: Path) -> tuple[int, int, int] | None:
|
|
26
|
+
try:
|
|
27
|
+
value = path.lstat()
|
|
28
|
+
if stat.S_ISDIR(value.st_mode):
|
|
29
|
+
return value.st_dev, value.st_ino, value.st_mtime_ns
|
|
30
|
+
except OSError:
|
|
31
|
+
pass
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RefreshLock:
|
|
36
|
+
"""Bounded, fail-closed lock; call release even after an exchange fails."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, directory: str | Path, timeout: float = 1.0):
|
|
39
|
+
directory = Path(directory).absolute()
|
|
40
|
+
self.paths = (directory / ".oauth_refresh.lock", Path(str(directory.resolve()) + ".lock"))
|
|
41
|
+
self.timeout = max(0.0, timeout)
|
|
42
|
+
self._held: list[tuple[Path, int, tuple[int, int, int]]] = []
|
|
43
|
+
self._mutex = threading.Lock()
|
|
44
|
+
self._stop = threading.Event()
|
|
45
|
+
self._thread: threading.Thread | None = None
|
|
46
|
+
self._compromised = False
|
|
47
|
+
|
|
48
|
+
def acquire(self) -> bool:
|
|
49
|
+
deadline = time.monotonic() + self.timeout
|
|
50
|
+
with self._mutex:
|
|
51
|
+
if self._held:
|
|
52
|
+
return not self._compromised and all(
|
|
53
|
+
_snapshot(path) == stamp and time.time_ns() - stamp[2] < STALE_SECONDS * 1e9
|
|
54
|
+
for path, _, stamp in self._held)
|
|
55
|
+
self._compromised = False
|
|
56
|
+
self._stop.clear()
|
|
57
|
+
while True:
|
|
58
|
+
for path in self.paths:
|
|
59
|
+
entry = self._claim(path)
|
|
60
|
+
if entry is None:
|
|
61
|
+
break
|
|
62
|
+
self._held.append(entry)
|
|
63
|
+
if len(self._held) == len(self.paths):
|
|
64
|
+
self._thread = threading.Thread(target=self._heartbeat, daemon=True)
|
|
65
|
+
try:
|
|
66
|
+
self._thread.start()
|
|
67
|
+
except RuntimeError:
|
|
68
|
+
self._release_paths()
|
|
69
|
+
self._thread = None
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
self._release_paths()
|
|
73
|
+
remaining = deadline - time.monotonic()
|
|
74
|
+
if remaining <= 0:
|
|
75
|
+
return False
|
|
76
|
+
time.sleep(min(RETRY_SECONDS, remaining))
|
|
77
|
+
|
|
78
|
+
def _claim(self, path: Path) -> tuple[Path, int, tuple[int, int, int]] | None:
|
|
79
|
+
try:
|
|
80
|
+
path.mkdir(mode=0o700)
|
|
81
|
+
except FileExistsError:
|
|
82
|
+
before = _snapshot(path)
|
|
83
|
+
if before is None or time.time_ns() - before[2] <= STALE_SECONDS * 1e9:
|
|
84
|
+
return None
|
|
85
|
+
try:
|
|
86
|
+
if _snapshot(path) != before:
|
|
87
|
+
return None
|
|
88
|
+
path.rmdir()
|
|
89
|
+
path.mkdir(mode=0o700)
|
|
90
|
+
except OSError:
|
|
91
|
+
return None
|
|
92
|
+
except OSError:
|
|
93
|
+
return None
|
|
94
|
+
before = _snapshot(path)
|
|
95
|
+
descriptor = None
|
|
96
|
+
claimed = False
|
|
97
|
+
try:
|
|
98
|
+
descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
|
99
|
+
value = os.fstat(descriptor)
|
|
100
|
+
if before is None or (value.st_dev, value.st_ino, value.st_mtime_ns) != before:
|
|
101
|
+
return None
|
|
102
|
+
now = time.time_ns()
|
|
103
|
+
os.utime(descriptor, ns=(now, now))
|
|
104
|
+
value = os.fstat(descriptor)
|
|
105
|
+
stamp = value.st_dev, value.st_ino, value.st_mtime_ns
|
|
106
|
+
before = stamp
|
|
107
|
+
if _snapshot(path) != stamp:
|
|
108
|
+
return None
|
|
109
|
+
claimed = True
|
|
110
|
+
result, descriptor = (path, descriptor, stamp), None
|
|
111
|
+
return result
|
|
112
|
+
except OSError:
|
|
113
|
+
return None
|
|
114
|
+
finally:
|
|
115
|
+
if descriptor is not None:
|
|
116
|
+
os.close(descriptor)
|
|
117
|
+
if not claimed and before is not None and _snapshot(path) == before:
|
|
118
|
+
try:
|
|
119
|
+
path.rmdir()
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
def _renew(self) -> bool:
|
|
124
|
+
for index, (path, descriptor, stamp) in enumerate(self._held):
|
|
125
|
+
if _snapshot(path) != stamp or time.time_ns() - stamp[2] >= STALE_SECONDS * 1e9:
|
|
126
|
+
return False
|
|
127
|
+
try:
|
|
128
|
+
now = time.time_ns()
|
|
129
|
+
os.utime(descriptor, ns=(now, now))
|
|
130
|
+
value = os.fstat(descriptor)
|
|
131
|
+
updated = value.st_dev, value.st_ino, value.st_mtime_ns
|
|
132
|
+
if _snapshot(path) != updated:
|
|
133
|
+
return False
|
|
134
|
+
self._held[index] = path, descriptor, updated
|
|
135
|
+
except OSError:
|
|
136
|
+
return False
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
def _heartbeat(self) -> None:
|
|
140
|
+
while not self._stop.wait(UPDATE_SECONDS):
|
|
141
|
+
with self._mutex:
|
|
142
|
+
if not self._renew():
|
|
143
|
+
self._compromised = True
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
def _release_paths(self) -> None:
|
|
147
|
+
for path, descriptor, stamp in reversed(self._held):
|
|
148
|
+
try:
|
|
149
|
+
if _snapshot(path) == stamp:
|
|
150
|
+
path.rmdir()
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
finally:
|
|
154
|
+
os.close(descriptor)
|
|
155
|
+
self._held.clear()
|
|
156
|
+
|
|
157
|
+
def release(self) -> None:
|
|
158
|
+
self._stop.set()
|
|
159
|
+
thread = self._thread
|
|
160
|
+
if thread is not None:
|
|
161
|
+
thread.join(timeout=1.0)
|
|
162
|
+
with self._mutex:
|
|
163
|
+
self._release_paths()
|
|
164
|
+
self._thread = None
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def compromised(self) -> bool:
|
|
168
|
+
with self._mutex:
|
|
169
|
+
if any(_snapshot(path) != stamp or time.time_ns() - stamp[2] >= STALE_SECONDS * 1e9
|
|
170
|
+
for path, _, stamp in self._held):
|
|
171
|
+
self._compromised = True
|
|
172
|
+
return self._compromised
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Refresh machine-local Claude logins without racing the native rotating grant.
|
|
2
|
+
|
|
3
|
+
Claude Code 2.1.310 holds both refresh locks across the token request. The storage
|
|
4
|
+
lock protects only the final read/compare/write, never the network round trip.
|
|
5
|
+
An invalid_grant response can leave an empty-token tombstone; that is not a newer
|
|
6
|
+
login and must not displace a successfully rotated grant. A removed login is logout.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import time
|
|
13
|
+
import urllib.error
|
|
14
|
+
import urllib.request
|
|
15
|
+
|
|
16
|
+
from claude_oauth_lock import RefreshLock
|
|
17
|
+
from mcp_oauth import CLIENT_LOCK, CLIENT_LOCK_STALE_S, ClientLock
|
|
18
|
+
|
|
19
|
+
REFRESH_MIN_EXPIRED = 300
|
|
20
|
+
REFRESH_FAIL_BACKOFF = 600
|
|
21
|
+
REFRESH_DENIED_BACKOFF = 21600
|
|
22
|
+
CLIENT_LOCK_PROBE_S = 1.0
|
|
23
|
+
CLIENT_LOCK_WAIT_S = 10.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def read_login(store):
|
|
27
|
+
"""Unreadable, missing, or non-object logins cannot authorize a token write."""
|
|
28
|
+
try:
|
|
29
|
+
doc = store.read()
|
|
30
|
+
login = doc.get('claudeAiOauth') if isinstance(doc, dict) else None
|
|
31
|
+
return dict(login) if isinstance(login, dict) else None
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def client_lock_busy(directory):
|
|
37
|
+
deadline = time.monotonic() + CLIENT_LOCK_PROBE_S
|
|
38
|
+
while True:
|
|
39
|
+
try:
|
|
40
|
+
if time.time() - os.stat(os.path.join(directory, CLIENT_LOCK)).st_mtime > CLIENT_LOCK_STALE_S:
|
|
41
|
+
return False
|
|
42
|
+
except OSError:
|
|
43
|
+
return False
|
|
44
|
+
if time.monotonic() >= deadline:
|
|
45
|
+
return True
|
|
46
|
+
time.sleep(0.05)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def rotated_login(login, data, now):
|
|
50
|
+
result = dict(login, accessToken=data['access_token'])
|
|
51
|
+
if data.get('refresh_token'):
|
|
52
|
+
result['refreshToken'] = data['refresh_token']
|
|
53
|
+
result['expiresAt'] = int((now + float(data.get('expires_in') or 3600)) * 1000)
|
|
54
|
+
if data.get('refresh_token_expires_in'):
|
|
55
|
+
result['refreshTokenExpiresAt'] = int((now + float(data['refresh_token_expires_in'])) * 1000)
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OAuthRefresher:
|
|
60
|
+
def __init__(self, *, now, force, token_url, client_id, say, park_dead_grant, clear_expired):
|
|
61
|
+
self.now = now
|
|
62
|
+
self.force = force
|
|
63
|
+
self.token_url = token_url
|
|
64
|
+
self.client_id = client_id
|
|
65
|
+
self.say = say
|
|
66
|
+
self.park = park_dead_grant
|
|
67
|
+
self.clear_expired = clear_expired
|
|
68
|
+
|
|
69
|
+
def refresh(self, aid, directory, store):
|
|
70
|
+
# This lock must fail closed: a grace period after access expiry does not
|
|
71
|
+
# prove that another process has stopped refreshing the same rotating grant.
|
|
72
|
+
lock = RefreshLock(directory)
|
|
73
|
+
if not lock.acquire():
|
|
74
|
+
self.say(f'{aid}: oauth refresh skipped — the client holds its refresh lock; the next pass retries')
|
|
75
|
+
return None
|
|
76
|
+
try:
|
|
77
|
+
# Reread AFTER ownership: the client may have refreshed while we waited.
|
|
78
|
+
login = read_login(store)
|
|
79
|
+
if login is None:
|
|
80
|
+
return None
|
|
81
|
+
if login.get('accessToken') and login.get('expiresAt', 0) / 1000 > self.now + 60:
|
|
82
|
+
return login['accessToken']
|
|
83
|
+
if not self.eligible(aid, directory, login):
|
|
84
|
+
return None
|
|
85
|
+
if client_lock_busy(directory) or lock.compromised:
|
|
86
|
+
self.say(f'{aid}: oauth refresh skipped — the client holds its credential lock ({CLIENT_LOCK})')
|
|
87
|
+
return None
|
|
88
|
+
data = self.post(aid, directory, login['refreshToken'], store)
|
|
89
|
+
if data is None:
|
|
90
|
+
return None
|
|
91
|
+
rotated = rotated_login(login, data, self.now)
|
|
92
|
+
if not self.save(aid, directory, store, login['refreshToken'], rotated):
|
|
93
|
+
return None
|
|
94
|
+
try:
|
|
95
|
+
os.remove(os.path.join(directory, '.oauth-refresh.json'))
|
|
96
|
+
except OSError:
|
|
97
|
+
pass
|
|
98
|
+
if self.clear_expired(directory):
|
|
99
|
+
self.say(f'{aid}: dead-auth marker cleared (refresh grant works again)')
|
|
100
|
+
self.say(f'{aid}: oauth access token refreshed via refresh-token grant')
|
|
101
|
+
return rotated['accessToken']
|
|
102
|
+
finally:
|
|
103
|
+
lock.release()
|
|
104
|
+
|
|
105
|
+
def eligible(self, aid, directory, login):
|
|
106
|
+
if not login.get('refreshToken'):
|
|
107
|
+
self.park(aid, directory, 'no-refresh-token',
|
|
108
|
+
'credential has no refresh token and its access token expired')
|
|
109
|
+
return False
|
|
110
|
+
if login.get('expiresAt', 0) / 1000 > self.now - REFRESH_MIN_EXPIRED:
|
|
111
|
+
return False
|
|
112
|
+
refresh_expiry = login.get('refreshTokenExpiresAt')
|
|
113
|
+
if refresh_expiry and refresh_expiry / 1000 <= self.now:
|
|
114
|
+
self.park(aid, directory, 'refresh-token-expired',
|
|
115
|
+
'the refresh token itself expired; only a re-login can fix it')
|
|
116
|
+
return False
|
|
117
|
+
if not self.force:
|
|
118
|
+
try:
|
|
119
|
+
with open(os.path.join(directory, '.oauth-refresh.json')) as stream:
|
|
120
|
+
if json.load(stream).get('retry_after', 0) > self.now:
|
|
121
|
+
return False
|
|
122
|
+
except (OSError, ValueError, AttributeError):
|
|
123
|
+
pass
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
def back_off(self, aid, directory, wait, why, denials=0):
|
|
127
|
+
path = os.path.join(directory, '.oauth-refresh.json')
|
|
128
|
+
try:
|
|
129
|
+
with open(path + '.tmp', 'w') as stream:
|
|
130
|
+
json.dump({'retry_after': int(self.now + wait), 'error': why, 'denials': denials,
|
|
131
|
+
'at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}, stream)
|
|
132
|
+
os.replace(path + '.tmp', path)
|
|
133
|
+
except OSError:
|
|
134
|
+
pass
|
|
135
|
+
self.say(f'{aid}: oauth refresh failed ({why}); backing off {wait}s; limits left as-is')
|
|
136
|
+
|
|
137
|
+
def post(self, aid, directory, refresh_token, store):
|
|
138
|
+
body = json.dumps({'grant_type': 'refresh_token', 'refresh_token': refresh_token,
|
|
139
|
+
'client_id': self.client_id}).encode()
|
|
140
|
+
request = urllib.request.Request(self.token_url, data=body, headers={
|
|
141
|
+
'Content-Type': 'application/json', 'User-Agent': 'claude-multiacc/1.0',
|
|
142
|
+
})
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads(urllib.request.urlopen(request, timeout=30).read().decode())
|
|
145
|
+
except Exception as error:
|
|
146
|
+
# A sign-in/logout can replace this grant without the refresh lock.
|
|
147
|
+
# A failure of the old request must never park the new login.
|
|
148
|
+
disk = read_login(store)
|
|
149
|
+
if disk is None or disk.get('refreshToken') != refresh_token:
|
|
150
|
+
self.say(f'{aid}: oauth login changed during a failed refresh; leaving its state unchanged')
|
|
151
|
+
return None
|
|
152
|
+
if isinstance(error, urllib.error.HTTPError) and error.code in (400, 401, 403):
|
|
153
|
+
self.denied(aid, directory, error, store, refresh_token)
|
|
154
|
+
else:
|
|
155
|
+
why = f'HTTP {error.code}' if isinstance(error, urllib.error.HTTPError) else type(error).__name__
|
|
156
|
+
self.back_off(aid, directory, REFRESH_FAIL_BACKOFF, why)
|
|
157
|
+
return None
|
|
158
|
+
if not isinstance(data, dict) or not data.get('access_token'):
|
|
159
|
+
disk = read_login(store)
|
|
160
|
+
if disk is not None and disk.get('refreshToken') == refresh_token:
|
|
161
|
+
self.back_off(aid, directory, REFRESH_DENIED_BACKOFF, 'no access_token in response')
|
|
162
|
+
return None
|
|
163
|
+
return data
|
|
164
|
+
|
|
165
|
+
def denied(self, aid, directory, error, store, refresh_token):
|
|
166
|
+
body = ''
|
|
167
|
+
try:
|
|
168
|
+
body = error.read(400).decode('utf-8', 'replace')
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
denials = 1
|
|
172
|
+
try:
|
|
173
|
+
with open(os.path.join(directory, '.oauth-refresh.json')) as stream:
|
|
174
|
+
denials += int(json.load(stream).get('denials', 0))
|
|
175
|
+
except (OSError, ValueError, TypeError, AttributeError):
|
|
176
|
+
pass
|
|
177
|
+
lock = ClientLock(directory, CLIENT_LOCK_WAIT_S)
|
|
178
|
+
if not lock.acquire():
|
|
179
|
+
return # no rotation occurred; retry without marking an uncertain login
|
|
180
|
+
try:
|
|
181
|
+
disk = read_login(store)
|
|
182
|
+
if disk is None or disk.get('refreshToken') != refresh_token:
|
|
183
|
+
return
|
|
184
|
+
if 'invalid_grant' in body:
|
|
185
|
+
self.park(aid, directory, f'refresh-denied-http-{error.code}',
|
|
186
|
+
'the refresh grant was refused as invalid_grant (revoked or rotated away)')
|
|
187
|
+
elif denials >= 3:
|
|
188
|
+
self.park(aid, directory, f'refresh-denied-http-{error.code}',
|
|
189
|
+
f'the refresh grant was refused {denials} times in a row')
|
|
190
|
+
self.back_off(aid, directory, REFRESH_DENIED_BACKOFF,
|
|
191
|
+
f'HTTP {error.code} — refresh token may be revoked; re-login needed', denials)
|
|
192
|
+
finally:
|
|
193
|
+
lock.release()
|
|
194
|
+
|
|
195
|
+
def save(self, aid, directory, store, started_with, login):
|
|
196
|
+
lock = ClientLock(directory, CLIENT_LOCK_WAIT_S)
|
|
197
|
+
held = False
|
|
198
|
+
try:
|
|
199
|
+
held = lock.acquire()
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
if not held and os.path.isdir(lock.path):
|
|
203
|
+
self.say(f'{aid}: the client kept its credential lock ({CLIENT_LOCK}) past '
|
|
204
|
+
f'{int(CLIENT_LOCK_WAIT_S)}s — writing the rotated login without it')
|
|
205
|
+
try:
|
|
206
|
+
disk = read_login(store)
|
|
207
|
+
# Native invalid_grant clears these three fields but leaves the login
|
|
208
|
+
# object. A missing document/key is logout, never a tombstone to restore.
|
|
209
|
+
tombstone = (disk is not None and disk.get('refreshToken') == ''
|
|
210
|
+
and disk.get('accessToken') == '' and disk.get('expiresAt') == 0)
|
|
211
|
+
if disk is None or (disk.get('refreshToken') != started_with and not tombstone):
|
|
212
|
+
self.say(f'{aid}: credential was refreshed by something else mid-flight — '
|
|
213
|
+
f'keeping the newer one in the {store.kind} (or its removal)')
|
|
214
|
+
return False
|
|
215
|
+
try:
|
|
216
|
+
# Store implementations merge only this key, preserving current MCP state.
|
|
217
|
+
store.write({'claudeAiOauth': login})
|
|
218
|
+
except Exception as error:
|
|
219
|
+
self.say(f'{aid}: token refreshed but credentials NOT persisted '
|
|
220
|
+
f'({type(error).__name__}) — re-login may be needed')
|
|
221
|
+
return False
|
|
222
|
+
return True
|
|
223
|
+
finally:
|
|
224
|
+
if held:
|
|
225
|
+
lock.release()
|