From 32ce5d8cdc4e66b722f423d74b2ae6710e9c4632 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 23 Oct 2026 07:46:51 -0400 Subject: [PATCH 1/7] Derive projection producer identity from its declared classes --- src/agent_comms/declared_family.py | 10 +++++++++- src/agent_comms/typed_table.py | 21 +++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/agent_comms/declared_family.py b/src/agent_comms/declared_family.py index 94536f24cc..1d07fe693d 100644 --- a/src/agent_comms/declared_family.py +++ b/src/agent_comms/declared_family.py @@ -234,9 +234,17 @@ def members_with(cls, capability: type) -> tuple[type[Self], ...]: if issubclass(member, cls) and issubclass(member, capability) ) + @classmethod + def family_root(cls) -> type[DeclaredFamily]: + """The nominal root recorded when this declaration was constructed.""" + root = cls._family_root + if root is None: + raise TypeError(f"{cls.__name__} has no declared family root") + return root + @classmethod def declared_position(cls) -> int: - root = cast(type[DeclaredFamily], cls._family_root) + root = cls.family_root() return root.members_with(root).index(cls) diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index 02ee6e697a..c5940d8cb9 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -13,7 +13,6 @@ import os import re import sqlite3 -import sys import types from abc import ABC, abstractmethod from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence @@ -1973,14 +1972,20 @@ def producer_digest(cls) -> str: @classmethod @cache def _producer_digest(cls) -> str: - producers = cls.producer_classes() - modules = { - cls.__module__, - *(row.__module__ for row in cls.members_with(cls)), - *(owner.__module__ for producer in producers - for owner in producer.__mro__ if owner is not object), + owners = { + owner + for row in (cls, *cls.members_with(cls)) + for owner in row.__mro__ + if issubclass(owner, cls) } - source = "\0".join(getsource(sys.modules[name]) for name in sorted(modules)) + for producer in cls.producer_classes(): + root = producer.family_root() + owners.update(owner for owner in producer.__mro__ + if issubclass(owner, cls) or issubclass(owner, root)) + source = "\0".join( + f"{owner.__module__}.{owner.__qualname__}\0{getsource(owner)}" + for owner in sorted(owners, key=lambda owner: (owner.__module__, owner.__qualname__)) + ) return hashlib.sha256(source.encode()).hexdigest() From fb040467d43a1c688879319f9867fccd9d0510e0 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Fri, 23 Oct 2026 07:49:42 -0400 Subject: [PATCH 2/7] Check real producer source changes in isolated packages --- src/agent_comms/typed_table.py | 3 +- tests/test_bus.py | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index c5940d8cb9..145f63a5c2 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -1972,13 +1972,14 @@ def producer_digest(cls) -> str: @classmethod @cache def _producer_digest(cls) -> str: + producers = cls.producer_classes() owners = { owner for row in (cls, *cls.members_with(cls)) for owner in row.__mro__ if issubclass(owner, cls) } - for producer in cls.producer_classes(): + for producer in producers: root = producer.family_root() owners.update(owner for owner in producer.__mro__ if issubclass(owner, cls) or issubclass(owner, root)) diff --git a/tests/test_bus.py b/tests/test_bus.py index 5b6b00644e..d7401d1526 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -1,8 +1,12 @@ import os +import shutil import signal import sqlite3 +import subprocess +import sys from contextlib import closing, contextmanager from dataclasses import replace +from pathlib import Path import pytest @@ -48,6 +52,66 @@ from bus_record_fixture import publication_processes, record_from +@pytest.fixture +def projection_source(tmp_path: Path) -> Path: + checkout = tmp_path / "producer-source" + source = checkout / "src" / "agent_comms" + original = Path(__file__).resolve().parents[1] / "src" / "agent_comms" + shutil.copytree(original, source, ignore=shutil.ignore_patterns("__pycache__", "skills")) + (source / "skills").symlink_to(original / "skills", target_is_directory=True) + subprocess.run(("git", "init", "-q", str(checkout)), check=True, capture_output=True) + subprocess.run( + ("git", "-C", str(checkout), "-c", "user.name=Digest fixture", + "-c", "user.email=digest@example.invalid", "-c", "commit.gpgsign=false", + "-c", "core.hooksPath=/dev/null", "commit", "--allow-empty", "-qm", "Source fixture"), + check=True, capture_output=True, + ) + return source + + +def projection_source_digest(source: Path) -> str: + return subprocess.run( + (sys.executable, "-I", "-c", + "import sys; sys.path.insert(0, sys.argv[1]); " + "from agent_comms.messages import BusProjectionTables; " + "print(BusProjectionTables._producer_digest())", str(source.parent)), + check=True, capture_output=True, text=True, timeout=60, + ).stdout.strip() + + +@pytest.mark.parametrize(("module", "old", "new"), ( + ("typed_table.py", "Durable files compare their stored DDL text", + "An unrelated infrastructure docstring: durable files compare their stored DDL text"), + ("field_codec.py", "class TimestampText(", + "def unrelated_projection_probe():\n return 17\n\n\nclass TimestampText("), +)) +def test_projection_digest_ignores_infrastructure_source( + projection_source: Path, module: str, old: str, new: str, +) -> None: + before = projection_source_digest(projection_source) + path = projection_source / module + original = path.read_text() + assert original.count(old) == 1 + path.write_text(original.replace(old, new)) + assert projection_source_digest(projection_source) == before + + +@pytest.mark.parametrize(("module", "old", "new"), ( + ("bus_publication.py", "yield SentSource(self.audience.sender_lookup, self.seq)", + "yield SentSource(self.audience.sender_lookup, self.seq + 1)"), + ("bus_activity_index.py", '=max("{name}",excluded.', '=min("{name}",excluded.'), +)) +def test_projection_digest_tracks_producing_source( + projection_source: Path, module: str, old: str, new: str, +) -> None: + before = projection_source_digest(projection_source) + path = projection_source / module + original = path.read_text() + assert original.count(old) == 1 + path.write_text(original.replace(old, new)) + assert projection_source_digest(projection_source) != before + + def test_bus_attachment_and_failed_write_do_not_install_a_schema(tmp_path): bus = Bus(Bus.path_in(tmp_path / "not-installed")) assert not bus.path.parent.exists() From 85b5f57d7b602a5a65fd7dce40ec191bd6a71413 Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 07:53:30 -0400 Subject: [PATCH 3/7] Cover shared projection-family behavior in source identity checks --- src/agent_comms/typed_table.py | 7 ++- tests/test_bus.py | 93 ++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index 145f63a5c2..1780e74cd6 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -1981,8 +1981,11 @@ def _producer_digest(cls) -> str: } for producer in producers: root = producer.family_root() - owners.update(owner for owner in producer.__mro__ - if issubclass(owner, cls) or issubclass(owner, root)) + owners.update( + owner + for owner in producer.__mro__ + if issubclass(owner, cls) or issubclass(owner, root) + ) source = "\0".join( f"{owner.__module__}.{owner.__qualname__}\0{getsource(owner)}" for owner in sorted(owners, key=lambda owner: (owner.__module__, owner.__qualname__)) diff --git a/tests/test_bus.py b/tests/test_bus.py index d7401d1526..7a71031c81 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -61,32 +61,67 @@ def projection_source(tmp_path: Path) -> Path: (source / "skills").symlink_to(original / "skills", target_is_directory=True) subprocess.run(("git", "init", "-q", str(checkout)), check=True, capture_output=True) subprocess.run( - ("git", "-C", str(checkout), "-c", "user.name=Digest fixture", - "-c", "user.email=digest@example.invalid", "-c", "commit.gpgsign=false", - "-c", "core.hooksPath=/dev/null", "commit", "--allow-empty", "-qm", "Source fixture"), - check=True, capture_output=True, + ( + "git", + "-C", + str(checkout), + "-c", + "user.name=Digest fixture", + "-c", + "user.email=digest@example.invalid", + "-c", + "commit.gpgsign=false", + "-c", + "core.hooksPath=/dev/null", + "commit", + "--allow-empty", + "-qm", + "Source fixture", + ), + check=True, + capture_output=True, ) return source def projection_source_digest(source: Path) -> str: return subprocess.run( - (sys.executable, "-I", "-c", - "import sys; sys.path.insert(0, sys.argv[1]); " - "from agent_comms.messages import BusProjectionTables; " - "print(BusProjectionTables._producer_digest())", str(source.parent)), - check=True, capture_output=True, text=True, timeout=60, + ( + sys.executable, + "-I", + "-c", + "import sys; sys.path.insert(0, sys.argv[1]); " + "from agent_comms.messages import BusProjectionTables; " + "print(BusProjectionTables._producer_digest())", + str(source.parent), + ), + check=True, + capture_output=True, + text=True, + timeout=60, ).stdout.strip() -@pytest.mark.parametrize(("module", "old", "new"), ( - ("typed_table.py", "Durable files compare their stored DDL text", - "An unrelated infrastructure docstring: durable files compare their stored DDL text"), - ("field_codec.py", "class TimestampText(", - "def unrelated_projection_probe():\n return 17\n\n\nclass TimestampText("), -)) +@pytest.mark.parametrize( + ("module", "old", "new"), + ( + ( + "typed_table.py", + "Durable files compare their stored DDL text", + "An unrelated infrastructure docstring: durable files compare their stored DDL text", + ), + ( + "field_codec.py", + "class TimestampText(", + "def unrelated_projection_probe():\n return 17\n\n\nclass TimestampText(", + ), + ), +) def test_projection_digest_ignores_infrastructure_source( - projection_source: Path, module: str, old: str, new: str, + projection_source: Path, + module: str, + old: str, + new: str, ) -> None: before = projection_source_digest(projection_source) path = projection_source / module @@ -96,13 +131,27 @@ def test_projection_digest_ignores_infrastructure_source( assert projection_source_digest(projection_source) == before -@pytest.mark.parametrize(("module", "old", "new"), ( - ("bus_publication.py", "yield SentSource(self.audience.sender_lookup, self.seq)", - "yield SentSource(self.audience.sender_lookup, self.seq + 1)"), - ("bus_activity_index.py", '=max("{name}",excluded.', '=min("{name}",excluded.'), -)) +@pytest.mark.parametrize( + ("module", "old", "new"), + ( + ( + "bus_publication.py", + "yield SentSource(self.audience.sender_lookup, self.seq)", + "yield SentSource(self.audience.sender_lookup, self.seq + 1)", + ), + ("bus_activity_index.py", '=max("{name}",excluded.', '=min("{name}",excluded.'), + ( + "messages.py", + "for record in BusCapability.history(db):", + "for record in reversed(tuple(BusCapability.history(db))):", + ), + ), +) def test_projection_digest_tracks_producing_source( - projection_source: Path, module: str, old: str, new: str, + projection_source: Path, + module: str, + old: str, + new: str, ) -> None: before = projection_source_digest(projection_source) path = projection_source / module From ce90a2be53d034c0b59eafc9dd1e34ae24f1db41 Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 07:57:37 -0400 Subject: [PATCH 4/7] Document the one-time producer digest format rebuild --- src/agent_comms/typed_table.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index 1780e74cd6..36af07f898 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -1960,7 +1960,11 @@ def update( class ProducedTable(ProducedRow, TypedTable, declared_name=None): - """A named table whose producing family owns one source commitment.""" + """A named table whose producing family owns one source commitment. + + The first install after this change rebuilds bus projections once because the digest + format changed. + """ @classmethod def producer_digest(cls) -> str: From 354884cacc00f9f6f24fc36931942b4e5099b78b Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 08:12:59 -0400 Subject: [PATCH 5/7] Keep family roots concrete and type mixed producer owners --- src/agent_comms/declared_family.py | 16 ++++++++-------- src/agent_comms/typed_table.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/agent_comms/declared_family.py b/src/agent_comms/declared_family.py index 1daab00c42..f20219c60a 100644 --- a/src/agent_comms/declared_family.py +++ b/src/agent_comms/declared_family.py @@ -42,7 +42,7 @@ def __new__( roots = { base._family_root for base in bases - if isinstance(base, _FamilyMeta) and base._family_root is not None + if isinstance(base, _FamilyMeta) and base is not DeclaredFamily } if len(roots) > 1: raise TypeError("A declaration cannot belong to two families.") @@ -87,7 +87,10 @@ def __new__( cls: type[DeclaredFamily] = super().__new__( mcs, name, bases, namespace, registry_config=config, **class_options ) - cls._family_root = cls if is_root else root + if is_root: + cls._family_root = cls + elif root is not None: + cls._family_root = root return cast(_FamilyMeta, cls) @staticmethod @@ -130,7 +133,7 @@ class DeclaredFamily(ABC, metaclass=_FamilyMeta): declared_name: ClassVar[str] family_discriminator: ClassVar[str] = "kind" - _family_root: ClassVar[type[DeclaredFamily] | None] = None + _family_root: ClassVar[type[DeclaredFamily]] _family_affix: ClassVar[str] __registry__: ClassVar[dict[str, type[DeclaredFamily]]] @@ -237,10 +240,7 @@ def members_with(cls, capability: type) -> tuple[type[Self], ...]: @classmethod def family_root(cls) -> type[DeclaredFamily]: """The nominal root recorded when this declaration was constructed.""" - root = cls._family_root - if root is None: - raise TypeError(f"{cls.__name__} has no declared family root") - return root + return cls._family_root @classmethod def declared_position(cls) -> int: @@ -257,7 +257,7 @@ class DeclaredWireFamily(DeclaredFamily, BaseModel, metaclass=_WireFamilyMeta): declared_name: ClassVar[str] family_discriminator: ClassVar[str] - _family_root: ClassVar[type[DeclaredFamily] | None] + _family_root: ClassVar[type[DeclaredFamily]] _family_affix: ClassVar[str] __registry__: ClassVar[dict[str, type[DeclaredFamily]]] diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index 36af07f898..a1cb264220 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -1977,7 +1977,7 @@ def producer_digest(cls) -> str: @cache def _producer_digest(cls) -> str: producers = cls.producer_classes() - owners = { + owners: set[type[DeclaredFamily]] = { owner for row in (cls, *cls.members_with(cls)) for owner in row.__mro__ From 60cabe8c9ea64be5e3af25880d409046a442b0ea Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 08:19:13 -0400 Subject: [PATCH 6/7] Remove the remaining null root probe and migrate the native fixture route --- src/agent_comms/declared_family.py | 5 +++-- stack/test-native-global-extensions.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/agent_comms/declared_family.py b/src/agent_comms/declared_family.py index 447ffbf61d..2c19507ebb 100644 --- a/src/agent_comms/declared_family.py +++ b/src/agent_comms/declared_family.py @@ -87,8 +87,9 @@ def __new__( ) if is_root: cls._family_root = cls - elif root is not None: - cls._family_root = root + else: + for family_root in roots: + cls._family_root = family_root return cast(FamilyMeta, cls) @staticmethod diff --git a/stack/test-native-global-extensions.py b/stack/test-native-global-extensions.py index e06984b4a4..a0cd541fdc 100755 --- a/stack/test-native-global-extensions.py +++ b/stack/test-native-global-extensions.py @@ -18,6 +18,7 @@ import time import tempfile +from agent_comms.comms import wire from agent_comms.field_codec import FieldCodec from agent_comms.storage_rules import StorageRoots from agent_comms.native_package import NativeImportsManifest, VerifiedNativePackage @@ -62,10 +63,9 @@ def main(fixture: Path): } environment.update(RestartEnvironment(native_config=agent, agent_directory=agent).encode_native()) from agent_comms.child_process import ProcessIdentity - from agent_comms.comms import Comms from agent_comms.threads import Thread - comms = Comms(root / "wire") + comms = wire(root / "wire") thread = Thread("native-startup-fixture", frozenset(), str(project), storage_roots=StorageRoots(root / "scratch"), process_identity=ProcessIdentity.capture(os.getpid())) From 7b755f04676c3b8e103e794fc21f619a50b4a5e1 Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 08:22:06 -0400 Subject: [PATCH 7/7] Repin native inputs after fixing the global extension fixture --- stack/pi-native.sha256 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stack/pi-native.sha256 b/stack/pi-native.sha256 index b13826a331..3e2db0c631 100644 --- a/stack/pi-native.sha256 +++ b/stack/pi-native.sha256 @@ -57,4 +57,4 @@ d64e8c05c7932d377034820b1db0df45612afe64885c0765f8f348a12e1bc955 dist/core/skil 0391543cf703a93a0163b37b3ccebe53e0f12bff6f6162b86d56fb017fe49011 dist/core/system-prompt.js # agent-comms-native-stock-v1 @earendil-works/pi-coding-agent@0.85.1 # agent-comms-native-tree-v1 17edf8badd73bafccf592b651b5a9c0f71b2b05b89db470d1042d3ccb544a0b9 -# agent-comms-native-inputs-v1 a420ec4c8414f1e9b5355eeaf303c1c1f798d47c876d5612bf2512daf445294b +# agent-comms-native-inputs-v1 e57296bd11e2b731df3c744633772054a8568de2a96dc2012623aa83a54c8cda