Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
fd576f0
P401: discover session owners through a screen-free scope
trissim Oct 21, 2026
84478dc
P401: inherit native ancestry and select the private test root
trissim Oct 21, 2026
5b5ce7b
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
5bb6d9d
P401: model unavailable origins and let views return sidebar focus
trissim Oct 21, 2026
cc6067a
P401: exercise sidebar return focus and closed-origin behavior
trissim Oct 21, 2026
af941ed
P401: inherit the UI-free navigation owner across session scopes
trissim Oct 21, 2026
13c6ba5
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
42202b4
P401: verify navigation in the real sidebar and type its workspace owner
trissim Oct 21, 2026
045bd1d
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
c4ef839
P401: carry typed destinations and ask the cursor owner about menus
trissim Oct 21, 2026
4b99950
P401: let transcript scopes own every reader and absent operation
trissim Oct 21, 2026
c2c74d1
P401: match launches only through actual native session origins
trissim Oct 21, 2026
643ca9e
P401: check live launch matching through the existing agent binding
trissim Oct 21, 2026
df7fc97
P401: format the touched session ownership family
trissim Oct 21, 2026
4218fc8
P401: derive native application contracts and pin merged ancestor sup…
trissim Oct 21, 2026
c4903ec
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
476a74b
Own history registration, native launch state and foreign read outcomes
trissim Oct 21, 2026
610e42f
Exercise real foreign stored reads alongside deferred status preserva…
trissim Oct 21, 2026
3c8a4f9
Use the current Comms constructor in the foreign read journey
trissim Oct 21, 2026
dde8f90
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
adf4060
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
3d474ab
Construct native binding members and own routed target registration
trissim Oct 21, 2026
bda0bcf
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
77a224f
Keep delivery settlement out of the prompt draft
trissim Oct 21, 2026
6de7cf6
Consume the declared missing thread presentation through the MRO
trissim Oct 21, 2026
baeb40c
Track paired package mains and include the presentation owner
trissim Oct 21, 2026
dee358a
Own compose controls and remove the false other-route read
trissim Oct 21, 2026
3b7d795
Use Core other-route status and its leaf error owner
trissim Oct 21, 2026
2bde9cb
Follow the declared route error and resolved Core source
trissim Oct 21, 2026
7382992
Own agent startup selection once on the application binding
trissim Oct 21, 2026
a1e3038
Require the startup binding at every application entrypoint
trissim Oct 21, 2026
590d66f
Integrate current main and preserve the incarnation migration checkpoint
trissim Oct 21, 2026
cea1258
Keep DM incarnation identity and derive current view targets from Cor…
trissim Oct 21, 2026
dddccab
Use the existing leaf projection for reference-backed tab labels
trissim Oct 21, 2026
550f9e5
Cover visible DM replacement and incarnation-specific view retirement
trissim Oct 21, 2026
8ddf7af
Pair session ownership with the constructor-restored Core snapshot
trissim Oct 21, 2026
72effea
Declare ACP route errors at their leaf import boundary
trissim Oct 21, 2026
a4fbde4
Merge current Toad main and preserve the paired incarnation migration
trissim Oct 21, 2026
4f1bac4
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 21, 2026
726c662
Retain activity rows by incarnation and publish current reference labels
trissim Oct 21, 2026
8878cdc
Check renamed-tab paint through the existing UI frame driver
trissim Oct 21, 2026
43e78f4
Merge current main and check settled renamed-DM render ownership
trissim Oct 21, 2026
d6a2c3b
Use the current Core goal execution family after main integration
trissim Oct 21, 2026
a5bf65c
Keep conversation attachment contracts on AgentConnection
trissim Oct 21, 2026
1fbde86
Read the bound agent in the slow thread-open journey
trissim Oct 21, 2026
9bf0993
Pin the current P434 owner for paired app qualification
trissim Oct 21, 2026
3826c78
Integrate native helpers through the existing connection owners
trissim Oct 21, 2026
a32f8cf
Use declared route and send owners in the private UI fixtures
trissim Oct 21, 2026
1a4e4c5
Remove retired participants test and observe current notifications
trissim Oct 21, 2026
4fe7738
Check channel notification lifetime at its subscription owner
trissim Oct 21, 2026
7bd56f7
Let session state and composed controls own their decisions
trissim Oct 22, 2026
77b2b89
Delete the unused selection accessor and finish P401 checks
trissim Oct 22, 2026
8da6f2b
Place mounted session checks in the frontend test layer
trissim Oct 22, 2026
09b75e0
Merge remote-tracking branch 'origin/main' into fix/p401-session-scope
trissim Oct 22, 2026
c27ccdb
Exercise route startup and historical notifications through real views
trissim Oct 22, 2026
a751354
Merge main into P401 session scopes and remove stale qualification paths
trissim Oct 23, 2026
5e71f7a
Ask session scopes to match launches without filtering view classes
trissim Oct 23, 2026
3e13954
Merge origin/main into P401 session scope
trissim Oct 23, 2026
733005d
Annotate P401 session scope parameters and fail loud on agent read er…
trissim Oct 23, 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
6 changes: 3 additions & 3 deletions src/toad/acp/agent_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from dataclasses import dataclass, replace
from collections.abc import Callable
from agent_comms.route_selection import RouteSelection
from agent_comms.thread_presentation import SessionDisconnected
from agent_comms.thread_presentation import SessionDisconnected, ThreadPresentation
import toad
from toad import constants
from acp.exceptions import RequestError
Expand Down Expand Up @@ -317,14 +317,14 @@ async def run(self) -> None:
self.agent.events.publish(AgentReady(session_id=self.agent.session_id, reconnected=self.reconnecting))


