From f9e56ee8de90c339e7235e60e88bc3fd1578e0ac Mon Sep 17 00:00:00 2001 From: Citation fixture Date: Fri, 23 Oct 2026 09:55:41 -0400 Subject: [PATCH 1/2] Copy a root by its files' layout, and give check-root tests their own HOME HistoryArchive.files_in decoded the whole history catalogue in today's HistorySource shape only to learn where archived roots live. Every rehearsal copy (StoreBackup.copy_tree -> RootFile.owned_files) therefore refused a root still in an older shape before any carry could read it; test_carry_bus_history has been red all day on whichever predecessor field the decoder met first. Archived roots are wherever attach puts them, so archive_directory owns that layout once and files_in lists it without decoding. scratch_root derived the agent-scratch directory from StorageRule() before durable_home set the test's HOME, so it wrote under the developer's real ~/.cache/agent-scratch; once dc7557a3c made the rule read HOME when built, the registry's rule and the fixture disagreed and CheckRoot refused the root. scratch_root now requests durable_home and derives from the rule under it. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/historical_views.py | 21 ++++++++++++++++----- tests/conftest.py | 13 +++++-------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/agent_comms/historical_views.py b/src/agent_comms/historical_views.py index fc4927035d..04766e4ae0 100644 --- a/src/agent_comms/historical_views.py +++ b/src/agent_comms/historical_views.py @@ -409,13 +409,24 @@ def __init__(self, bus_path: Path): @classmethod def files_in(cls, root: Path) -> frozenset[Path]: + """Archived roots are found by where attach puts them, never by decoding the + catalogue: a copy of a root in an older shape must copy before any carry reads it.""" declared = super().files_in(root) - for source in cls(Bus.path_in(root)).sources(): - archived = Path(source.root) - if archived.is_relative_to(root): - declared |= RootFile.paths_in(archived) + for archived in cls.archives_in(root): + declared |= RootFile.paths_in(archived) return declared + @staticmethod + def archive_directory(root: Path) -> Path: + return root / "history" + + @classmethod + def archives_in(cls, root: Path) -> tuple[Path, ...]: + directory = cls.archive_directory(root) + if not directory.is_dir(): + return () + return tuple(sorted(path for path in directory.iterdir() if path.is_dir())) + def sources(self) -> tuple[HistorySource, ...]: with self.read_sources() as sources: return sources @@ -463,7 +474,7 @@ def attach(self, source_root: Path, *, source_read: HistorySource | None = None) existing = next((s for s in sources if s.original_root == str(source_root)), None) if existing is not None: return existing - parent = self.bus_path.parent / "history" + parent = self.archive_directory(self.bus_path.parent) parent.mkdir(mode=0o700, exist_ok=True) stage = Path(tempfile.mkdtemp(prefix="source-", dir=parent)) try: diff --git a/tests/conftest.py b/tests/conftest.py index 5e6dc8fbe7..b43329bc5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -239,16 +239,13 @@ def without_mutation_guards(db: sqlite3.Connection, table: type[TypedTable]) -> @pytest.fixture -def scratch_root(tmp_path_factory): +def scratch_root(durable_home): + """The test's own agent-scratch, derived from the storage rule under the test's HOME.""" from agent_comms.storage_rules import StorageRule - base = tmp_path_factory.getbasetemp() - scratch = StorageRule().scratch_base.resolve() - if not base.resolve().is_relative_to(scratch): - base = scratch / "pytest" - base.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(prefix="check-root-", dir=base) as directory: - yield Path(directory) + root = StorageRule().scratch_base / "pytest" / "check-root" + root.mkdir(parents=True) + return root @pytest.fixture From f3479cc575023b15d4539bfcd9e90b2aac65f549 Mon Sep 17 00:00:00 2001 From: Citation fixture Date: Fri, 23 Oct 2026 09:58:58 -0400 Subject: [PATCH 2/2] Resolve archived stores with their own owners so companions stay with them With the copy no longer refusing predecessor roots, the carry rehearsal met the red behind it: an archived bus.sqlite3's -wal and -shm were reported as changed files no step names. Archived files were keyed to HistoryArchive in owned_files but never became primaries, so their store never claimed its companions. RootFile.archived_roots declares the whole roots a declaration keeps; owned_files takes primaries from primaries_in, which recurses through them. Carry walks (declarations_in, existing_in) still see only the top root, so no carry rewrites an archive. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/historical_views.py | 10 +++++----- src/agent_comms/locked_store.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/agent_comms/historical_views.py b/src/agent_comms/historical_views.py index 04766e4ae0..cf07533b34 100644 --- a/src/agent_comms/historical_views.py +++ b/src/agent_comms/historical_views.py @@ -412,7 +412,7 @@ def files_in(cls, root: Path) -> frozenset[Path]: """Archived roots are found by where attach puts them, never by decoding the catalogue: a copy of a root in an older shape must copy before any carry reads it.""" declared = super().files_in(root) - for archived in cls.archives_in(root): + for archived in cls.archived_roots(root): declared |= RootFile.paths_in(archived) return declared @@ -421,11 +421,11 @@ def archive_directory(root: Path) -> Path: return root / "history" @classmethod - def archives_in(cls, root: Path) -> tuple[Path, ...]: - directory = cls.archive_directory(root) - if not directory.is_dir(): + def archived_roots(cls, directory: Path) -> tuple[Path, ...]: + archives = cls.archive_directory(directory) + if not archives.is_dir(): return () - return tuple(sorted(path for path in directory.iterdir() if path.is_dir())) + return tuple(sorted(path for path in archives.iterdir() if path.is_dir())) def sources(self) -> tuple[HistorySource, ...]: with self.read_sources() as sources: diff --git a/src/agent_comms/locked_store.py b/src/agent_comms/locked_store.py index eba890b9b7..d6076ac4af 100644 --- a/src/agent_comms/locked_store.py +++ b/src/agent_comms/locked_store.py @@ -162,6 +162,21 @@ def existing_in(cls, root: Path) -> dict[Path, type[Self]]: return {path: owner for owner, directory in cls.declarations_in(root) for path in owner.matching_in(directory)} + @classmethod + def archived_roots(cls, directory: Path) -> tuple[Path, ...]: + """Whole bus roots this declaration keeps as immutable copies under ``directory``.""" + return () + + @classmethod + def primaries_in(cls, root: Path) -> dict[Path, type[Self]]: + """Existing declared files under ``root`` and every root archived in it, each with + its own owner, so an archived store's companions stay with that store.""" + primaries = cls.existing_in(root) + for owner, directory in RootFile.declarations_in(root): + for archived in owner.archived_roots(directory): + primaries.update(cls.primaries_in(archived)) + return primaries + @classmethod def owned_files( cls, root: Path, paths: Collection[Path], @@ -172,7 +187,7 @@ def owned_files( undeclared existing file acquires content behavior without inventing a store declaration for it. """ - primaries = cls.existing_in(root) + primaries = cls.primaries_in(root) declarations = {member: owner for owner, directory in cls.declarations_in(root) for member in owner.files_in(directory)} declarations.update(primaries)