claude-multiacc 2.0.61 → 2.0.65

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 (244) hide show
  1. package/README.md +6 -2
  2. package/bin/claude-accounts +4 -4
  3. package/bin/codex +54 -13
  4. package/bin/codex-accounts +28 -7
  5. package/docs/ACCOUNT_OPERATIONS.md +2 -2
  6. package/docs/AUTORESUME.md +16 -2
  7. package/docs/CODEX.md +58 -2
  8. package/docs/CODEX_MCP_AUTH.md +58 -0
  9. package/lib/__pycache__/audit.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
  12. package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
  13. package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
  14. package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
  15. package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
  16. package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
  17. package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
  28. package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
  29. package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
  30. package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
  34. package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
  35. package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
  36. package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
  37. package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
  38. package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
  39. package/lib/__pycache__/report.cpython-314.pyc +0 -0
  40. package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
  41. package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
  42. package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
  43. package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
  44. package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
  45. package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
  46. package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
  47. package/lib/autoresume.py +34 -13
  48. package/lib/autoresume_cross.py +2 -1
  49. package/lib/autoresume_handoff.py +13 -5
  50. package/lib/codex_exec_stdin.sh +45 -0
  51. package/lib/codex_mcp_oauth.py +204 -0
  52. package/lib/codex_mcp_oauth.sh +29 -0
  53. package/lib/codex_mcp_oauth_locks.py +158 -0
  54. package/lib/codex_mcp_oauth_native.py +102 -0
  55. package/lib/codex_mcp_oauth_store.py +149 -0
  56. package/lib/codex_resume_cwd.py +57 -0
  57. package/lib/codex_retry_process.sh +37 -0
  58. package/lib/codex_speed.py +351 -0
  59. package/lib/codex_speed.sh +39 -0
  60. package/lib/common.sh +16 -3
  61. package/lib/model_match.py +4 -1
  62. package/lib/session_settings.py +25 -6
  63. package/lib/shim_path.py +6 -0
  64. package/package.json +10 -5
  65. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
  68. package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
  69. package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
  70. package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
  101. package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
  102. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
  103. package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
  104. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
  105. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
  106. package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
  107. package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
  108. package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
  109. package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
  111. package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
  112. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
  113. package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
  114. package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
  115. package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
  116. package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
  117. package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
  118. package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
  119. package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
  120. package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
  121. package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
  122. package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
  123. package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
  124. package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
  125. package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
  126. package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
  127. package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
  128. package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
  129. package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
  130. package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
  131. package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
  132. package/tests/fake_codex_tui.py +23 -1
  133. package/tests/run-tests.sh +23 -7
  134. package/tests/test_autoresume_cwd.py +71 -0
  135. package/tests/test_autoresume_cwd_handoff.py +92 -0
  136. package/tests/test_autoresume_cwd_reader.py +105 -0
  137. package/tests/test_autoresume_cwd_tmux.py +54 -0
  138. package/tests/test_codex_exec_stdin.py +178 -0
  139. package/tests/test_codex_mcp_oauth.py +146 -0
  140. package/tests/test_codex_mcp_oauth_adopt.py +87 -0
  141. package/tests/test_codex_mcp_oauth_migration.py +141 -0
  142. package/tests/test_codex_mcp_oauth_native.py +109 -0
  143. package/tests/test_codex_mcp_oauth_safety.py +130 -0
  144. package/tests/test_codex_mcp_oauth_shim.py +188 -0
  145. package/tests/test_codex_retry_process.py +143 -0
  146. package/tests/test_codex_speed.py +236 -0
  147. package/tests/test_mutate_lock.py +107 -0
  148. package/tests/test_shim_path.py +44 -4
  149. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  150. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  151. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  152. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  153. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  154. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  155. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  156. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  157. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  158. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  159. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  160. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  161. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  162. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  163. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  164. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  165. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  166. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  167. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  168. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  169. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  170. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  171. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  172. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  173. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  174. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  175. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  176. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  177. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  178. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  179. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  180. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  181. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  182. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  183. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  184. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  185. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  186. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  187. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  188. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  189. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  190. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  191. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  192. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  193. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  194. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  195. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  196. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  197. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  198. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  199. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  200. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  201. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  202. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  203. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  204. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  205. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  206. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  207. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  208. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  209. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  210. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  211. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  212. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  213. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  214. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  215. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  216. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  217. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  218. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  219. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  220. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  221. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  222. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  223. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  224. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  225. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  226. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  227. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  228. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  229. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  230. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  231. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  232. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  233. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  234. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  235. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  236. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  237. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  238. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  239. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  240. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  241. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  242. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  243. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  244. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