async def observe_owner(self, presentation) -> None:
async def observe_owner(self, presentation: ThreadPresentation) -> None:
"""Load again when Core shows a newer owner process than the one this session loaded.

A restarted owner answers through the still-open connection, but the queue
and cursor views are bound to the process named in the load response and
refuse the new one's updates until a new load binds them.
"""
if not self.settled.is_set() or self.reconnecting or presentation is None:
if not self.settled.is_set() or self.reconnecting:
return
if self.agent.coordination is None:
return
Expand Down
75 changes: 50 additions & 25 deletions src/toad/acp/comms_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any

from agent_comms.acp_extension import (
Expand All @@ -27,7 +28,7 @@
from agent_comms.context_reading import UnmeasuredContext
from agent_comms.errors import RouteChangedError
from agent_comms.mro_dispatch import MroDispatch, handles
from agent_comms.thread_presentation import ThreadPresentation
from agent_comms.thread_presentation import NoThreadPresentation, ThreadPresentation

from toad.acp.client_session import ClientSessionRequest
from toad.acp.context_measurement import ContextMeasurement
Expand All @@ -54,7 +55,9 @@ async def consume_metadata(self, metadata) -> None:
from .sdk_boundary import DecodeCommsMetadataTask

authority = ClientSessionRequest(self.agent, self.session_id)
facts = await self.agent.controller.task_runner.run(DecodeCommsMetadataTask(metadata))
facts = await self.agent.controller.task_runner.run(
DecodeCommsMetadataTask(metadata)
)
authority.require()
for fact in facts:
self.dispatch_sync(fact)
Expand All @@ -64,7 +67,7 @@ def accepts_turn(self):

def require_compaction_update(self):
if self.compaction_update is None:
raise ValueError('Compaction result was missing')
raise ValueError("Compaction result was missing")
return self.compaction_update

@handles(TextRouteUpdate)
Expand All @@ -73,7 +76,11 @@ def text_route(self, update: TextRouteUpdate) -> None:

@handles(InputFailedUpdate)
def input_failed(self, update: InputFailedUpdate) -> None:
self.agent.events.publish(core_events.CommsUpdated(update, recover_draft=False, session_id=self.session_id))
self.agent.events.publish(
core_events.CommsUpdated(
update, recover_draft=False, session_id=self.session_id
)
)

@handles(TranscriptChangedUpdate)
def transcript_changed(self, update: TranscriptChangedUpdate) -> None:
Expand All @@ -87,7 +94,11 @@ def turn_changed(self, update: TurnChangedUpdate) -> None:
binding = agent.presentation.managed_turns()
if not binding.receive(update.state):
return
agent.events.publish(core_events.CommsUpdated(update, session_id=self.session_id, sequence=binding.sequence))
agent.events.publish(
core_events.CommsUpdated(
update, session_id=self.session_id, sequence=binding.sequence
)
)

@handles(CursorAdvancedUpdate)
def cursor_advanced(self, update: CursorAdvancedUpdate) -> None:
Expand All @@ -114,15 +125,15 @@ def queue_changed(self, update: QueueChangedUpdate) -> None:
def input_started(self, update: InputStartedUpdate) -> None:
if update.scope.unidentified_start(update.input_id):
if self.session_id == self.agent.session_id:
self.agent.events.publish(core_events.CommsUpdated(update, session_id=self.session_id))
self.agent.events.publish(
core_events.CommsUpdated(update, session_id=self.session_id)
)
elif update.scope.matches_session(self.session_id):
starts = self.agent.queue_attachment.started(update, self.session_id)
self.agent._post_queue_view(starts)

@handles(CoordinationChangedUpdate)
def coordination_changed(self, update: CoordinationChangedUpdate) -> None:
from pathlib import Path

agent = self.agent
root = agent.controller.transcripts.access.launch_root.path
if Path(update.wire_root).resolve() != root.resolve():
Expand All @@ -131,7 +142,9 @@ def coordination_changed(self, update: CoordinationChangedUpdate) -> None:
agent.project_root_path = Path(update.worktree)
agent.context_measurement = ContextMeasurement.saved(update.context_usage)
agent.update_status_line()
agent.events.publish(core_events.CommsUpdated(agent.coordination, self.session_id))
agent.events.publish(
core_events.CommsUpdated(agent.coordination, self.session_id)
)
agent.session.coordinated_title(update.title)

@handles(GoalChangedUpdate)
Expand All @@ -151,12 +164,16 @@ def compaction_committed(self, update: CompactionCommittedUpdate) -> None:
@handles(TranscriptSnapshotUpdate)
def transcript_snapshot(self, update: TranscriptSnapshotUpdate) -> None:
controller = self.agent.controller
controller.start_operation(controller.publish_transcript_snapshot(
update, controller.surface, controller.session))
controller.start_operation(
controller.publish_transcript_snapshot(
update, controller.surface, controller.session
)
)

@handles(InputDeliveryChangedUpdate)
def input_delivery_changed(self, update: InputDeliveryChangedUpdate) -> None:
from toad.core.events import InputDispositionsChanged

self.agent.events.publish(InputDispositionsChanged())

@handles(McpClientAcknowledgementUpdate)
Expand Down Expand Up @@ -197,11 +214,16 @@ def __init__(self, agent, session_id, *, turn_token) -> None:
self.turn_token = turn_token

def accepts_turn(self):
return (super().accepts_turn()
and self.agent.controller.accepts_turn_snapshot(self.turn_token))
return super().accepts_turn() and self.agent.controller.accepts_turn_snapshot(
self.turn_token
)

@handles(NoThreadPresentation)
def missing_thread_presentation(self, presentation: NoThreadPresentation) -> object:
return self.DISCARD

@handles(ThreadPresentation)
def thread_presentation(self, presentation) -> None:
def thread_presentation(self, presentation: ThreadPresentation) -> None:
identity = presentation.read_identity
if identity is None:
return # An unavailable source supplies no turn settlement.
Expand All @@ -211,9 +233,10 @@ def publish_thread_turn(self, root, thread):
"""Only the live transcript member can supply actual owner settlement."""
coordination = self.agent.coordination
self.agent.controller.require_owner(
coordination, ClientSessionRequest(self.agent, self.session_id))
if (root != coordination.wire_root or thread.incarnation != coordination.thread):
raise ValueError('Turn snapshot belongs to another original thread source')
coordination, ClientSessionRequest(self.agent, self.session_id)
)
if root != coordination.wire_root or thread.incarnation != coordination.thread:
raise ValueError("Turn snapshot belongs to another original thread source")
self.turn_changed(TurnChangedUpdate(thread.turn_state))


