Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
eeee8ec
Project captured display identities through registry rows
trissim Oct 29, 2026
d1fdcfc
Acquire relationship projections through captured rows and input capa…
trissim Oct 29, 2026
b8c1418
Keep birth projection and retained rows on their wire owners
trissim Oct 29, 2026
b6ee567
Retain group source inputs before acquiring complete relationship views
trissim Oct 29, 2026
6a8a18d
Require the already supplied person at relationship entry construction
trissim Oct 29, 2026
d9b4c5e
Acquire selected lookup threads through the captured row owner
trissim Oct 29, 2026
ae91096
Repair nominal acquisition interfaces and real fixture preconditions
trissim Oct 29, 2026
9b105df
Serialize retained thread rows through their existing raw owner
trissim Oct 29, 2026
0524dd6
Let ThreadIncarnation strictly resolve its original current peer
trissim Oct 29, 2026
544dc19
Defer captured goal fields and carry lookup metadata across revisions
trissim Oct 30, 2026
e66d9b7
Join channel activity to captured tag declarations
trissim Oct 30, 2026
e46b1e1
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
d66befe
Delete the stale ThreadRows.lookup_names left by the #3632 merge
trissim Oct 30, 2026
b5d760a
Keep channel membership and capability refusal on their declarations
trissim Oct 30, 2026
4eb1f00
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
3c295bc
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
0b8893d
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
e1ee783
Preserve scoped copy records and mutate actual producer bodies
trissim Oct 30, 2026
fa34aa0
Integrate current main with captured thread membership
trissim Oct 30, 2026
1f3a4c3
Hold one display selection and bus cut across history pages
trissim Oct 30, 2026
556868b
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
9ef2162
Keep captured reader retirement inside its page-use owner
trissim Oct 30, 2026
b09c25b
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
0575f6a
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
44bbc97
Give each captured thread row its own lazy decoded value
trissim Oct 30, 2026
043ef56
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
b41818b
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
069aadc
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
6302e33
Own written thread rows through their decoded override
trissim Oct 30, 2026
5b4af2c
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
4c600de
Use declared JSON and incarnation values at row and release boundaries
trissim Oct 30, 2026
73bf866
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
c1bd982
Give capability joins one acquisition and refusal owner
trissim Oct 30, 2026
d8f0595
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
4cfd1e2
Keep history pages on their original archive catalogue
trissim Oct 30, 2026
f8ee6dc
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
ff5e9d0
Keep display construction and captured reader policy with their owners
trissim Oct 30, 2026
12012e1
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
8d45e9e
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
a6eb302
Merge remote-tracking branch 'origin/main' into work/frame-baseline--…
trissim Oct 30, 2026
e90e4b5
Keep the history traversal in integrated_page; the catalogue check wr…
trissim Oct 30, 2026
40963e0
Page history tests through one reader, so main's history_page test ru…
trissim Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/agent_comms/attempt_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def require_session_exit(self, session_files: tuple[Path, ...]) -> None:
if not self.issued_for(self._store):
raise RecoveryBlocked("native exit observation requires acquired owner loss")
session_dir = (
self._store.session.path.parent / "native-sessions" / self.release.incarnation().lookup
self._store.session.path.parent / "native-sessions" / self.release.incarnation.lookup
)
proc = Path("/proc")
if not proc.is_dir():
Expand Down
74 changes: 56 additions & 18 deletions src/agent_comms/bus.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Append-only database history and capability-declared record queries."""

from __future__ import annotations
Expand Down Expand Up @@ -49,6 +49,7 @@
ResourceClaim,
)
from agent_comms.errors import CommittedMessageError, RelationViolationError, UnclaimedError
from agent_comms.coordination_errors import CoordinationReadUnavailable
from agent_comms.field_codec import ClaimId, FieldCodec, MessageId, ThreadLookup
from agent_comms.locked_store import BusFile, FileStore, SQLiteReader, StoreCommittedError
from agent_comms.message_reference import MessageReference
Expand Down Expand Up @@ -130,13 +131,18 @@
def advanced(self, source: BusReader, selection: DisplayUnreadSelection) -> DisplayUnread:
queries = selection.queries()
through = source.revision()
if through == self.through and tuple(self.counted) == queries:
return self
earlier = {query.counting: query for query in self.counted}
kept = {query: query for query in queries if query in self.counted}
moved = {query: (earlier[query.counting], parts) for query in queries
if query not in kept and query.counting in earlier
and (parts := query.moved(earlier[query.counting]))}
reusable = {query: earlier[query.counting] for query in queries
if query.counting in earlier
and query.same_membership(earlier[query.counting])}
kept = {query: previous for query, previous in reusable.items()
if query == previous}
if through == self.through and len(kept) == len(queries):
if tuple(self.counted) == queries:
return self
return DisplayUnread(through, {query: self.counted[kept[query]] for query in queries})
moved = {query: (previous, parts) for query, previous in reusable.items()
if query not in kept and (parts := query.moved(previous))}
progressed = (*kept, *(previous for previous, _ in moved.values()))
appended = source.count_unread(
progressed if through != self.through else (), through=through, after=self.through)
Expand Down Expand Up @@ -174,6 +180,10 @@
with self.observing() as db:
yield BusSnapshot.from_connection(db)

@abstractmethod
def captured_reading(self) -> AbstractContextManager[BusSnapshot]:
"""Retain a cut independently of ordinary reads on the acquiring thread."""

def revision(self) -> int:
with self.observing() as db:
return BusRow.last_seq(db)
Expand All @@ -186,10 +196,10 @@
with self.observing() as db:
return BusActivityIndex.aggregate(db, predicate, parameters)

def participant_activity(self, names: frozenset[str]) -> ChannelActivity:
marks = ','.join('?' for _ in names)
def participant_activity(self, predicate: str,
parameters: tuple[object, ...]) -> ChannelActivity:
with self.observing() as db:
return BusDisplayIndex.aggregate(db, f'participant IN ({marks})', tuple(names))
return BusDisplayIndex.aggregate(db, predicate, parameters)

def claim_projection(self) -> ClaimProjection:
with self.observing() as db:
Expand Down Expand Up @@ -253,9 +263,24 @@

def page(self, before: int, limit: int) -> tuple[BusRecord, ...]:
with self.observing() as db:
return BusCapability.at_sequences(db, tuple(row.seq for row in BusRow.select(
db, where="{seq} < ?", parameters=(before,),
order_by=("seq",), descending=True, limit=limit)))
return BusCapability.at_sequences(
db, tuple(row.seq for row in self._page_rows(db, before, limit)),
)

def input_page(self, before: int, limit: int) -> tuple[MessageInput, ...]:
"""The same global sequence window, without attachment/report/answer acquisition."""
with self.observing() as db:
return tuple(
MessageInput.read_from(db, row.seq)
for row in self._page_rows(db, before, limit)
)

@staticmethod
def _page_rows(db: sqlite3.Connection, before: int, limit: int) -> list[BusRow]:
return BusRow.select(
db, where="{seq} < ?", parameters=(before,),
order_by=("seq",), descending=True, limit=limit,
)

def message_history(self) -> Iterator[Message]:
"""Public bulk export, paged within one committed SQLite snapshot."""
Expand Down Expand Up @@ -288,22 +313,22 @@
upper: int | None = None, descending: bool = False, limit: int = 100,
kinds: tuple[type[MessageKind], ...] = (),
) -> tuple[Message, ...]:
"""A captured display scope narrows the addressed table before decoding."""
"""A captured display scope reads public message capabilities, not delivery payloads."""
with self.observing() as db:
upper = BusRow.last_seq(db) + 1 if upper is None else upper
order = "DESC" if descending else "ASC"
members = ""
if kinds:
members = MessageInput.render(' AND {type} IN (') + ','.join('?' for _ in kinds) + ')'
rows = db.execute(
return tuple(Message.selected(
db,
f'SELECT seq FROM "{Addressed.table.declared_name}" '
f'JOIN "{MessageInput.table.declared_name}" USING(seq) '
f'WHERE ({predicate}) AND seq > ? AND seq < ?{members} '
f'ORDER BY seq {order} LIMIT ?',
(*parameters, lower, upper, *(kind.declared_name for kind in kinds), limit),
)
headers = (BusRow.require(db, seq=FieldCodec.decode(int, row[0])) for row in rows)
return BusCapability.at_sequences(db, tuple(header.seq for header in headers))
descending=descending,
))

def latest_conversations(
self, predicate: str, parameters: tuple[object, ...], through: int,
Expand Down Expand Up @@ -469,7 +494,8 @@
counts[query] = FieldCodec.decode(int, db.execute(
f'WITH {ReadCursor.sql("?")}, requests AS ('
f'SELECT \'viewer\' AS actor,* FROM "{Addressed.table.declared_name}" '
f'JOIN "{MessageInput.table.declared_name}" USING(seq) WHERE seq > ? AND seq <= ? '
f'JOIN "{MessageInput.table.declared_name}" USING(seq) '
f'WHERE seq > ? AND seq <= ? '
f'AND ({query.predicate}) AND sender NOT IN (SELECT value FROM json_each(?))), '
f'{ReadCursor.joined_sql} SELECT count(*) FROM cursors WHERE {ReadCursor.unread_sql}',
(CursorRow.binding(query.cursors), max(after, query.lower_bound), through,
Expand Down Expand Up @@ -498,6 +524,14 @@
with self.reader_snapshot() as db:
yield db

@contextmanager
def captured_reading(self) -> Iterator[BusSnapshot]:
# Unlike a nested ordinary read, a presentation outlives this worker's
# next request. Its transaction must never enter the thread-local pool.
with CoordinationReadUnavailable.checking():
with closing(self.read_snapshot(self.path, check_same_thread=False)) as db:
yield BusSnapshot.from_connection(db)


class Bus(FileStore, StoredBusReader, ReconciledFile, WalStore, CarriedWriter, BusFile, Watched):
"""One SQLite transaction owns the sequence and all of a record's fields."""
Expand Down Expand Up @@ -655,3 +689,7 @@
@contextmanager
def reading(self) -> Iterator[BusSnapshot]:
yield self

