claude-multiacc 2.0.53 → 2.0.55

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.
Files changed (74) hide show
  1. package/README.md +5 -3
  2. package/bin/codex +26 -5
  3. package/docs/AUTORESUME.md +21 -6
  4. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  5. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  6. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  7. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  8. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  9. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  17. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  18. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  19. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  20. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  21. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  22. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  23. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  24. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  25. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  26. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  27. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  28. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  29. package/lib/autoresume.py +22 -5
  30. package/lib/autoresume_receipt.py +78 -0
  31. package/lib/claude_pool_prepare.py +2 -0
  32. package/lib/codex_autoresume_mode.py +58 -0
  33. package/package.json +1 -1
  34. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  35. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  36. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  37. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  38. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  39. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  40. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  41. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  42. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  43. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  44. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  45. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  46. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  47. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  48. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  49. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  50. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  51. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  52. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  53. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  54. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  55. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  56. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  57. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  58. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  59. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  60. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  61. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  62. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  63. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  64. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
  65. package/tests/autoresume_tmux_support.py +135 -0
  66. package/tests/fake_codex_tui.py +81 -0
  67. package/tests/run-tests.sh +10 -1
  68. package/tests/test_autoresume.py +7 -0
  69. package/tests/test_autoresume_codex_records.py +74 -0
  70. package/tests/test_autoresume_cursor.py +87 -0
  71. package/tests/test_autoresume_mode.py +89 -0
  72. package/tests/test_autoresume_receipt.py +117 -0
  73. package/tests/test_autoresume_tmux.py +100 -0
  74. package/tests/test_autoresume_tmux_modes.py +80 -0