Expand All @@ -234,7 +257,7 @@ class ChangedAttachmentRead(ThreadReadOutcome):

@dataclass(frozen=True)
class SettledThreadRead(ThreadReadOutcome):
presentation: ThreadPresentation | None
presentation: ThreadPresentation

def present(self, consumer) -> None:
consumer(self.presentation)
Expand All @@ -243,7 +266,7 @@ def present(self, consumer) -> None:
class ThreadRead:
"""A presentation read, with settlement owned by its captured source."""

def settle(self, presentation: ThreadPresentation | None) -> ThreadReadOutcome:
def settle(self, presentation: ThreadPresentation) -> ThreadReadOutcome:
return SettledThreadRead(presentation)

async def reconcile(self, transcripts) -> None:
Expand All @@ -268,18 +291,20 @@ class PendingThreadRead(ThreadRead):
authority: ClientSessionRequest
snapshot: OwnerSnapshotConsumer

def settle(self, presentation: ThreadPresentation | None) -> ThreadReadOutcome:
def settle(self, presentation: ThreadPresentation) -> ThreadReadOutcome:
current = self.agent.coordination
if current is None or (current.wire_root, current.thread) != (
self.coordination.wire_root, self.coordination.thread
self.coordination.wire_root,
self.coordination.thread,
):
return ChangedAttachmentRead()
if self.authority.retired:
return EndedSessionRead()
if presentation is not None:
self.snapshot.dispatch_sync(presentation)
self.snapshot.dispatch_sync(presentation)
return SettledThreadRead(presentation)