@contextmanager
def captured_reading(self) -> Iterator[BusSnapshot]:
yield self
10 changes: 3 additions & 7 deletions src/agent_comms/channel_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from .repo import Checkout
from .response_policy import DirectPolicy, MentionedPolicy
from .thread_identity import ThreadIncarnation
from .thread_name import ThreadName
from .typed_table import sql_literal
from .wake_policy import BoundedTriageWake, FullWake, MessageAudience

Expand Down Expand Up @@ -419,9 +420,7 @@ def bindings[C: CliCommand](
def validate(self, message: Message) -> None:
if message.sender == self.name and message.type.conversation_message:
raise RelationViolationError(f"Thread {message.sender!r} cannot message itself.")
allowed = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_")
if not set(self.name) <= allowed:
raise ValueError(f"Message target {self.name!r} is not a thread name or #channel.")
ThreadName.require(self.name)

def wake_policy(self, recipient: FrozenRecipient, mentions: frozenset[str]) -> FullWake:
return FullWake()
Expand Down Expand Up @@ -483,10 +482,7 @@ def __post_init__(self) -> None:
object.__setattr__(self, "name", self.incarnation.name)

def selected_name(self, snapshot: RegistryProvenance) -> str:
if not self.incarnation.current(snapshot):
raise UnregisteredThreadError(
f"Original DM peer {self.name!r} is no longer registered.")
return super().selected_name(snapshot)
return self.incarnation.require_current(snapshot).name

def require_publication(self, message: Message, snapshot: RegistryProvenance,
require_channel: Callable[[str], Channel]) -> None:
Expand Down
12 changes: 6 additions & 6 deletions src/agent_comms/check_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from .launch_runtime import ThreadLaunch
from .native_entries import ImportedSessionWriter, NativeEntry
from .native_session_files import CheckSession
from .read_basis import MemberActivityDisplayScope
from .read_basis import NamedActivityDisplayScope
from .registry_document import RosterVisibility
from .session_file import RegistrySessionLocation
from .thread_storage import StorageRoots
Expand Down Expand Up @@ -94,11 +94,11 @@ def _populate(self, copied: Comms) -> None:
targets = frozenset(
target for name in names for target in catalog.targets_for(snapshot.require(name).tags)
)
scope = MemberActivityDisplayScope("#check", targets, participant_names=selected | aliases)
scope = NamedActivityDisplayScope("#check", targets, participants=selected | aliases)
with self.source.bus.bus.reading() as source:
messages = scope.history(source, limit=self.max_records)
history = scope.history(source, limit=self.max_records)
copied.registry.store.prepare_native_root()
Bus.install(Bus.path_in(copied.root), history=messages)
Bus.install(Bus.path_in(copied.root), history=history)
copied.channels.catalog.replace(catalog)
declarations = {}
for name in names:
Expand Down Expand Up @@ -131,7 +131,7 @@ def _populate(self, copied: Comms) -> None:
edit.document.advance_claim(name)
edit.document.owners.advance(name)
edit.commit()
if messages:
if history:
# Imported history is displayable, not fresh input for this root.
# Position acceptance itself; human read markers own a different fact.
privacy = copied.registry.store.privacy()
Expand All @@ -141,7 +141,7 @@ def _populate(self, copied: Comms) -> None:
AcceptanceFrontier(
wire_root_id=privacy.namespace,
recipient_lookup=thread.incarnation.require_recorded().lookup,
through_seq=messages[-1].seq,
through_seq=history[-1].seq,
).advance(db)

def _session(self, original: Thread, root: Path, slot: str, project: Path) -> str:
Expand Down
6 changes: 3 additions & 3 deletions src/agent_comms/display_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ class DisplayReferences(RootedReferences):
@staticmethod
def recorded(root, source) -> DisplayReferences:
registry = source.provenance
targets = {name: HistoricalDisplayReference(str(root), thread.incarnation, source.key)
for name, thread in registry.named_members()}
targets = {name: HistoricalDisplayReference(str(root), incarnation, source.key)
for name, incarnation in registry.named_incarnations()}
return DisplayReferences(str(root), targets)

def for_source(self, source) -> DisplayReferences:
Expand Down Expand Up @@ -209,7 +209,7 @@ class CurrentDisplayReferences(SnapshotRestored, DisplayReferences):

@classmethod
def from_registry(cls, root: str, registry: RegistrySnapshot, channels: tuple[str, ...] = ()) -> Self:
return cls(root, {name: thread.incarnation for name, thread in registry.named_members()}, channels)
return cls(root, dict(registry.named_incarnations()), channels)

def __post_init__(self) -> None:
"""The target and lookup views of the one stored member map."""
Expand Down
Loading
Loading