@@ -51,6 +51,27 @@ def initialize_rollout():
51
51
  return rollout
52
52
 
53
53
 
54
+ def require_resume_cwd():
55
+ if os.environ.get("FAKE_REQUIRE_RESUME_CWD") != "1" or "resume" not in sys.argv:
56
+ return
57
+ path = Path(os.environ["CODEX_HOME"]) / "sessions/2026/10/10"
58
+ path /= "rollout-test-" + os.environ["FAKE_SID"] + ".jsonl"
59
+ saved = None
60
+ for line in path.read_text().splitlines():
61
+ row = json.loads(line)
62
+ payload = row.get("payload", {})
63
+ if row.get("type") in ("session_meta", "turn_context"):
64
+ saved = payload.get("cwd", saved)
65
+ elif row.get("type") == "event_msg" and payload.get("type") == "thread_settings_applied":
66
+ saved = payload.get("thread_settings", {}).get("cwd", saved)
67
+ matches = saved is None or os.path.realpath(saved) == os.getcwd()
68
+ record("resume-cwd", saved=saved, matches=matches)
69
+ if not matches:
70
+ print("Working directory · resume\n› 1. Use session directory\n 2. Use current directory", flush=True)
71
+ while True:
72
+ time.sleep(0.1)
73
+
74
+
54
75
  def require_resume_trust():
55
76
  if os.environ.get("FAKE_REQUIRE_TRUST") != "1" or "resume" not in sys.argv:
56
77
  return
@@ -67,7 +88,7 @@ def require_resume_trust():
67
88
  table = re.escape(json.dumps(os.getcwd()))
68
89
  trusted = re.search(r'^\s*\[projects\.' + table + r'\]\s*\n(?:(?!\s*\[).*\n)*?\s*trust_level\s*=\s*"trusted"',
69
90
  text, re.M) is not None
70
- record("trust", trusted=trusted)
91
+ record("trust", trusted=trusted, cwd=os.getcwd())
71
92
  if not trusted:
72
93
  print("Folder access\nTrust this folder?\n› 1. Trust and continue\n 2. Quit", flush=True)
73
94
  while True:
@@ -85,6 +106,7 @@ def main():
85
106
  settings = termios.tcgetattr(0)
86
107
  signal.signal(signal.SIGTERM, stop)
87
108
  signal.signal(signal.SIGHUP, stop)
109
+ require_resume_cwd()
88
110
  require_resume_trust()
89
111
  rollout = initialize_rollout()
90
112
  try:
@@ -2632,16 +2632,27 @@ regB="$(claude-accounts list 2>&1 | grep par2@test | awk '{print $1}')"
2632
2632
  [ -n "$regB" ] && claude-accounts remove "$regB" --yes >/dev/null 2>&1
2633
2633
 
2634
2634
  # ---- 15h. two parallel adds signing into the SAME email: exactly one wins ------------
2635
+ if python3 "$REPO_DIR/tests/test_mutate_lock.py"; then
2636
+ t_ok "mutation lock: missing-stat race preserves the replacement owner"
2637
+ else
2638
+ t_fail "mutation lock ownership suite" "see unittest output above"
2639
+ fi
2635
2640
  ( CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=same@test claude-accounts add >"$WORK/s1.out" 2>&1 ) &
2636
2641
  sA=$!
2637
2642
  ( CLAUDE_MULTIACC_FORCE_TTY=1 FAKE_EMAIL=same@test claude-accounts add >"$WORK/s2.out" 2>&1 ) &
2638
2643
  sB=$!
2639
- wait "$sA"; wait "$sB"
2644
+ sArc=0; sBrc=0
2645
+ wait "$sA" || sArc=$?
2646
+ wait "$sB" || sBrc=$?
2647
+ both="$(cat "$WORK/s1.out" "$WORK/s2.out" 2>/dev/null)"
2648
+ [ "$sArc:$sBrc" = "0:0" ] && t_ok "same-email parallel add: both callers succeed" \
2649
+ || t_fail "same-email parallel add exit status" "first=$sArc second=$sBrc; $both"
2640
2650
  n="$(claude-accounts list 2>&1 | grep -c same@test)"
