From bc82ef5194869b513d34b71c79008853756deee2 Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 08:51:39 -0400 Subject: [PATCH 1/3] Hash the capability classes that produce bus projections The projection digest from #2760 hashed only the record family and the table classes, so editing Message.projections or Claimed.project_into left every stored projection marked fresh. Each producer family now declares the capability whose declarations produce its rows, and the digest covers that capability's classes in the producers' MRO. ClaimOwner no longer re-encodes its fields on construction: its only builder, Acquisition.claimed, passes a validated acquisition's fields and one of its own claims. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/envelope_claim_transitions.py | 4 +--- src/agent_comms/messages.py | 11 ++++++++--- src/agent_comms/typed_table.py | 11 +++++++++-- tests/test_bus.py | 10 ++++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/agent_comms/envelope_claim_transitions.py b/src/agent_comms/envelope_claim_transitions.py index ef12b8349d..17377a0d3d 100644 --- a/src/agent_comms/envelope_claim_transitions.py +++ b/src/agent_comms/envelope_claim_transitions.py @@ -437,11 +437,9 @@ class UnclaimedResource(ResourceClaim): @dataclass(frozen=True) class ClaimOwner(AcquiringClaim, ResourceClaim): + # Built only by Acquisition.claimed from a validated acquisition and one of its own claims. resource: Annotated[str, ResourcePathText] - def __post_init__(self) -> None: - FieldCodec.require_fields(self) - def acquire( self, scope: RecordedScope, registry: RegistrySnapshot, publish: Callable[[], ClaimOwner] ) -> ClaimOwner: diff --git a/src/agent_comms/messages.py b/src/agent_comms/messages.py index e1f23cfb4b..23ea32a61a 100644 --- a/src/agent_comms/messages.py +++ b/src/agent_comms/messages.py @@ -71,11 +71,16 @@ class BusTables(TypedTable, AppendOnlyTables, SharedFileTables, DurableTables, d class BusProjectionTables(ProducedTable, SharedFileTables, DerivedTables, declared_name=None): """Reverse lookups are derived from the record, in its append transaction.""" + @classmethod + def producer_capabilities(cls) -> tuple[tuple[type[DeclaredFamily], type], ...]: + return ((BusRecord, BusCapability), (ClaimTransition, ClaimTransition)) + @classmethod def producer_classes(cls) -> tuple[type[DeclaredFamily], ...]: - return ( - *BusRecord.discover_members(agent_comms, BusCapability), - *ClaimTransition.discover_members(agent_comms, ClaimTransition), + return tuple( + member + for family, capability in cls.producer_capabilities() + for member in family.discover_members(agent_comms, capability) ) @classmethod diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index a1cb264220..d68f3dc515 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -1426,6 +1426,11 @@ class ProjectionProducerTables(SharedFileTables, DerivedTables): class ProducedRow(ABC): """Rows rebuilt when their declared producing modules change.""" + @classmethod + @abstractmethod + def producer_capabilities(cls) -> tuple[tuple[type[DeclaredFamily], type], ...]: + """Each record family and the capability whose declarations produce these rows.""" + @classmethod @abstractmethod def producer_classes(cls) -> tuple[type[DeclaredFamily], ...]: @@ -1983,12 +1988,14 @@ def _producer_digest(cls) -> str: for owner in row.__mro__ if issubclass(owner, cls) } + # The capability's classes outside the record family (Message, Claimed) hold the + # production code itself, so they belong to the digest beside the family. + capabilities = tuple(capability for _, capability in cls.producer_capabilities()) for producer in producers: - root = producer.family_root() owners.update( owner for owner in producer.__mro__ - if issubclass(owner, cls) or issubclass(owner, root) + if issubclass(owner, (cls, producer.family_root(), *capabilities)) ) source = "\0".join( f"{owner.__module__}.{owner.__qualname__}\0{getsource(owner)}" diff --git a/tests/test_bus.py b/tests/test_bus.py index 7a71031c81..1cdc58038d 100644 --- a/tests/test_bus.py +++ b/tests/test_bus.py @@ -145,6 +145,16 @@ def test_projection_digest_ignores_infrastructure_source( "for record in BusCapability.history(db):", "for record in reversed(tuple(BusCapability.history(db))):", ), + ( + "messages.py", + "yield MessageIdentitySource(self.seq, self.message_id, self.sender)", + "yield MessageIdentitySource(self.seq, self.message_id, self.target)", + ), + ( + "messages.py", + "HeldResources.apply(db, self.claim_transition, projected, holders)", + "HeldResources.apply(db, self.claim_transition, projected, holders)\n del projected", + ), ), ) def test_projection_digest_tracks_producing_source( From 62e8c258cbd4be28194c79a201cde92aa8de0390 Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 08:53:34 -0400 Subject: [PATCH 2/3] Drop the claim-table container check that 2e4c48119 removed 2e4c48119 narrowed ClaimProjection's codec check to last_seq, so ClaimProjection(1, []) now fails with AttributeError instead of MalformedRecord and test_exact_incarnation_and_generation_fence_releases went red on main. Every builder in src passes a dict; the assertion pinned a check that no longer exists, and the field comment claiming the codec checks the container is corrected. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/envelope_claim_transitions.py | 2 +- tests/test_envelope_claim_transitions.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/agent_comms/envelope_claim_transitions.py b/src/agent_comms/envelope_claim_transitions.py index 17377a0d3d..999c9d8579 100644 --- a/src/agent_comms/envelope_claim_transitions.py +++ b/src/agent_comms/envelope_claim_transitions.py @@ -463,7 +463,7 @@ class ClaimProjection(Mapping[str, ClaimOwner]): """Immutable sorted claim table plus the last processed envelope sequence.""" last_seq: Annotated[int, ProjectionSequence] = 0 - # Built from a dict, which the codec checks; held afterwards as a read-only view. + # Built from a dict by the transitions and the holder tables; held as a read-only view. _claims: dict[str, ClaimOwner] = field(default_factory=dict) def __post_init__(self) -> None: diff --git a/tests/test_envelope_claim_transitions.py b/tests/test_envelope_claim_transitions.py index 2a864b76f3..7764a28aaa 100644 --- a/tests/test_envelope_claim_transitions.py +++ b/tests/test_envelope_claim_transitions.py @@ -187,8 +187,6 @@ def test_exact_incarnation_and_generation_fence_releases(tmp_path): assert new[a].owner == "peer" and new[a].seq == 3 with pytest.raises(ClaimTransitionError): ClaimProjection(0, {a: new[a]}) - with pytest.raises(MalformedRecord): - ClaimProjection(1, []) with pytest.raises(ClaimTransitionError): transition(4, releases=(ClaimRelease(a, G1),)).project(new) From f60181092bb05b72161994c7b1eba0d748458e0a Mon Sep 17 00:00:00 2001 From: Ratchet test Date: Fri, 23 Oct 2026 09:04:01 -0400 Subject: [PATCH 3/3] Type the digest owners as the classes they are The producing capability's classes (Message, Claimed, BusCapability) are not DeclaredFamily members, so the owner set holds plain classes. Co-Authored-By: Claude Opus 5.5 --- src/agent_comms/typed_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent_comms/typed_table.py b/src/agent_comms/typed_table.py index d68f3dc515..bc0f9276a7 100644 --- a/src/agent_comms/typed_table.py +++ b/src/agent_comms/typed_table.py @@ -1982,7 +1982,7 @@ def producer_digest(cls) -> str: @cache def _producer_digest(cls) -> str: producers = cls.producer_classes() - owners: set[type[DeclaredFamily]] = { + owners: set[type] = { owner for row in (cls, *cls.members_with(cls)) for owner in row.__mro__