async def reconcile(self, transcripts) -> None:
self.settle(await transcripts.thread_presentation(
self.coordination.wire_root, self.coordination.thread.name))
read = await transcripts.thread_presentation(
self.coordination.wire_root, self.coordination.thread.name
)
self.settle(read.raised())
86 changes: 65 additions & 21 deletions src/toad/acp/transcript_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,22 @@
from __future__ import annotations

import asyncio
from functools import partial

from agent_comms.route_selection import RouteSelection
from agent_comms.acp_extension import TranscriptDraftUpdate, TranscriptSnapshotUpdate
from agent_comms.route_selection import RouteSelection
from agent_comms.thread_presentation import ThreadPresentation
from agent_comms.ui_model.observation import (
AwaitedRead, ReadTranscript, ReadTranscriptUpdate, RefreshTranscript,
AwaitedRead,
RefreshTranscript,
StoredRead,
TranscriptPublicationState,
UnreadableHistory,
)

from toad.owner_preparation import read_thread_presentation
from toad.session_scope import TranscriptScope


class CoordinationTranscriptReader:
"""The application's route-owned reads, shared by agents from construction."""
Expand All @@ -29,39 +37,75 @@ def __init__(self, access):
self.access = access

async def read(self, request: AwaitedRead):
observed = await self.access.preparation.run_thread(self.access.observation.observed)
observed = await self.access.preparation.run_thread(
self.access.observation.observed
)
return await observed.process.read(request)

async def service(self, root):
return await self.access.preparation.run_thread(
lambda: self.access.require(RouteSelection.capture(root)).service)

async def thread_presentation(self, root: str, thread: str):
"""Read one thread presentation through this reader's bound service."""
from toad.owner_preparation import read_thread_presentation
lambda: self.access.require(RouteSelection.capture(root)).service
)

async def thread_presentation(
self, root: str, thread: str
) -> StoredRead[ThreadPresentation]:
"""The stored-read owner classifies failures inside the read worker."""
reader = await self.service(root)
return await asyncio.to_thread(read_thread_presentation, reader, thread)
return await asyncio.to_thread(
UnreadableHistory.outcome,
partial(read_thread_presentation, reader, thread),
)

async def publication(self, update: TranscriptSnapshotUpdate) -> TranscriptSnapshotUpdate:
async def publication(
self, update: TranscriptSnapshotUpdate
) -> TranscriptSnapshotUpdate:
"""The published page while its witness is current, else the current page."""
identity = update.identity
current = await self.read(RefreshTranscript(identity.root, identity))
return update if current is None else current

async def page(self, root, thread, *, before=None, after=None, through=None,
historical_source: str | None = None):
snapshot = await self.snapshot(root, thread, before=before, after=after, through=through,
historical_source=historical_source)
async def page(
self,
scope: TranscriptScope,
*,
before=None,
after=None,
through=None,
historical_source: str | None = None,
):
snapshot = await self.snapshot(
scope,
before=before,
after=after,
through=through,
historical_source=historical_source,
)
return snapshot.page

async def snapshot(self, root, thread, *, before=None, after=None, through=None,
historical_source: str | None = None) -> TranscriptSnapshotUpdate:
return await self.read(ReadTranscript(
root, thread, before=before, after=after, through=through,
historical_source=historical_source))
async def snapshot(
self,
scope: TranscriptScope,
*,
before=None,
after=None,
through=None,
historical_source: str | None = None,
) -> TranscriptSnapshotUpdate:
return await self.read(
scope.snapshot_request(
before=before,
after=after,
through=through,
historical_source=historical_source,
)
)

async def updates(
self, root: str, thread: str, previous: TranscriptPublicationState, *, after=None,
self,
scope: TranscriptScope,
previous: TranscriptPublicationState,
*,
after=None,
) -> TranscriptSnapshotUpdate | TranscriptDraftUpdate:
return await self.read(ReadTranscriptUpdate(root, thread, after=after, previous=previous))
return await self.read(scope.update_request(previous, after=after))
Loading
Loading