2641
2651
  [ "$n" = "1" ] && t_ok "same-email parallel add: exactly one registered (other skipped)" \
2642
2652
  || t_fail "same-email parallel add" "count=$n (expected 1)"
2643
- cat "$WORK/s1.out" "$WORK/s2.out" 2>/dev/null | grep -q "already added as\|skipping" \
2644
- && t_ok "same-email parallel add: loser skipped gracefully" || t_fail "same-email skip msg" "no skip message"
2653
+ grep -qE "already added as|skipping" "$WORK/s1.out" "$WORK/s2.out" \
2654
+ && t_ok "same-email parallel add: loser skipped gracefully" \
2655
+ || t_fail "same-email skip msg" "first=$sArc second=$sBrc; no skip message in: $both"
2645
2656
  regS="$(claude-accounts list 2>&1 | grep same@test | awk '{print $1}')"
2646
2657
  [ -n "$regS" ] && claude-accounts remove "$regS" --yes >/dev/null 2>&1
2647
2658
 
@@ -7797,18 +7808,23 @@ fi
7797
7808
 
7798
7809
  # two parallel imports get distinct ids and both land in the manifest (lock coverage;
7799
7810
  # codex-review finding)
7800
- codex-accounts import p1@cx --no-sync >/dev/null 2>&1 &
7811
+ codex-accounts import p1@cx --no-sync >"$WORK/cx-import1.out" 2>&1 &
7801
7812
  imp1=$!
7802
- codex-accounts import p2@cx --no-sync >/dev/null 2>&1 &
7813
+ codex-accounts import p2@cx --no-sync >"$WORK/cx-import2.out" 2>&1 &
7803
7814
  imp2=$!
7804
- wait "$imp1"; wait "$imp2"
7815
+ imp1rc=0; imp2rc=0
7816
+ wait "$imp1" || imp1rc=$?
7817
+ wait "$imp2" || imp2rc=$?
7818
+ both="$(cat "$WORK/cx-import1.out" "$WORK/cx-import2.out" 2>/dev/null)"
7819
+ [ "$imp1rc:$imp2rc" = "0:0" ] && t_ok "codex: both parallel import callers succeed" \
7820
+ || t_fail "codex parallel import exit status" "first=$imp1rc second=$imp2rc; $both"
7805
7821
  n1="$(grep -c '"email": "p1@cx"' "$CX/accounts.json")"
7806
7822
  n2="$(grep -c '"email": "p2@cx"' "$CX/accounts.json")"
7807
7823
  id1="$(python3 -c "import json,sys; print(next((a['id'] for a in json.load(open('$CX/accounts.json'))['accounts'] if a['email']=='p1@cx'), ''))")"
7808
7824
  id2="$(python3 -c "import json,sys; print(next((a['id'] for a in json.load(open('$CX/accounts.json'))['accounts'] if a['email']=='p2@cx'), ''))")"
7809
7825
  { [ "$n1" = "1" ] && [ "$n2" = "1" ] && [ -n "$id1" ] && [ -n "$id2" ] && [ "$id1" != "$id2" ]; } \
7810
7826
  && t_ok "codex: parallel imports get distinct ids ($id1, $id2)" \
7811
- || t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2)"
7827
+ || t_fail "codex parallel import" "p1=$n1($id1) p2=$n2($id2); first=$imp1rc second=$imp2rc; $both"
7812
7828
  codex-accounts remove "$id1" --yes >/dev/null 2>&1
7813
7829
  codex-accounts remove "$id2" --yes >/dev/null 2>&1
7814
7830
 