@@ -0,0 +1,100 @@
1
+ """Actual tmux + shell + production shim/watcher, with a disposable fake Codex TUI."""
2
+
3
+ import os
4
+ import signal
5
+ import time
6
+ import unittest
7
+
8
+ from autoresume_tmux_support import SID, TMUX, TmuxScenario
9
+
10
+ QUOTA = {"message": "You've hit your usage limit. Try again later.",
11
+ "codex_error_info": "usage_limit_exceeded"}
12
+
13
+
14
+ @unittest.skipUnless(TMUX, "real tmux is unavailable")
15
+ class RealTmuxTests(unittest.TestCase):
16
+ def scenario(self, args=None, **environment):
17
+ case = TmuxScenario(**environment)
18
+ self.addCleanup(case.close)
19
+ case.start(args)
20
+ return case
21
+
22
+ def assert_rotated(self, case):
23
+ case.wait(lambda: len(case.records("ready")) == 2, "same session resumed on another account")
24
+ case.wait(lambda: "autoresume switch" in case.log.read_text(), "watcher confirms switch")
25
+ first, second = case.records("launch")
26
+ self.assertEqual((first["account"], second["account"]), ("acct-01", "acct-02"))
27
+ arguments = [arg for arg in second["argv"] if arg != "--no-daemon"]
28
+ self.assertEqual(arguments[:3], ["resume", "--yolo", SID])
29
+ self.assertIn("Continue", second["argv"][-1])
30
+ self.assertEqual(first["cwd"], second["cwd"])
31
+ self.assertEqual(case.records("keys"), [], "no keystroke may reach either TUI")
32
+ self.assertEqual(case.records("signal")[0]["signal"], signal.SIGTERM)
33
+ self.assertIn("error-cooldown", (case.pool / "acct-01/.limited").read_text())
34
+ self.assertEqual(list((case.pool / "tmp/autoresume").glob("r-*")), [])
35
+
36
+ def test_legacy_quota_resumes_same_session_and_prompt(self):
37
+ case = self.scenario(FAKE_LEGACY="1")
38
+ case.append({"type": "task_complete", "error": QUOTA})
39
+ self.assert_rotated(case)
40
+
41
+ def test_modern_client_runs_locally_and_resumes_same_session(self):
42
+ case = self.scenario()
43
+ case.append({"type": "task_complete", "error": QUOTA})
44
+ self.assert_rotated(case)
45
+ self.assertTrue(all("--no-daemon" in row["argv"] for row in case.records("launch")))
46
+
47
+ def test_quota_hold_leaves_tui_untouched_then_recovers(self):
48
+ case = self.scenario()
49
+ marker = case.pool / "acct-02/.limited"
50
+ marker.write_text(f"{int(time.time()) + 3600}\nbucket=error-cooldown reason=error-cooldown\n")
51
+ case.append({"type": "task_complete", "error": QUOTA})
52
+ case.wait(lambda: "autoresume hold" in case.log.read_text(), "no-room hold")
53
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
54
+ marker.unlink()
55
+ self.assert_rotated(case)
56
+
57
+ def test_user_message_cancels_pending_quota(self):
58
+ case = self.scenario(CODEX_MULTIACC_AR_GRACE="1.5")
59
+ case.append({"type": "task_complete", "error": QUOTA})
60
+ case.wait(lambda: "autoresume detect" in case.log.read_text(), "quota detected")
61
+ case.append({"type": "user_message", "message": "I will continue manually"})
62
+ time.sleep(2)
63
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
64
+ self.assertEqual(len(case.records("launch")), 1)
65
+
66
+ def test_interrupted_turn_cancels_then_next_quota_still_recovers(self):
67
+ case = self.scenario(CODEX_MULTIACC_AR_GRACE="1.5")
68
+ case.append({"type": "task_complete", "error": QUOTA})
69
+ case.wait(lambda: "autoresume detect" in case.log.read_text(), "quota detected")
70
+ case.append({"type": "turn_aborted", "reason": "interrupted"})
71
+ time.sleep(2)
72
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
73
+ case.append({"type": "task_complete", "error": QUOTA})
74
+ self.assert_rotated(case)
75
+
76
+ def test_normal_completion_and_old_quota_do_not_restart(self):
77
+ case = self.scenario()
78
+ case.append({"type": "task_complete", "error": QUOTA}, age=60)
79
+ case.append({"type": "task_complete", "last_agent_message": "Finished successfully"})
80
+ time.sleep(1.5)
81
+ self.assertNotIn("autoresume detect", case.log.read_text())
82
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
83
+
84
+ def test_synchronized_panes_refuse_without_stopping_or_typing(self):
85
+ case = self.scenario()
86
+ case.tmux("set-window-option", "-t", case.pane, "synchronize-panes", "on")
87
+ case.append({"type": "task_complete", "error": QUOTA})
88
+ case.wait(lambda: "autoresume giveup" in case.log.read_text(), "unsafe pane refused")
89
+ self.assertIn("reason=sync", case.log.read_text())
90
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
91
+ os.kill(case.records("launch")[0]["pid"], 0)
92
+
93
+ def test_unresponsive_client_is_killed_then_resumed(self):
94
+ case = self.scenario(FAKE_IGNORE_TERM="1")
95
+ case.append({"type": "task_complete", "error": QUOTA})
96
+ self.assert_rotated(case)
97
+
98
+
99
+ if __name__ == "__main__":
100
+ unittest.main()
@@ -0,0 +1,80 @@
1
+ """Real-tmux launch modes, kill switches and proof that a resumed prompt was consumed."""
2
+
3
+ import time
4
+ import unittest
5
+
6
+ from autoresume_tmux_support import SID, TMUX, TmuxScenario
7
+
8
+ QUOTA = {"type": "task_complete", "error": {"message": "You've hit your usage limit.",
9
+ "codex_error_info": "usage_limit_exceeded"}}
10
+
11
+
12
+ @unittest.skipUnless(TMUX, "real tmux is unavailable")
13
+ class RealTmuxModesTests(unittest.TestCase):
14
+ def scenario(self, args=None, supervised=True, **environment):
15
+ case = TmuxScenario(**environment)
16
+ self.addCleanup(case.close)
17
+ case.start(args, supervised=supervised)
18
+ return case
19
+
20
+ def test_resume_id_and_last_preserve_session_and_submit_prompt(self):
21
+ for target in (SID, "--last"):
22
+ with self.subTest(target=target):
23
+ case = self.scenario(["--yolo", "resume", target])
24
+ case.append(QUOTA)
25
+ case.wait(lambda: len(case.records("prompt")) == 1, "resume prompt submitted")
26
+ case.wait(lambda: "autoresume switch" in case.log.read_text(), "switch receipt")
27
+ first, second = case.records("launch")
28
+ self.assertEqual((first["account"], second["account"]), ("acct-01", "acct-02"))
29
+ self.assertIn(SID, second["argv"])
30
+ self.assertNotIn("--last", second["argv"])
31
+ self.assertEqual(case.records("keys"), [])
32
+ case.close()
33
+
34
+ def test_explicit_no_daemon_is_supported_and_not_duplicated(self):
35
+ case = self.scenario(["--no-daemon", "--yolo"])
36
+ case.append(QUOTA)
37
+ case.wait(lambda: "autoresume switch" in case.log.read_text(), "explicit local-mode restart")
38
+ self.assertEqual([row["argv"].count("--no-daemon") for row in case.records("launch")], [1, 1])
39
+ self.assertEqual(case.records("keys"), [])
40
+
41
+ def test_bare_codex_launch_resumes_without_empty_array_failure(self):
42
+ case = self.scenario([])
43
+ case.append(QUOTA)
44
+ case.wait(lambda: "autoresume switch" in case.log.read_text(), "bare command restart")
45
+ first, second = case.records("launch")
46
+ self.assertEqual(first["argv"], ["--no-daemon"])
47
+ self.assertEqual(second["argv"][:3], ["resume", "--no-daemon", SID])
48
+ self.assertEqual(case.records("prompt")[0]["text"], second["argv"][-1])
49
+ self.assertEqual(case.records("keys"), [])
50
+
51
+ def test_environment_kill_switch_preserves_daemon_mode(self):
52
+ case = self.scenario(supervised=False, CODEX_MULTIACC_AUTORESUME="0")
53
+ case.append(QUOTA)
54
+ time.sleep(1)
55
+ self.assertNotIn("--no-daemon", case.records("launch")[0]["argv"])
56
+ self.assertNotIn("autoresume watch", case.log.read_text())
57
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
58
+
59
+ def test_pool_kill_switch_stops_existing_watcher(self):
60
+ case = self.scenario()
61
+ (case.pool / "autoresume.off").touch()
62
+ case.wait(lambda: not list((case.pool / "tmp/autoresume").glob("*.state")), "watcher stopped")
63
+ case.append(QUOTA)
64
+ time.sleep(1)
65
+ self.assertEqual((case.records("signal"), case.records("keys")), ([], []))
66
+
67
+ def test_missing_resume_prompt_is_reported_as_failed_handoff(self):
68
+ case = self.scenario(FAKE_DROP_PROMPT="1")
69
+ case.append(QUOTA)
70
+ case.wait(lambda: len(case.records("ready")) >= 2, "resumed client launched")
71
+ case.wait(lambda: "stopped=1" in case.log.read_text(), "missing prompt reported", timeout=20)
72
+ self.assertNotIn("autoresume switch", case.log.read_text())
73
+ self.assertEqual(case.log.read_text().count("autoresume detect"), 1)
74
+ self.assertEqual(len(case.records("launch")), 2)
75
+ self.assertEqual(case.records("prompt"), [])
76
+ self.assertEqual(case.records("keys"), [])
77
+
78
+
79
+ if __name__ == "__main__":
80
+ unittest.main()