claude-multiacc 2.0.56 → 2.0.57
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/README.md +7 -2
- package/bin/claude +71 -8
- package/bin/codex +67 -14
- package/docs/AUTORESUME-HANDOFF.md +82 -0
- package/docs/AUTORESUME.md +16 -5
- 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_python.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_registry.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__/shim_path.cpython-312.pyc +0 -0
- package/lib/autoresume.py +58 -14
- package/lib/autoresume_cross.py +143 -0
- package/lib/autoresume_cross_dispatch.py +114 -0
- package/lib/autoresume_cross_receipt.py +138 -0
- package/lib/autoresume_handoff.py +191 -0
- package/lib/autoresume_probe.py +36 -0
- package/package.json +1 -1
- 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_codex_records.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_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_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_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_tmux.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_tmux_modes.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_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_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/tests/autoresume_cross_receipt_support.py +58 -0
- package/tests/autoresume_cross_tmux_environment.py +59 -0
- package/tests/autoresume_cross_tmux_support.py +112 -0
- package/tests/autoresume_cross_unit_support.py +92 -0
- package/tests/fixtures/cross_provider_cli.py +121 -0
- package/tests/test_autoresume.py +3 -1
- package/tests/test_autoresume_cross_claude_holders.py +81 -0
- package/tests/test_autoresume_cross_guards.py +133 -0
- package/tests/test_autoresume_cross_holders.py +89 -0
- package/tests/test_autoresume_cross_policy.py +123 -0
- package/tests/test_autoresume_cross_receipt.py +141 -0
- package/tests/test_autoresume_cross_tmux.py +140 -0
- package/tests/test_autoresume_cross_tmux_races.py +45 -0
- package/tests/test_autoresume_cross_tokens.py +102 -0
- package/tests/test_autoresume_handoff_argv.py +127 -0
- package/tests/test_autoresume_handoff_history.py +115 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Cross-provider history must identify one real main conversation before any restart."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
|
|
10
|
+
import test_autoresume as fixtures
|
|
11
|
+
import autoresume_handoff as handoff
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HandoffHistoryTests(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
temporary = tempfile.TemporaryDirectory(prefix="ar-handoff-history-")
|
|
17
|
+
self.addCleanup(temporary.cleanup)
|
|
18
|
+
self.home = Path(temporary.name).resolve()
|
|
19
|
+
self.cwd = self.home / "project with 'quotes'"
|
|
20
|
+
self.cwd.mkdir()
|
|
21
|
+
|
|
22
|
+
def source(self, provider, records=None):
|
|
23
|
+
sid = fixtures.SID if provider == "claude" else fixtures.CX_SID
|
|
24
|
+
directory = self.home / provider / ("projects/project" if provider == "claude" else "sessions/2026/10/11")
|
|
25
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
filename = sid + ".jsonl" if provider == "claude" else "rollout-test-" + sid + ".jsonl"
|
|
27
|
+
path = directory / filename
|
|
28
|
+
if records is None:
|
|
29
|
+
records = [fixtures.cc_user(1000, "Source task content.", cwd=str(self.cwd))] if provider == "claude" else [
|
|
30
|
+
fixtures.cx_meta(1000, cwd=str(self.cwd)),
|
|
31
|
+
fixtures.cx_event(1001, {"type": "user_message", "message": "Source task content."})]
|
|
32
|
+
path.write_text("".join(json.dumps(record) + "\n" for record in records))
|
|
33
|
+
return sid, path
|
|
34
|
+
|
|
35
|
+
def test_both_main_history_formats_return_exact_file_identity(self):
|
|
36
|
+
for provider in ("claude", "codex"):
|
|
37
|
+
with self.subTest(provider=provider):
|
|
38
|
+
sid, path = self.source(provider)
|
|
39
|
+
result = handoff.validate_source(provider, sid, str(path), str(self.cwd))
|
|
40
|
+
self.assertIsNotNone(result)
|
|
41
|
+
self.assertEqual((result["provider"], result["sid"], result["cwd"]),
|
|
42
|
+
(provider, sid, str(self.cwd)))
|
|
43
|
+
self.assertEqual((result["path"], result["inode"], result["offset"]),
|
|
44
|
+
(str(path.resolve()), path.stat().st_ino, path.stat().st_size))
|
|
45
|
+
|
|
46
|
+
def test_wrong_session_and_replaced_inode_are_rejected(self):
|
|
47
|
+
for provider in ("claude", "codex"):
|
|
48
|
+
with self.subTest(provider=provider):
|
|
49
|
+
sid, path = self.source(provider)
|
|
50
|
+
self.assertIsNone(handoff.validate_source(provider, fixtures.SID2, str(path), str(self.cwd)))
|
|
51
|
+
inode = path.stat().st_ino
|
|
52
|
+
replacement = path.with_suffix(".replacement")
|
|
53
|
+
replacement.write_bytes(path.read_bytes())
|
|
54
|
+
replacement.replace(path)
|
|
55
|
+
self.assertNotEqual(path.stat().st_ino, inode)
|
|
56
|
+
self.assertIsNone(handoff.validate_source(provider, sid, str(path), str(self.cwd), inode=inode))
|
|
57
|
+
|
|
58
|
+
def test_codex_subagent_and_noninteractive_histories_are_rejected(self):
|
|
59
|
+
original = fixtures.cx_meta(1000, cwd=str(self.cwd))
|
|
60
|
+
alterations = ({"session_id": fixtures.CX_SID2}, {"source": {"subagent": "spawn"}},
|
|
61
|
+
{"originator": "codex_exec"}, {"thread_source": "subagent"})
|
|
62
|
+
for change in alterations:
|
|
63
|
+
with self.subTest(change=change):
|
|
64
|
+
record = copy.deepcopy(original)
|
|
65
|
+
record["payload"].update(change)
|
|
66
|
+
sid, path = self.source("codex", [record])
|
|
67
|
+
self.assertIsNone(handoff.validate_source("codex", sid, str(path), str(self.cwd)))
|
|
68
|
+
|
|
69
|
+
def test_claude_sidechain_only_and_foreign_conversation_are_rejected(self):
|
|
70
|
+
records = [fixtures.cc_user(1000, "Subagent task.", cwd=str(self.cwd), isSidechain=True),
|
|
71
|
+
fixtures.cc_user(1000, "Other conversation.", cwd=str(self.cwd), sessionId=fixtures.SID2)]
|
|
72
|
+
for record in records:
|
|
73
|
+
with self.subTest(sidechain=record["isSidechain"]):
|
|
74
|
+
sid, path = self.source("claude", [record])
|
|
75
|
+
self.assertIsNone(handoff.validate_source("claude", sid, str(path), str(self.cwd)))
|
|
76
|
+
|
|
77
|
+
def test_missing_directory_empty_and_malformed_histories_are_rejected(self):
|
|
78
|
+
for provider in ("claude", "codex"):
|
|
79
|
+
sid, path = self.source(provider)
|
|
80
|
+
fifo = path.parent / "not-a-history-fifo"
|
|
81
|
+
os.mkfifo(fifo)
|
|
82
|
+
for candidate in (path.parent / "absent.jsonl", path.parent, fifo):
|
|
83
|
+
with self.subTest(provider=provider, candidate=candidate.name):
|
|
84
|
+
self.assertIsNone(handoff.validate_source(provider, sid, str(candidate), str(self.cwd)))
|
|
85
|
+
for text in ("", "not json\n", "[]\n", '{"type":"session_meta","payload":null}\n'):
|
|
86
|
+
with self.subTest(provider=provider, malformed=text):
|
|
87
|
+
path.write_text(text)
|
|
88
|
+
self.assertIsNone(handoff.validate_source(provider, sid, str(path), str(self.cwd)))
|
|
89
|
+
|
|
90
|
+
def test_shared_history_symlink_resolves_to_the_exact_source(self):
|
|
91
|
+
for provider in ("claude", "codex"):
|
|
92
|
+
with self.subTest(provider=provider):
|
|
93
|
+
sid, path = self.source(provider)
|
|
94
|
+
link = self.home / (provider + "-account-history.jsonl")
|
|
95
|
+
link.symlink_to(path)
|
|
96
|
+
result = handoff.validate_source(provider, sid, str(link), str(self.cwd))
|
|
97
|
+
self.assertIsNotNone(result)
|
|
98
|
+
self.assertEqual(result["path"], str(path.resolve()))
|
|
99
|
+
|
|
100
|
+
def test_invalid_provider_id_and_missing_working_directory_are_rejected(self):
|
|
101
|
+
sid, path = self.source("claude")
|
|
102
|
+
for provider, target_sid, cwd in (("other", sid, str(self.cwd)), ("claude", "--last", str(self.cwd)),
|
|
103
|
+
("claude", sid, str(self.home / "absent"))):
|
|
104
|
+
with self.subTest(provider=provider, sid=target_sid, cwd=cwd):
|
|
105
|
+
self.assertIsNone(handoff.validate_source(provider, target_sid, str(path), cwd))
|
|
106
|
+
|
|
107
|
+
def test_claude_leading_progress_does_not_hide_valid_main_history(self):
|
|
108
|
+
records = [{"type": "progress", "data": {"type": "hook_progress"}},
|
|
109
|
+
fixtures.cc_user(1000, "Main task.", cwd=str(self.cwd))]
|
|
110
|
+
sid, path = self.source("claude", records)
|
|
111
|
+
self.assertIsNotNone(handoff.validate_source("claude", sid, str(path), str(self.cwd)))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if __name__ == "__main__":
|
|
115
|
+
unittest.main()
|