@@ -0,0 +1,71 @@
1
+ """Account rotation restores the bound Codex session's last working directory."""
2
+
3
+ import unittest
4
+
5
+ import test_autoresume as fixtures
6
+
7
+
8
+ class CodexResumeDirectoryTests(unittest.TestCase):
9
+ def setUp(self):
10
+ self.pool = fixtures.CodexWatcherTests(methodName="runTest")
11
+ self.pool.setUp()
12
+ self.addCleanup(self.pool.tearDown)
13
+ self.saved = self.pool.tmp / "saved project ü"
14
+ self.saved.mkdir()
15
+
16
+ def restart(self, records, expected, setup=None):
17
+ pool = self.pool
18
+ records.append(fixtures.cx_error(pool.t0, "usage_limit_exceeded"))
19
+ result, _, runner, _ = pool.run_codex(["resume", fixtures.CX_SID], records, setup=setup)
20
+ self.assertEqual(result, 0)
21
+ fields, argv = pool.relaunch_of(runner)
22
+ self.assertEqual(fields["cwd"], str(expected))
23
+ self.assertEqual(runner.probes[0]["cwd"], str(expected))
24
+ self.assertEqual(argv[-2], fixtures.CX_SID)
25
+ self.assertEqual(pool.events(), ["watch", "detect", "switch"])
26
+
27
+ def test_recorded_session_directory_wins_over_launch_directory(self):
28
+ self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved))], self.saved)
29
+
30
+ def test_latest_turn_directory_wins_over_initial_session_directory(self):
31
+ self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.pool.work)),
32
+ {"type": "turn_context", "payload": {"cwd": str(self.saved)}}], self.saved)
33
+
34
+ def test_latest_own_thread_settings_win_without_a_new_turn(self):
35
+ settings = {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID,
36
+ "thread_settings": {"cwd": str(self.saved)}}
37
+ self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.pool.work)),
38
+ {"type": "turn_context", "payload": {"cwd": str(self.pool.work)}},
39
+ fixtures.cx_event(self.pool.t0 - 1, settings)], self.saved)
40
+
41
+ def test_foreign_thread_settings_and_tool_workdirs_do_not_change_directory(self):
42
+ foreign = {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID2,
43
+ "thread_settings": {"cwd": str(self.pool.work)}}
44
+ tool = {"type": "response_item", "payload": {"type": "function_call", "name": "exec_command",
45
+ "arguments": '{"workdir":"/tmp","cmd":"pwd"}'}}
46
+ self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved)),
47
+ fixtures.cx_event(self.pool.t0 - 1, foreign), tool], self.saved)
48
+
49
+ def test_invalid_saved_values_keep_last_valid_directory(self):
50
+ invalid = [None, 7, {}, "relative/project", "/tmp\nnot-a-directory", "/tmp\0bad"]
51
+ records = [fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved))]
52
+ records.extend({"type": "turn_context", "payload": {"cwd": value}} for value in invalid)
53
+ self.restart(records, self.saved)
54
+
55
+ def test_deleted_saved_directory_falls_back_to_existing_launch_directory(self):
56
+ self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.saved / "deleted"))],
57
+ self.pool.work)
58
+
59
+ def test_relaunch_cursor_does_not_hide_the_saved_directory(self):
60
+ def setup(watcher, runner, path):
61
+ info = path.stat()
62
+ watcher.st["rollout_cursor"] = "%d:%d" % (info.st_ino, info.st_size)
63
+ runner.at(self.pool.t0 + 1, lambda: fixtures.append(path, [
64
+ fixtures.cx_error(self.pool.t0 + 1, "usage_limit_exceeded")]))
65
+
66
+ self.restart([fixtures.cx_meta(self.pool.launched - 100, cwd=str(self.pool.work)),
67
+ {"type": "turn_context", "payload": {"cwd": str(self.saved)}}], self.saved, setup=setup)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()
@@ -0,0 +1,92 @@
1
+ """A Codex handoff binds the saved directory and history to one validated snapshot."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import tempfile
6
+ import unittest
7
+ from unittest import mock
8
+
9
+ import test_autoresume as fixtures
10
+ from autoresume_cross_unit_support import scenario
11
+ import autoresume_cross as cross
12
+ import autoresume_handoff as handoff
13
+
14
+
15
+ class CodexDirectoryHandoffTests(unittest.TestCase):
16
+ def setUp(self):
17
+ temporary = tempfile.TemporaryDirectory(prefix="ar-cwd-handoff-")
18
+ self.addCleanup(temporary.cleanup)
19
+ self.home = Path(temporary.name).resolve()
20
+ self.initial = self.home / "initial"
21
+ self.saved = self.home / "saved project"
22
+ self.initial.mkdir()
23
+ self.saved.mkdir()
24
+ self.path = self.home / ("rollout-test-" + fixtures.CX_SID + ".jsonl")
25
+ self.meta = fixtures.cx_meta(1000, cwd=str(self.initial))
26
+
27
+ def write(self, records):
28
+ self.path.write_text("".join(json.dumps(row) + "\n" for row in [self.meta] + records))
29
+
30
+ def validate(self, cwd=None):
31
+ return handoff.validate_source("codex", fixtures.CX_SID, str(self.path), str(cwd or self.saved))
32
+
33
+ def test_changed_directory_from_turn_or_settings_replaces_initial_directory(self):
34
+ turn = {"type": "turn_context", "payload": {"cwd": str(self.saved)}}
35
+ settings = fixtures.cx_event(1001, {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID,
36
+ "thread_settings": {"cwd": str(self.saved)}})
37
+ for record in (turn, settings):
38
+ with self.subTest(kind=record["type"]):
39
+ self.write([record])
40
+ history = self.validate()
41
+ self.assertEqual(history["cwd"], str(self.saved))
42
+ self.assertEqual(history["offset"], self.path.stat().st_size)
43
+ self.assertIsNone(self.validate(self.initial))
44
+
45
+ def test_foreign_settings_cannot_authorize_unrelated_directory(self):
46
+ settings = fixtures.cx_event(1001, {"type": "thread_settings_applied", "thread_id": fixtures.CX_SID2,
47
+ "thread_settings": {"cwd": str(self.saved)}})
48
+ self.write([settings])
49
+ self.assertIsNone(self.validate())
50
+ self.assertIsNotNone(self.validate(self.initial))
51
+
52
+ def test_saved_directory_after_large_history_is_read_beyond_initial_prefix(self):
53
+ self.write([{"type": "response_item", "payload": {"text": "x" * handoff.MAX_SOURCE_BYTES}},
54
+ {"type": "turn_context", "payload": {"cwd": str(self.saved)}}])
55
+ self.assertEqual(self.validate()["cwd"], str(self.saved))
56
+
57
+ def test_appended_settings_after_snapshot_do_not_change_snapshot_directory(self):
58
+ self.write([{ "type": "turn_context", "payload": {"cwd": str(self.saved)}}])
59
+ offset = self.path.stat().st_size
60
+ original = handoff.codex_resume_cwd
61
+ later = {"type": "turn_context", "payload": {"cwd": str(self.initial)}}
62
+ with mock.patch.object(handoff, "codex_resume_cwd", side_effect=lambda *args, **kwargs: (
63
+ fixtures.append(self.path, [later]), original(*args, **kwargs))[1]):
64
+ history = self.validate()
65
+ self.assertEqual((history["cwd"], history["offset"]), (str(self.saved), offset))
66
+ self.assertIsNone(self.validate())
67
+
68
+ def test_replacement_during_directory_read_is_rejected(self):
69
+ self.write([{"type": "turn_context", "payload": {"cwd": str(self.saved)}}])
70
+ replacement = self.path.with_suffix(".replacement")
71
+ replacement.write_bytes(self.path.read_bytes())
72
+ original = handoff.codex_resume_cwd
73
+ with mock.patch.object(handoff, "codex_resume_cwd", side_effect=lambda *args, **kwargs: (
74
+ replacement.replace(self.path), original(*args, **kwargs))[1]):
75
+ self.assertIsNone(self.validate())
76
+
77
+ def test_cross_provider_plan_probes_in_latest_saved_directory(self):
78
+ case, watcher, runner, history, _ = scenario(self, "codex")
79
+ saved = case.tmp / "saved project"
80
+ saved.mkdir()
81
+ fixtures.append(history, [{"type": "turn_context", "payload": {"cwd": str(saved)}}])
82
+ plan = cross.plan_fallback(watcher, watcher.tracker.pending, "")
83
+ self.assertIsNotNone(plan)
84
+ self.assertEqual(plan["history"]["cwd"], str(saved))
85
+ self.assertEqual(runner.target_probes[-1]["cwd"], str(saved))
86
+ self.assertTrue(cross.revalidate(watcher, plan))
87
+ fixtures.append(history, [{"type": "turn_context", "payload": {"cwd": str(case.work)}}])
88
+ self.assertFalse(cross.revalidate(watcher, plan))
89
+
90
+
91
+ if __name__ == "__main__":
92
+ unittest.main()
@@ -0,0 +1,105 @@
1
+ """Read only a bound Codex thread's saved cwd, even across chunk and restart boundaries."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+ from unittest import mock
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(ROOT / "lib"))
12
+ from codex_resume_cwd import codex_resume_cwd # noqa: E402
13
+ import session_settings # noqa: E402
14
+
15
+ SID = "01a13956-95c9-7d41-9a8b-b3e5ff79d3b2"
16
+ OTHER = "0aaaaaaa-0000-7000-8000-000000000000"
17
+
18
+
19
+ def line(kind, **payload):
20
+ return json.dumps({"type": kind, "payload": payload}, ensure_ascii=False)
21
+
22
+
23
+ class CodexCwdReaderTests(unittest.TestCase):
24
+ def setUp(self):
25
+ directory = tempfile.TemporaryDirectory(prefix="codex-cwd-reader-")
26
+ self.addCleanup(directory.cleanup)
27
+ self.path = Path(directory.name) / "rollout.jsonl"
28
+ self.meta = line("session_meta", id=SID, cwd="/initial project")
29
+
30
+ def write(self, records):
31
+ self.path.write_text("\n".join(records) + "\n", encoding="utf-8")
32
+ return self.path
33
+
34
+ def test_bound_id_is_checked_before_any_saved_context(self):
35
+ path = self.write([self.meta, line("turn_context", cwd="/latest")])
36
+ self.assertEqual(codex_resume_cwd(path, SID), "/latest")
37
+ self.assertEqual(codex_resume_cwd(path), "/latest")
38
+ self.assertIsNone(codex_resume_cwd(path, OTHER))
39
+ boundary = path.stat().st_size
40
+ with path.open("a", encoding="utf-8") as output:
41
+ output.write(line("turn_context", cwd="/after-snapshot") + "\n")
42
+ self.assertEqual(codex_resume_cwd(path, SID), "/after-snapshot")
43
+ self.assertEqual(codex_resume_cwd(path, SID, upto=boundary), "/latest")
44
+
45
+ def test_missing_or_unreadable_history_has_no_directory(self):
46
+ for path in (None, self.path, self.path.parent):
47
+ with self.subTest(path=path):
48
+ self.assertIsNone(codex_resume_cwd(path, SID))
49
+ self.write([self.meta])
50
+ with mock.patch("builtins.open", side_effect=PermissionError("denied")):
51
+ self.assertIsNone(codex_resume_cwd(self.path, SID))
52
+
53
+ def test_invalid_metadata_cannot_bind_a_later_context(self):
54
+ bad = ["{", "[]", "null", json.dumps({"type": "session_meta", "payload": []}),
55
+ line("session_meta", id=None, cwd="/initial"),
56
+ line("turn_context", id=SID, cwd="/initial")]
57
+ for first in bad:
58
+ with self.subTest(first=first):
59
+ path = self.write([first, line("turn_context", cwd="/latest")])
60
+ self.assertIsNone(codex_resume_cwd(path, SID))
61
+
62
+ def test_malformed_and_invalid_later_records_preserve_last_valid_cwd(self):
63
+ bad = ["{", '[]', '{"payload":{"cwd":',
64
+ json.dumps({"type": "turn_context", "payload": None}),
65
+ line("event_msg", type="thread_settings_applied", thread_settings=[])]
66
+ bad.extend(line("turn_context", cwd=value)
67
+ for value in (None, 7, {}, "relative", "/bad\npath", "/bad\rpath", "/bad\0path"))
68
+ path = self.write([self.meta, line("turn_context", cwd="/saved ü")] + bad)
69
+ self.assertEqual(codex_resume_cwd(path, SID), "/saved ü")
70
+
71
+ def test_foreign_settings_and_subagent_context_do_not_change_main_cwd(self):
72
+ path = self.write([
73
+ self.meta,
74
+ line("event_msg", type="thread_settings_applied", thread_id=OTHER,
75
+ thread_settings={"cwd": "/foreign"}),
76
+ line("turn_context", thread_id=OTHER, cwd="/foreign-context"),
77
+ line("turn_context", turn_id="child", root_turn_id="parent", cwd="/subagent"),
78
+ line("response_item", type="function_call", cwd="/tool", arguments='{"cwd":"/quoted"}'),
79
+ line("session_meta", id=OTHER, cwd="/copied-parent"),
80
+ ])
81
+ self.assertEqual(codex_resume_cwd(path, SID), "/initial project")
82
+
83
+ def test_reverse_chunks_reassemble_long_records_and_keep_latest_settings(self):
84
+ saved = "/saved " + "é" * 200
85
+ path = self.write([
86
+ self.meta, line("turn_context", cwd="/older"),
87
+ line("event_msg", type="thread_settings_applied", thread_id=SID,
88
+ thread_settings={"cwd": saved}),
89
+ line("response_item", type="message", text="padding" * 100),
90
+ ])
91
+ with mock.patch.object(session_settings, "CHUNK", 127):
92
+ self.assertEqual(codex_resume_cwd(path, SID), saved)
93
+
94
+ def test_initial_metadata_remains_fallback_beyond_reverse_scan_limit(self):
95
+ large_meta = line("session_meta", id=SID, cwd="/initial project", base_instructions="x" * (1 << 20))
96
+ path = self.write([large_meta, line("session_meta", id=OTHER, cwd="/copied-parent"),
97
+ line("response_item", type="message", text="padding" * 100)])
98
+ self.assertEqual(codex_resume_cwd(path, SID), "/initial project")
99
+ with mock.patch.object(session_settings, "CHUNK", 128), \
100
+ mock.patch.object(session_settings, "SCAN_LIMIT", 128):
101
+ self.assertEqual(codex_resume_cwd(path, SID), "/initial project")
102
+
103
+
104
+ if __name__ == "__main__":
105
+ unittest.main()
@@ -0,0 +1,54 @@
1
+ """A quota restart restores the saved directory before Codex checks directory and trust."""
2
+
3
+ import json
4
+ import time
5
+ import unittest
6
+
7
+ from autoresume_tmux_support import SID, TMUX, TmuxScenario
8
+
9
+ QUOTA = {"type": "task_complete", "error": {"message": "You've hit your usage limit.",
10
+ "codex_error_info": "usage_limit_exceeded"}}
11
+
12
+
13
+ @unittest.skipUnless(TMUX, "real tmux is unavailable")
14
+ class CodexCwdTmuxTests(unittest.TestCase):
15
+ def test_quota_restart_restores_latest_directory_before_trust_and_prompt(self):
16
+ self.check_restart("turn_context")
17
+
18
+ def test_settings_change_restores_directory_before_the_next_turn(self):
19
+ self.check_restart("thread_settings_applied")
20
+
21
+ def check_restart(self, record_type):
22
+ case = TmuxScenario(FAKE_REQUIRE_RESUME_CWD="1", FAKE_REQUIRE_TRUST="1")
23
+ self.addCleanup(case.close)
24
+ saved = case.home / "saved project Ω"
25
+ saved.mkdir()
26
+ source = case.pool / "acct-01/config.toml"
27
+ target = case.pool / "acct-02/config.toml"
28
+ source.write_text("# The original account stays unchanged.\n")
29
+ case.start()
30
+ context = {"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
31
+ "type": "turn_context", "payload": {"cwd": str(saved)}}
32
+ if record_type == "thread_settings_applied":
33
+ context.update(type="event_msg", payload={"type": record_type, "thread_id": SID,
34
+ "thread_settings": {"cwd": str(saved)}})
35
+ with case.rollout.open("a") as stream:
36
+ stream.write(json.dumps(context) + "\n")
37
+ case.append(QUOTA)
38
+ case.wait(lambda: "autoresume switch" in case.log.read_text(), "restart in saved directory confirmed")
39
+ first, second = case.records("launch")
40
+ self.assertEqual([first["cwd"], second["cwd"]], [str(case.work), str(saved)])
41
+ self.assertEqual([first["account"], second["account"]], ["acct-01", "acct-02"])
42
+ self.assertIn(SID, second["argv"])
43
+ self.assertEqual([row["matches"] for row in case.records("resume-cwd")], [True])
44
+ self.assertEqual([(row["trusted"], row["cwd"]) for row in case.records("trust")], [(True, str(saved))])
45
+ self.assertIn('[projects.' + json.dumps(str(saved), ensure_ascii=False) + ']', target.read_text())
46
+ self.assertNotIn(str(case.work), target.read_text())
47
+ self.assertEqual(source.read_text(), "# The original account stays unchanged.\n")
48
+ self.assertEqual(len(case.records("prompt")), 1)
49
+ self.assertEqual(case.log.read_text().count("autoresume switch"), 1)
50
+ self.assertEqual(case.records("keys"), [], "neither TUI receives a keystroke")
51
+
52
+
53
+ if __name__ == "__main__":
54
+ unittest.main()