From 5d36e8aebb97ad6e22199939ab45cc4a48db9d91 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 18:00:17 -0500 Subject: [PATCH 1/4] Preserve painted transcript pages across saved-history appends --- src/toad/transcript_preparation.py | 3 + src/toad/transcript_source_preparation.py | 21 ++- src/toad/widgets/committed_presentation.py | 53 ++++--- src/toad/widgets/conversation.py | 6 +- .../test_transcript_presentation_fencing.py | 138 ++++++++++++++++++ 5 files changed, 198 insertions(+), 23 deletions(-) diff --git a/src/toad/transcript_preparation.py b/src/toad/transcript_preparation.py index 0aae526a0..2e0c5750e 100644 --- a/src/toad/transcript_preparation.py +++ b/src/toad/transcript_preparation.py @@ -21,6 +21,7 @@ from toad.render_backend import ReusableRenderTask from toad.transcript_state import LatestViewportRequest +from toad.widgets.committed_presentation import PreparedAppendCommit, PreparedPageCommit from toad.widgets.history_anchor import LatestPosition from toad.widgets.message_notifications import NothingToRead, PaintedRead from toad.widgets.presentation_window import OLDER_END, PageEnd, PageRequest @@ -288,6 +289,7 @@ class PreparedPageSource: runtime: PreparationRuntime scope: PreparationScope through: TranscriptCursor + commit_preparation: ClassVar[type[PreparedPageCommit]] = PreparedPageCommit def __init__(self) -> None: # This source holds the resources after their widgets detach. These are the @@ -499,6 +501,7 @@ class SavedHistory(ReplaceablePageSource): # The work that reads and prepares one page of this kind of source. work: ClassVar[type[TranscriptPageWork]] = TranscriptPageWork + commit_preparation = PreparedAppendCommit def __init__( self, loader: Callable[..., Awaitable[TranscriptPage]], through: TranscriptCursor, diff --git a/src/toad/transcript_source_preparation.py b/src/toad/transcript_source_preparation.py index 89a2c774e..d3dce7976 100644 --- a/src/toad/transcript_source_preparation.py +++ b/src/toad/transcript_source_preparation.py @@ -310,8 +310,27 @@ def drawn_end_more(self) -> bool: edge = self.pages[-1].prepared return NEWER_END.can_extend(edge) or edge.page.has_newer + async def publish_append(self, through, publication_current: Callable[[], bool]) -> bool: + """A commit may retire live blocks only after publishing its destination. + + extend also serves held readers: its True can mean access advanced + without new paint. A followed commit must instead finish a known gap + through the existing bounded destination owner before reporting success. + """ + if not await self.extend(through, publication_current): + return False + if not publication_current(): + return False + if self.drawn_end_more: + if not await self.jump_latest( + LatestViewportRequest(LatestPosition.hold(self.window)), publication_current, + ): + return False + return (publication_current() and not self.drawn_end_more + and self.pages[-1].prepared.page.after.same_position(through)) + async def extend(self, through, publication_current: Callable[[], bool] = lambda: True) -> bool: - """Append to a complete followed tail; a gap uses the bounded latest read.""" + """Extend source access, preserving held readers that have not drawn its end.""" if not publication_current(): return False self.page_source.through = through diff --git a/src/toad/widgets/committed_presentation.py b/src/toad/widgets/committed_presentation.py index 82267b020..05aa6a607 100644 --- a/src/toad/widgets/committed_presentation.py +++ b/src/toad/widgets/committed_presentation.py @@ -263,6 +263,13 @@ class PreparedPageCommit(PreparedCommit): history: TranscriptHistory page: PreparedTranscriptPage + @classmethod + async def read_snapshot[Snapshot]( + cls, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, + ) -> Snapshot: + """Replacement needs the complete destination, even for a closed prefix.""" + return await read() + @classmethod async def capture(cls, view: Conversation, history: TranscriptHistory, page: TranscriptPage, sequences: frozenset[int]) -> Self: from toad.transcript_preparation import TranscriptPageRenderTask @@ -281,21 +288,40 @@ async def draw(self, cursor, is_current: Callable[[], bool], live_blocks) -> boo if not history.source_ready or not is_current(): return False operation = history.reserve_source_work(partial(CommitTranscript, live_blocks=live_blocks)) - return await operation.execute(history, partial( - history.show_newest, + return await operation.execute(history, partial(self.publish, cursor, is_current)) + + async def publish(self, cursor: TranscriptCursor, is_current: Callable[[], bool]) -> bool: + history = self.history + return await history.show_newest( PreparedReplacementViewportRequest(LatestPosition.hold(history.window), self.page), publication_current=is_current, - )) + ) + + +class PreparedAppendCommit(PreparedPageCommit): + """The saved source extends its original pages, retaining their rendered prefix.""" + + @classmethod + async def read_snapshot[Snapshot]( + cls, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, + ) -> Snapshot: + try: + return await read(position=history.page_source.through.later_page()) + except TranscriptChanged: + return await read() + + async def publish(self, cursor: TranscriptCursor, is_current: Callable[[], bool]) -> bool: + return await self.history.publish_append(cursor, publication_current=is_current) class CommitPlan(ABC): """The captured viewport intent owns source acquisition and publication.""" - @abstractmethod async def read_snapshot[Snapshot]( self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, ) -> Snapshot: - """Acquire the saved destination this plan will publish.""" + """The source's declared commit member owns acquisition and publication.""" + return await history.page_source.commit_preparation.read_snapshot(read, history) @abstractmethod def current(self, window: HistoryWindow) -> bool: @@ -339,18 +365,12 @@ class FollowTailCommit(CommitPlan): def __init__(self, window: HistoryWindow) -> None: self.revision = window.scroll_revision - async def read_snapshot[Snapshot]( - self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, - ) -> Snapshot: - # No after cursor: Core reads newest-first at its captured saved cut. - return await read() - def current(self, window: HistoryWindow) -> bool: return window.follows_tail and window.scroll_revision == self.revision async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage, sequences: frozenset[int]) -> PreparedPageCommit: - return await PreparedPageCommit.capture(view, history, page, sequences) + return await history.page_source.commit_preparation.capture(view, history, page, sequences) def finish(self, view, cursor) -> None: view.transcript.binding.painted(view.transcript, cursor) @@ -363,15 +383,6 @@ def restore_reader(self, window) -> None: window.anchor() class RetainViewportCommit(CommitPlan): - async def read_snapshot[Snapshot]( - self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, - ) -> Snapshot: - try: - return await read(position=history.page_source.through.later_page()) - except TranscriptChanged: - # A replaced source cannot extend the old reader's source range. - return await read() - def current(self, window: HistoryWindow) -> bool: # Extending the source leaves the loaded rows and reader position intact. # Retirement checks the current viewport again when applying the change. diff --git a/src/toad/widgets/conversation.py b/src/toad/widgets/conversation.py index 91e2c5093..04f8c7299 100644 --- a/src/toad/widgets/conversation.py +++ b/src/toad/widgets/conversation.py @@ -1782,7 +1782,11 @@ async def check_prune(self) -> None: # Height pressure is not source evidence. Dropping the source # pager or an uncovered wire claim lets a later observation # recreate the same record as a new tail arrival. - if self.contents.virtual_size.height > high_mark: + # Saved pages already have a bounded preparation owner. Only + # the native tail consumes the live-block pressure allowance. + live_height = (self.contents.virtual_size.height + - self.transcript.binding.history.outer_size.height) + if live_height > high_mark: self.transcript.require_commit() else: await self.prune_window(low_mark, high_mark) diff --git a/tests/textual/test_transcript_presentation_fencing.py b/tests/textual/test_transcript_presentation_fencing.py index cbfca6add..1cdb1170e 100644 --- a/tests/textual/test_transcript_presentation_fencing.py +++ b/tests/textual/test_transcript_presentation_fencing.py @@ -154,6 +154,144 @@ async def test_a_certified_read_that_expired_acquires_no_application(workspace): assert view.transcript.binding.history.page_source.through.session_file == str(journal) +async def test_followed_saved_append_keeps_painted_page_and_fragments(workspace): + import json + from dataclasses import replace + + thread = workspace.thread("append-source") + journal = write_journal( + thread.project_path / "append.jsonl", + assistant_entry("FIRST_RETAINED_ROW", id="first"), + assistant_entry("SECOND_RETAINED_ROW", id="second", parent="first"), + assistant_entry("THIRD_RETAINED_ROW", id="third", parent="second"), + ) + workspace.comms.registry.declare(replace( + thread, session_file=RegistrySessionLocation.from_path(str(journal), workspace.comms.root))) + async with workspace.app(resume="append-source") as ui: + await ui.until_shows("THIRD_RETAINED_ROW") + view = ui.conversation + await ui.app.coordination_access.stop_observing() + await view.transcript.suspend() + history = view.transcript.binding.history + assert view.window.follows_tail + pages = tuple(history.pages) + fragments = tuple(history.fragments_in_range) + assert pages and fragments + with journal.open("a") as output: + output.write(json.dumps(assistant_entry( + "APPENDED_SAVED_ROW", id="appended", parent="third")) + "\n") + view.transcript.require_commit() + await ui.until_shows("APPENDED_SAVED_ROW") + assert view.transcript.binding.history is history + assert all(page in history.pages for page in pages) + current = tuple(history.fragments_in_range) + assert all(any(fragment is kept for kept in current) for fragment in fragments) + painted = ui.painted(view.window) + assert all(marker in painted for marker in ( + "FIRST_RETAINED_ROW", "SECOND_RETAINED_ROW", "THIRD_RETAINED_ROW")) + assert ui.failure() is None + + +async def test_followed_commit_publishes_a_known_undrawn_gap_before_settling(workspace): + import json + from dataclasses import replace + + thread = workspace.thread("gap-source") + journal = write_journal(thread.project_path / "gap.jsonl", + assistant_entry("BEFORE_SOURCE_GAP", id="before")) + workspace.comms.registry.declare(replace( + thread, session_file=RegistrySessionLocation.from_path(str(journal), workspace.comms.root))) + async with workspace.app(resume="gap-source") as ui: + await ui.until_shows("BEFORE_SOURCE_GAP") + view = ui.conversation + await ui.app.coordination_access.stop_observing() + await view.transcript.suspend() + history = view.transcript.binding.history + await ui.until(lambda: history.source_ready, why="the original source work to finish") + with journal.open("a") as output: + output.write(json.dumps(assistant_entry( + "AFTER_SOURCE_GAP", id="after", parent="before")) + "\n") + view.transcript.dirty = view.transcript.commit_required = True + entered, release = asyncio.Event(), asyncio.Event() + pending = asyncio.create_task(view.transcript.publish(HeldSourceCase, entered, release)) + try: + # A successful publication return can also mean a guarded no-op. + # Hold the existing real read only AFTER the commit was admitted. + await ui.until(entered.is_set, why="the admitted commit to acquire its real source") + page = workspace.comms.transcripts.capture_page_read("gap-source").read() + history.retain_committed(page.after) + assert view.window.follows_tail and history.drawn_end_more + finally: + release.set() + await pending + # Check the publication contract before a later lookahead can hide it. + assert not history.drawn_end_more + assert history.pages[-1].prepared.page.after.same_position(page.after) + await ui.until_shows("AFTER_SOURCE_GAP") + assert ui.failure() is None + + +async def test_closed_same_source_commit_acquires_the_complete_replacement(workspace): + import json + from dataclasses import replace + + thread = workspace.thread("closed-prefix") + journal = write_journal(thread.project_path / "closed.jsonl", + assistant_entry("CLOSED_PREFIX_MUST_SURVIVE", id="before")) + workspace.comms.registry.declare(replace( + thread, session_file=RegistrySessionLocation.from_path(str(journal), workspace.comms.root))) + async with workspace.app(resume="closed-prefix") as ui: + await ui.until_shows("CLOSED_PREFIX_MUST_SURVIVE") + view = ui.conversation + await ui.app.coordination_access.stop_observing() + await view.transcript.suspend() + history = view.transcript.binding.history + history.close() + with journal.open("a") as output: + output.write(json.dumps(assistant_entry( + "CLOSED_PREFIX_SUCCESSOR", id="after", parent="before")) + "\n") + view.transcript.dirty = view.transcript.commit_required = True + await view.transcript.publish(CommitPublication) + await ui.until_shows("CLOSED_PREFIX_SUCCESSOR") + assert "CLOSED_PREFIX_MUST_SURVIVE" in ui.painted(view.window) + assert view.transcript.binding.history is history + assert ui.failure() is None + + +@pytest.mark.parametrize("pressure", (False, True)) +async def test_prune_pressure_counts_live_blocks_not_saved_history(workspace, pressure): + from dataclasses import replace + from toad.preferences import SETTINGS + + thread = workspace.thread("pressure-source") + journal = write_journal(thread.project_path / "pressure.jsonl", + assistant_entry("SAVED_PRESSURE_ROW_ONE\n\nSAVED_PRESSURE_ROW_TWO", id="saved")) + workspace.comms.registry.declare(replace( + thread, session_file=RegistrySessionLocation.from_path(str(journal), workspace.comms.root))) + async with workspace.app(resume="pressure-source") as ui: + await ui.until_shows("SAVED_PRESSURE_ROW_TWO") + view = ui.conversation + await ui.app.coordination_access.stop_observing() + await view.post(AgentResponse("\n\n".join( + f"Actual live response paragraph {number}" for number in range(128)))) + await ui.until(lambda: view.contents.virtual_size.height + - view.transcript.binding.history.outer_size.height > 100, + why="the real live response to have its laid-out height") + await view.transcript.suspend() + history = view.transcript.binding.history + live_height = view.contents.virtual_size.height - history.outer_size.height + threshold = live_height + 1 - 2 * int(pressure) + assert threshold >= 100 and history.outer_size.height > 2 + assert view.contents.virtual_size.height > threshold + ui.app.saved_settings.set(SETTINGS.ui.prune_low_mark, threshold) + ui.app.saved_settings.set(SETTINGS.ui.prune_excess, 0) + view.transcript.dirty = view.transcript.commit_required = False + view._require_check_prune = True + await view.check_prune() + assert view.transcript.commit_required is pressure + assert ui.failure() is None + + async def test_closing_the_transcript_mid_application_paints_nothing(workspace): async with workspace.app() as ui: view = ui.conversation From df3519186511eedcf40eabbb457efc94d3b3824b Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 18:27:26 -0500 Subject: [PATCH 2/4] Keep failed append acquisition coupled to replacement publication --- src/toad/widgets/committed_presentation.py | 48 ++++++++------- .../test_transcript_presentation_fencing.py | 58 ++++++++++++++++++- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/toad/widgets/committed_presentation.py b/src/toad/widgets/committed_presentation.py index 05aa6a607..255ad6540 100644 --- a/src/toad/widgets/committed_presentation.py +++ b/src/toad/widgets/committed_presentation.py @@ -246,6 +246,12 @@ class PreparedCommit: history: CommittedHistory sequences: frozenset[int] + @classmethod + async def capture(cls, view: Conversation, history: TranscriptHistory, page: TranscriptPage, + sequences: frozenset[int]) -> Self: + """A retained reader accepts coverage without preparing replacement paint.""" + return cls(history, sequences) + @property def outgoing(self) -> frozenset[OutgoingMessageReference]: return self.history.outgoing_coverage @@ -305,23 +311,34 @@ class PreparedAppendCommit(PreparedPageCommit): async def read_snapshot[Snapshot]( cls, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, ) -> Snapshot: - try: - return await read(position=history.page_source.through.later_page()) - except TranscriptChanged: - return await read() + return await read(position=history.page_source.through.later_page()) async def publish(self, cursor: TranscriptCursor, is_current: Callable[[], bool]) -> bool: return await self.history.publish_append(cursor, publication_current=is_current) class CommitPlan(ABC): - """The captured viewport intent owns source acquisition and publication.""" + """Capture source acquisition and viewport publication together for one read.""" + + preparation: type[PreparedCommit] + + def preparation_for_source(self, source: type[PreparedPageCommit]) -> type[PreparedCommit]: + """A held viewport retains its original paint after a successful read.""" + return PreparedCommit async def read_snapshot[Snapshot]( self, read: Callable[..., Awaitable[Snapshot]], history: CommittedHistory, ) -> Snapshot: - """The source's declared commit member owns acquisition and publication.""" - return await history.page_source.commit_preparation.read_snapshot(read, history) + """The source declares acquisition; its result fixes this read's publication.""" + source = history.page_source.commit_preparation + self.preparation = self.preparation_for_source(source) + try: + return await source.read_snapshot(read, history) + except TranscriptChanged: + # A full retry is a replacement, even when the file path is reused + # and its new cursor contains the old byte offset. + self.preparation = PreparedPageCommit + return await PreparedPageCommit.read_snapshot(read, history) @abstractmethod def current(self, window: HistoryWindow) -> bool: @@ -342,14 +359,9 @@ async def prepare(self, view: Conversation, history: TranscriptHistory, sequences |= history.covered_sequences(required) | frozenset( sequence for sequence in required if page.after.covers_incoming(sequence) ) - return await self.extend(view, history, page, sequences) + return await self.preparation.capture(view, history, page, sequences) return await PreparedPageCommit.capture(view, history, page, sequences) - @abstractmethod - async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage, - sequences: frozenset[int]) -> PreparedCommit: - """Prepare publication within the history's existing source.""" - def commit(self, prepared: PreparedCommit, cursor: TranscriptCursor) -> None: """Apply source metadata only after the final retirement checks.""" @@ -368,9 +380,9 @@ def __init__(self, window: HistoryWindow) -> None: def current(self, window: HistoryWindow) -> bool: return window.follows_tail and window.scroll_revision == self.revision - async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage, - sequences: frozenset[int]) -> PreparedPageCommit: - return await history.page_source.commit_preparation.capture(view, history, page, sequences) + def preparation_for_source(self, source: type[PreparedPageCommit]) -> type[PreparedPageCommit]: + """A following viewport publishes through the same member that acquired it.""" + return source def finish(self, view, cursor) -> None: view.transcript.binding.painted(view.transcript, cursor) @@ -403,10 +415,6 @@ def permits(self, view, candidates) -> bool: return not any(widget in visible and visible[widget][0].overlaps(viewport) for widget in candidates) - async def extend(self, view: Conversation, history: TranscriptHistory, page: TranscriptPage, - sequences: frozenset[int]) -> PreparedCommit: - return PreparedCommit(history, sequences) - def commit(self, prepared, cursor) -> None: prepared.history.retain_committed(cursor) diff --git a/tests/textual/test_transcript_presentation_fencing.py b/tests/textual/test_transcript_presentation_fencing.py index 1cdb1170e..0590fcce2 100644 --- a/tests/textual/test_transcript_presentation_fencing.py +++ b/tests/textual/test_transcript_presentation_fencing.py @@ -231,7 +231,8 @@ async def test_followed_commit_publishes_a_known_undrawn_gap_before_settling(wor assert ui.failure() is None -async def test_closed_same_source_commit_acquires_the_complete_replacement(workspace): +@pytest.mark.parametrize("following", (False, True)) +async def test_closed_same_source_commit_acquires_complete_coverage(workspace, following): import json from dataclasses import replace @@ -247,17 +248,70 @@ async def test_closed_same_source_commit_acquires_the_complete_replacement(works await view.transcript.suspend() history = view.transcript.binding.history history.close() + if not following: + view.window.release_anchor() + pages = tuple(history.pages) + fragments = tuple(history.fragments_in_range) + position = view.window.reader_position with journal.open("a") as output: output.write(json.dumps(assistant_entry( "CLOSED_PREFIX_SUCCESSOR", id="after", parent="before")) + "\n") view.transcript.dirty = view.transcript.commit_required = True await view.transcript.publish(CommitPublication) - await ui.until_shows("CLOSED_PREFIX_SUCCESSOR") + if following: + await ui.until_shows("CLOSED_PREFIX_SUCCESSOR") + else: + await ui.step() + assert tuple(history.pages) == pages + assert all(actual is original for actual, original in + zip(history.fragments_in_range, fragments, strict=True)) + assert view.window.reader_position == position + assert "CLOSED_PREFIX_SUCCESSOR" not in ui.painted(view.window) assert "CLOSED_PREFIX_MUST_SURVIVE" in ui.painted(view.window) assert view.transcript.binding.history is history assert ui.failure() is None +@pytest.mark.parametrize("following", (False, True)) +async def test_rewritten_saved_file_replaces_paint_even_when_its_cursor_grew(workspace, following): + from dataclasses import replace + from agent_comms.errors import TranscriptChanged + + thread = workspace.thread("rewritten-source") + journal = write_journal(thread.project_path / "rewritten.jsonl", + assistant_entry("SUPERSEDED_SOURCE_ROW", id="old")) + workspace.comms.registry.declare(replace( + thread, session_file=RegistrySessionLocation.from_path(str(journal), workspace.comms.root))) + async with workspace.app(resume="rewritten-source") as ui: + await ui.until_shows("SUPERSEDED_SOURCE_ROW") + view = ui.conversation + await ui.app.coordination_access.stop_observing() + await view.transcript.suspend() + history = view.transcript.binding.history + original = history.page_source.through + if not following: + view.window.release_anchor() + # Reuse the path, but replace its native bytes with a longer record. + # Filename/offset containment alone cannot witness this old prefix. + write_journal(journal, assistant_entry( + "REPLACEMENT_SOURCE_ROW " * 80, id="new")) + + def read_replacement(): + read = workspace.comms.transcripts.capture_page_read("rewritten-source") + with pytest.raises(TranscriptChanged): + read.read(position=original.later_page()) + return read.read() + + replacement = await asyncio.to_thread(read_replacement) + assert replacement.after.contains(original) + view.transcript.dirty = view.transcript.commit_required = True + await view.transcript.publish(CommitPublication) + await ui.until_shows("REPLACEMENT_SOURCE_ROW") + assert "SUPERSEDED_SOURCE_ROW" not in ui.painted(view.window) + assert view.transcript.binding.history is history + assert ui.failure() is None + + @pytest.mark.parametrize("pressure", (False, True)) async def test_prune_pressure_counts_live_blocks_not_saved_history(workspace, pressure): from dataclasses import replace From 9467b17fab2f92bfe1146c337bb9d2b0af18e753 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 21:46:27 -0500 Subject: [PATCH 3/4] Establish actual saved paint before fencing transcript tests --- .../test_transcript_presentation_fencing.py | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/tests/textual/test_transcript_presentation_fencing.py b/tests/textual/test_transcript_presentation_fencing.py index 0590fcce2..0e3fd05bf 100644 --- a/tests/textual/test_transcript_presentation_fencing.py +++ b/tests/textual/test_transcript_presentation_fencing.py @@ -6,6 +6,8 @@ import pytest from support.journals import assistant_entry, write_journal +from support.ui import TextualUi +from toad.widgets.transcript_history import TranscriptHistory from agent_comms.session_file import RegistrySessionLocation from agent_comms.threads import Thread from toad.transcript_publication import CommitPublication, TranscriptPublication @@ -59,6 +61,17 @@ async def held(): release.set() +async def hold_saved_prefix(ui: TextualUi, marker: str) -> TranscriptHistory: + """Freeze actual saved paint, not an initial live replay of the same text.""" + history = ui.conversation.transcript.binding.history + await ui.until(lambda: history.source_ready and history.state.reports_coverage + and marker in ui.painted(history), + why="the saved prefix to paint before its source is held") + await ui.app.coordination_access.stop_observing() + await ui.conversation.transcript.suspend() + return history + + async def stays_waiting(task) -> bool: """Whether `task` is still unfinished after the loop had time to run it (the pump is held).""" done, _ = await asyncio.wait({task}, timeout=0.03) @@ -170,9 +183,7 @@ async def test_followed_saved_append_keeps_painted_page_and_fragments(workspace) async with workspace.app(resume="append-source") as ui: await ui.until_shows("THIRD_RETAINED_ROW") view = ui.conversation - await ui.app.coordination_access.stop_observing() - await view.transcript.suspend() - history = view.transcript.binding.history + history = await hold_saved_prefix(ui, "THIRD_RETAINED_ROW") assert view.window.follows_tail pages = tuple(history.pages) fragments = tuple(history.fragments_in_range) @@ -204,16 +215,15 @@ async def test_followed_commit_publishes_a_known_undrawn_gap_before_settling(wor async with workspace.app(resume="gap-source") as ui: await ui.until_shows("BEFORE_SOURCE_GAP") view = ui.conversation - await ui.app.coordination_access.stop_observing() - await view.transcript.suspend() - history = view.transcript.binding.history - await ui.until(lambda: history.source_ready, why="the original source work to finish") + history = await hold_saved_prefix(ui, "BEFORE_SOURCE_GAP") with journal.open("a") as output: output.write(json.dumps(assistant_entry( "AFTER_SOURCE_GAP", id="after", parent="before")) + "\n") view.transcript.dirty = view.transcript.commit_required = True entered, release = asyncio.Event(), asyncio.Event() - pending = asyncio.create_task(view.transcript.publish(HeldSourceCase, entered, release)) + case = view.transcript.binding.capture(view.transcript, HeldSourceCase, entered, release) + assert case is not None + pending = asyncio.create_task(case.publish()) try: # A successful publication return can also mean a guarded no-op. # Hold the existing real read only AFTER the commit was admitted. @@ -225,7 +235,12 @@ async def test_followed_commit_publishes_a_known_undrawn_gap_before_settling(wor release.set() await pending # Check the publication contract before a later lookahead can hide it. - assert not history.drawn_end_more + assert not history.drawn_end_more, ( + case.current(), case.plan.current(view.window), + history.source_ready, view.window.follows_tail, + view.transcript.dirty, view.transcript.commit_required, + history.pages[-1].prepared.page.after, history.page_source.through, + ) assert history.pages[-1].prepared.page.after.same_position(page.after) await ui.until_shows("AFTER_SOURCE_GAP") assert ui.failure() is None @@ -244,9 +259,7 @@ async def test_closed_same_source_commit_acquires_complete_coverage(workspace, f async with workspace.app(resume="closed-prefix") as ui: await ui.until_shows("CLOSED_PREFIX_MUST_SURVIVE") view = ui.conversation - await ui.app.coordination_access.stop_observing() - await view.transcript.suspend() - history = view.transcript.binding.history + history = await hold_saved_prefix(ui, "CLOSED_PREFIX_MUST_SURVIVE") history.close() if not following: view.window.release_anchor() @@ -275,7 +288,7 @@ async def test_closed_same_source_commit_acquires_complete_coverage(workspace, f @pytest.mark.parametrize("following", (False, True)) async def test_rewritten_saved_file_replaces_paint_even_when_its_cursor_grew(workspace, following): from dataclasses import replace - from agent_comms.errors import TranscriptChanged + from agent_comms.coordination_errors import TranscriptChanged thread = workspace.thread("rewritten-source") journal = write_journal(thread.project_path / "rewritten.jsonl", @@ -285,9 +298,7 @@ async def test_rewritten_saved_file_replaces_paint_even_when_its_cursor_grew(wor async with workspace.app(resume="rewritten-source") as ui: await ui.until_shows("SUPERSEDED_SOURCE_ROW") view = ui.conversation - await ui.app.coordination_access.stop_observing() - await view.transcript.suspend() - history = view.transcript.binding.history + history = await hold_saved_prefix(ui, "SUPERSEDED_SOURCE_ROW") original = history.page_source.through if not following: view.window.release_anchor() From 674b014390f9529043e834756a41876a0d86cfad Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 1 Nov 2026 23:10:52 -0500 Subject: [PATCH 4/4] Record incomplete installed wrapping capture --- docs/performance/scroll-series.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/performance/scroll-series.md b/docs/performance/scroll-series.md index 97a09e89c..f5a73b74e 100644 --- a/docs/performance/scroll-series.md +++ b/docs/performance/scroll-series.md @@ -257,3 +257,13 @@ Installed T124640Z before source edits: each physical notch delivers one SGR whe After: actual T134212Z dependencies plus only the Toad source overlay, same authentic P108 root/display/font. Settings search shows default1; editing to3 immediately gives3rows/notch in both windows, then1 persists on reset. At1 each viewport receives15 notches (five isolated, ten fast) and makes15 one-row native moves. Ten rapid notches cover10 rows briskly rather than coarse two-row jumps, with no animation backlog; this is not controlled latency A/B. Recording and inspected frames: `/home/ts/.cache/agent-scratch/core-p170-wheel/after/wheel.mp4`; result.json separates reader-restoration compensation. An initial Ctrl+A edit inserted31 (Home, not Select All); that real data is retained, then explicit selection corrected3→1. No provider input/replay. Original index restored byte-identically, bus0bytes; original config untouched via a separate private copy; normal close. Source **3files+19/-9, net+10**, approved Src-Budget10, ratchet0/no positive metrics; AST282 Toad/254 native modules, no omissions, dynamic binding remains qualified. Installed acceptance is still Codex-owned. +### 2026-11-01 installed wrapping capture — incomplete, no timing verdict + +| Installed inputs | Recording and sampling | Qualification | +|---|---|---| +| Core `4d3a1c083`, Toad `2f982fe7`, Textual `7c62867` | Capture05: 36-second, 60-fps recording completed (video rc0). Read-only py-spy requested 20 seconds at 100 Hz, fell over 40 seconds behind and exceeded capture_live's 50-second wall limit (profiler wrapper rc1). After the owned target exited it wrote 573 samples, 0 sample errors. | **Not a completed fixed-scenario or frame-time result.** The first image still shows Connecting; held PageUp/PageDown began before saved history was ready. The later image shows connected history. No total-frame or input-to-paint median/p95 is established, and sampling backlog prevents a representative CPU-time claim. | + +This was the installed default `toad-comms` launcher in plain st on owned Xvfb `:3`, viewing this agent's existing active session with private XDG directories. No prompt was submitted and no uncertain input was replayed. The owned frontend/display retired and the host lease released. Artifacts: `/home/ts/.cache/agent-scratch/pi-ui-performance/installed-scroll-4d3-05/` (`launch.json`, `before.png`, `after.png`, `scroll.mp4`, `profile.log`, `held-pageup-manifest.json`, `held-pageup.speedscope.json`, `result.json`, `retirement.json`); controller log beside that directory. This does not establish movement during the hold from the endpoint images alone. + +The subsequent receipt-height fix, Toad `375ec8f9` / Core `c8b2fb4d4`, was installed after this recording. It has no physical timing result in this row. `capture_live.py --frame-meter` uses `sys.remote_exec`; that running-process injection was not used. A qualifying follow-through must establish ready history and actual composer focus before gestures, retain the normal-launch instrumentation boundary, and report the receipt-height and wrapping behavior on the exact reopened build rather than transferring this failed run to it. +