Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
c440ea0
Use the original clicked widget for sidebar row menus
trissim Oct 16, 2026
d996e1a
Bind physical UI input to the recorder display and terminal identity
trissim Oct 16, 2026
9214241
Focus the verified terminal through the input owner
trissim Oct 16, 2026
6ff0ebb
Derive input display from the required frontend process
trissim Oct 16, 2026
7451cd4
Timestamp the verified physical input target
trissim Oct 16, 2026
bd50051
Measure installed sidebar drag before layout ownership repair
trissim Oct 16, 2026
f6489fa
Acknowledge live capture entry only after instrumentation is installed
trissim Oct 16, 2026
a70383f
Distinguish instrumented drag attribution from latency acceptance
trissim Oct 16, 2026
e6bcf4a
Sample sidebar width at native frame layout instead of Core events
trissim Oct 16, 2026
e4173b8
Skip layout for rejected transcript render results
trissim Oct 15, 2026
fca9564
Reject obsolete rows before opening a history change
trissim Oct 15, 2026
e757526
Record frame-sampled resize measurement without claiming improvement
trissim Oct 16, 2026
464dbd1
Let native frames finish before render batches replace row geometry
trissim Oct 16, 2026
4c6dcd9
Pin the paired InputEvent-owned retired-screen dispatch repair
trissim Oct 16, 2026
840c21a
Name display cleanup by the action it performs
trissim Oct 16, 2026
5180207
Merge remote-tracking branch 'origin/main' into perf/pipelined-history
trissim Oct 16, 2026
2f905c8
Keep lazy history comparison on the installed DM-hotfix input code
trissim Oct 16, 2026
f16b442
Merge commit '464dbd11c59f3cbcd8176051d08e8fc37669654a' into perf/pip…
trissim Oct 16, 2026
d3acb45
Limit capture sudo to read-only py-spy profiling
trissim Oct 16, 2026
6ce5d0d
Merge remote-tracking branch 'origin/main' into perf/pipelined-history
trissim Oct 16, 2026
9317c9b
Ask Core display views instead of probing a wire message page
trissim Oct 16, 2026
9905205
Pin paired Core display-view revision API
trissim Oct 16, 2026
9c7245e
Declare accepted event types on native subscriptions
trissim Oct 16, 2026
7207bc5
Declare the Core history view once on each conversation kind
trissim Oct 16, 2026
4a8355a
Pin Core view-owned revision dependencies after forwarding cleanup
trissim Oct 16, 2026
90127e6
Merge remote-tracking branch 'origin/main' into perf/pipelined-history
trissim Oct 16, 2026
2eba9e3
Merge remote-tracking branch 'origin/main' into perf/pipelined-history
trissim Oct 16, 2026
44a97cc
Delete the retained UI display identity; compare original Core pages
trissim Oct 16, 2026
94baea7
Consume the Core scoped revision and its original store observation
trissim Oct 16, 2026
091d00d
Make viewport demand own bounded transcript page reads
trissim Oct 16, 2026
d9fe6ce
Pin the checked Core scoped view revision
trissim Oct 16, 2026
2d361a5
Use Core status for Core-scoped history before ACP attachment
trissim Oct 16, 2026
c4b9949
Derive budget bounds and count reads without empty page widgets
trissim Oct 16, 2026
f9018f2
Merge remote-tracking branch 'origin/main' into fix/history-siblings
trissim Oct 16, 2026
a4ed50a
Keep Core observation custody while the first wire page awaits paint
trissim Oct 16, 2026
7dd4bf6
Give live outgoing dividers a live clock and record sibling ownership…
trissim Oct 16, 2026
3e8e751
Merge commit 'a4ed50a45' into fix/history-siblings
trissim Oct 16, 2026
4d919e2
Merge commit 'c4b99492' into fix/history-siblings
trissim Oct 16, 2026
4fd0185
Merge remote-tracking branch 'origin/main' into fix/p38-display-revision
trissim Oct 16, 2026
f52c2f6
Merge commit '4fd018579' into fix/history-siblings
trissim Oct 16, 2026
71f8cc1
Derive wire readiness from the retained window, not its tail placeholder
trissim Oct 16, 2026
52d200e
Put paging and document lifetime on LineHistory and compose painted r…
trissim Oct 16, 2026
ab1ed55
Preserve the source gap when reader movement rejects tail preparation
trissim Oct 16, 2026
e482b5c
Merge commit '71f8cc138' into fix/history-siblings
trissim Oct 16, 2026
f45a066
Capture painted read requests before scheduling work
trissim Oct 16, 2026
8ee04ef
Merge remote-tracking branch 'origin/main' into fix/viewport-page-adm…
trissim Oct 16, 2026
c21eab0
Merge commit '71f8cc138' into fix/viewport-page-admission
trissim Oct 16, 2026
1b95cae
Let measured scroll demand request its full runway
trissim Oct 16, 2026
8dce269
Merge remote-tracking branch 'origin/main' into fix/history-siblings
trissim Oct 16, 2026
a6227f0
Merge commit '1b95caea6' into fix/history-siblings
trissim Oct 16, 2026
e82a85a
Keep reconnect failure with its session and complete application cleanup
trissim Oct 16, 2026
9dbe41f
Rename the complete Toad session-load family to SessionLoadPlan
trissim Oct 16, 2026
c4029f0
Limit reconnect handling to named route and transport failures
trissim Oct 16, 2026
4365698
Merge commit 'c4029f08e' into refactor/shared-runtime-names
trissim Oct 16, 2026
1c453c8
Migrate every transcript consumer to owned native and wire cursor beh…
trissim Oct 16, 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: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Responsive scrolling and interaction in the live Toad application: about 16 ms o
- Read the source before editing: trace each fact through its declaration, storage, lifecycle and every consumer.
- When behavior is hard to explain, simplify. Remove states, layers and distinctions; do not add new ones to cover the old ones.
- Prefer deleting code to adding it. If the current design is the problem, say so and propose the replacement.
- WireHistory and TranscriptHistory keep only irreducible page-source and source-identity differences. Shared behavior belongs in LineHistory; behavior shared with other parents belongs in a capability those parents compose. Compare the complete sibling family, not only the reported symptom.
- Work moved to a thread is still waiting: this CPython build has a GIL, so a CPU-bound thread takes turns with the UI.
- The UI is a view of canonical history. Never keep a second copy of queue, turn, goal or message state in a widget.
- Report plainly: what changed, what evidence supports it, what still fails, the next step. No status ceremony; no hashes unless asked.
Expand All @@ -20,6 +21,7 @@ Responsive scrolling and interaction in the live Toad application: about 16 ms o
- Verify in the real application on Tristan's saved sessions with the configured provider. This is standing authorization. Fork sessions for new input; never replay an uncertain input.
- Performance evidence is one table: `docs/performance/scroll-series.md`, one line per change, from `tools/performance/capture_live.py` on the fixed scenario (a fork of the saved session; held PageUp from the input box; wheel bursts with reversal; an active response; End; tab return). Record total frame time and input-to-paint latency, median and p95. No per-change receipts.
- Known environment: unset `NO_COLOR` and `TOAD_VALIDATION`; run real `st` on an isolated display.
- Privileged tooling is limited to read-only `sudo -n py-spy dump/record`. Never use sudo for Python or process-code injection, including an owned frontend. Load the existing live meter at normal-user frontend startup when attribution needs instrumentation.

## Workflow

Expand Down
21 changes: 21 additions & 0 deletions docs/performance/scroll-series.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,24 @@ Normal launcher with a source overlay, plain `st` on owned :197; installed depen
- Confirmed physical native → already-open #comms → native clicks at194c61acc retained the same window, history and page range; source identity and width87 were unchanged, and **all15/15 common prepared line arrays survived**. This closes the earlier unchanged-source warm-array gap for this bounded journey. Evidence: `wire-acceptance/warm-result.json` and receipts/screenshots. No autonomous throughput claim.

AST source corpus:283 modules, zero parse omissions; original before/after owner and consumer sites under `.artifacts/held-pageup/resume10604/`, including the corrected Wire override family. Dynamic binding remains qualified, not full NRA proof. Current PR ratchet has LineHistory GodClassExcess+4 against e8716b963 (earlier+6), plus WireHistory ForeignAbsenceProbe+1: the existing empty-wire-interval check now reads membership from PreparedTranscriptPage rather than a separate parameter. No zero-debt claim. No automated Toad tests or synthetic app. All owned frontends/helpers closed; own Xvfb preserved. #675 remains draft, not merged/installed, and full throughput acceptance is blocked on the observed-read delays. Original sessions, BLOCKED goal and UNKNOWN attempts remain untouched.

## 2026-10-16: installed sidebar-resize baseline (13050), no repair yet

Normal-user startup of the exact installed T160107Z CLI (Toad d51f2b08a/Core8099d1f6e), plain `st` on owned :210; real growing #comms, no source overlay or provider input. The existing live frame meter was loaded at frontend startup, not injected. One 20-column physical edge drag took1.019s and produced12 changed percentages; only two affected-width frames painted. Input is exclusively through the reviewed `XvfbDisplay.input` owner. This is a sidebar-only diagnostic, **not the fixed scrolling scenario**.

| Change | Affected-width frame median/p95 ms | Input-to-paint median/p95 ms | Result |
|---|---:|---:|---|
| Instrumented installed baseline, no production repair | 525.8 / 825.9 (2 frames) | 280.3 / 784.3 (24 mouse inputs) | Per12 actual width changes: event-carrier queue wait mean1.94ms (p955.85, zero intervening frames); layout_sidebars5.12ms (18 calls); active native arrangement32.76ms excluding fragment measurement; fragment measure0.79ms (plain wrapping0.27ms subset); visible damage rendering plus composition19.42ms.98 plain fragment rewraps across7 measured widths:29 last-painted visible /69 offscreen, mean2.42/5.75 per changed-width step. No Stylesheet.apply/update_nodes in the drag window despite567 inline rule-update calls. Native arrangement/render work dominates, not plain wrapping. The full12s window's9.0/46.4ms frame figures include idle paints and must not stand in for the drag. |
| Frame-sampled width e6bcf4aa, source candidate; no acceptance | 229.6 / 898.0 (3 frames) | 310.0 / 798.3 (23/24 inputs paired) | Same gesture1.027s/12 changes, same dependencies/meter; growing #comms, not controlled A/B. Core layout event declaration/subscription/producers deleted; no Core queue hop. Native frame start consumes width, sidebar Resize feedback deleted.7 layout starts produced3 paints: one-layout-per-paint target still fails. Sidebar layout32.84ms total versus61.50 baseline; native arrangement414.64ms, fragment measure12.75ms, paint+composition224.75ms.85 plain rewraps:21 visible/64 offscreen. No responsiveness improvement demonstrated. Existing #673 rejected-publication fix is integrated only after this run; not included in these numbers. |
| Frame-sampled width plus existing #673 retention-result fix e757526a; source candidate, still fails | 1157.0 / 1157.0 (1 frame) | 626.0 / 1115.1 (24/24 inputs paired) | Same gesture/meter; added native child-placement counter for subsequent traversal comparisons.10 layout starts/1 paint;1402 child placements/194 unique widgets (75 ThreadRows,5 TranscriptPageViews).105 page plain rewraps:21 visible/84 offscreen. Rejected publications no longer open reader changes, but accepted batches still interrupt unfinished native layouts. Not an improvement. |
| Render-result publication waits for existing native Screen refresh; source candidate, acceptance unfinished | 47.9 / 84.3 (21 frames) | 33.8 / 77.1 (25/25 inputs paired) | Same gesture/meter/counter as preceding row; worst frame120.5ms.12 width changes,23 layout starts,21 paints through actual final50-column bar geometry.2392 child placements/193 unique widgets;168 page plain rewraps48 visible/120 offscreen, plus48 wraps outside page-measure scope (visibility unknown, not relabelled). The frame now finishes before a worker batch can keep the reader and replace its geometry. #673 acceptance remains after that wait. No new timer/queue/cache; existing screen callback owner. Growing histories and host pressure prevent controlled causal/A-B or uninstrumented acceptance claims. Requested p95<50ms/worst<33ms and offscreen-zero still fail. |

This is an attribution run with call/frame instrumentation, not uninstrumented latency acceptance. Phase spans run from first accepted width to first final-width paint; queue waits are not CPU work. Frame work includes all UI callbacks; worker Markdown parsing is not the main-thread plain-wrap timing. The py-spy100Hz nonblocking profile supports source attribution at `Compositor.reflow_subtrees_steps:1093`, `arrange_widget:1436`, `add_widget:1520/1553`, `SubtreeGeometry.capture:274`, and `Screen._render_damage_steps:1737`;146 samples and129 read errors preclude quantitative sampling percentages. The initial pausing1000Hz profile fell4.4s behind and is retained as an instrumentation failure, not application-latency evidence. Raw captures, source counts and scene identity: `~/.cache/agent-scratch/toad-sidebar-frame-owner/`; source repair awaits Claude's plan review. No automated tests, fake responses or apps.

## 2026-10-16: P37 shared-candidate diagnostic, not accepted

Private installed Toad f45a06650/Core3adf, including P37/P57/P60; no overlay. Plain `st` on isolated :218, existing saved active `core-retry`, seven-second held PageUp, wheel reversal, End and tab return through `XvfbDisplay.input`. The startup-loaded common frame meter covered every gesture. This is **not** the fixed fork/active-response scenario; no provider input was submitted. The recorder was not started, so there is no continuous video and no scrolling acceptance. Original data: `~/.cache/agent-scratch/toad-outgoing-live/delivery-labels-p57-p60-before-20261016-1640/`.

| Change | Total frame median/p95 ms | Input-to-paint median/p95 ms | Result |
|---|---:|---:|---|
| Viewport-owned source requests, before removing the remaining fragment-count clip | 12.2 / 71.6 | 22.1 / 148.6 | 484 frames,277 inputs during the gesture window. Two distinct bottom source page starts before scrolling,35 decoded page responses including repeated live refreshes (P60 owns that repetition). Held PageUp alone:299 frames,155 inputs, frame12.6/70.9ms, input17.7/124.8ms. Fourteen blocked PageUp inputs grouped into five lazy-edge waits:144.1,223.5,32.6,68.9,59.7ms. Screenshots show older history after PageUp and latest body after End/tab return; they cannot prove continuous motion. `rts-results/p37.json` says not_accepted and recording:null. No before/after improvement claim. |
2 changes: 1 addition & 1 deletion src/toad/acp/agent_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async def start(self, agent, processes=()):
selection = RouteSelection.for_child(env, cwd)
with ExitStack() as acquisition:
try:
acquisition.enter_context(selection.route.admit_client())
acquisition.enter_context(selection.route.client_scope())
await AcpIngress(selection, Path(cwd), self.attached_root(agent)).preflight()
except (OSError, ValueError, RuntimeError) as error:
agent.session.record_failure(LogAgentFail(
Expand Down
70 changes: 40 additions & 30 deletions src/toad/acp/agent_session.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"""ACP session handshake, durable metadata and reconnection custody."""
"""ACP session handshake, durable metadata and reconnection."""
from __future__ import annotations
from agent_comms.declared_family import DeclaredFamily
from agent_comms.child_process import join_retirement
import asyncio
import os
from pathlib import Path
from dataclasses import dataclass, replace
from agent_comms.acp_ingress import AcpIngress
from agent_comms.errors import RelationViolationError
from agent_comms.route_selection import RouteChanged, RouteSelection
import toad
from toad import constants, jsonrpc
from toad.core import events as core_events
Expand All @@ -15,7 +19,7 @@
from toad.db import DB, SessionMeta
from agent_comms.acp_failure import ACPFailure
from agent_comms.input_attempt import NotSentInput
from agent_comms.session_load import EnsuringSessionLoadAdmission, ExistingSessionLoadAdmission, FailedSessionLoadAdmission, SessionLoadAdmission
from agent_comms.session_load import EnsuringSessionLoadPlan, ExistingSessionLoadPlan, FailedSessionLoadPlan, SessionLoadPlan

PROTOCOL_VERSION = 1

Expand Down Expand Up @@ -72,6 +76,9 @@ class RefusedSessionDisposition(FailedSessionDisposition):

class AgentSession:
"""Own readiness and durable session effects; binding identity stays on the controller."""

reconnect_timeout_seconds = 30

def __init__(self, agent, pk):
self.agent = agent
self.task = None
Expand All @@ -82,7 +89,7 @@ def __init__(self, agent, pk):
self.chosen_name = None
self.connected = False
self.reconnecting = False
self.load_admission: SessionLoadAdmission = EnsuringSessionLoadAdmission()
self.load_plan: SessionLoadPlan = EnsuringSessionLoadPlan()
self.settled = asyncio.Event()

@property
Expand Down Expand Up @@ -118,11 +125,15 @@ def start(self):
self.task = asyncio.create_task(self.run())
self.task.add_done_callback(self.session_finished)

async def retire(self):
def begin_retirement(self):
self.close()
if self.retirement is None:
self.retirement = asyncio.create_task(self._retire())
await join_retirement(self.retirement)
self.retirement.add_done_callback(self.retirement_finished)
return self.retirement

async def retire(self):
await join_retirement(self.begin_retirement())

async def _retire(self):
pending = tuple(task for task in (self.task, *self.responses)
Expand All @@ -141,8 +152,6 @@ def session_finished(self, task):
error = task.exception()
if error is not None and self.accepts_updates:
self.startup_failed(f"{type(error).__name__}: {error}")
self.retirement = asyncio.create_task(self._retire())
self.retirement.add_done_callback(self.retirement_finished)

def retirement_finished(self, task):
if task.cancelled():
Expand All @@ -161,6 +170,7 @@ def record_failure(
self.failed()
self.disposition = state(event)
self.agent.events.publish(event)
self.begin_retirement()

def session_failed(self, failure):
self.record_failure(
Expand Down Expand Up @@ -189,7 +199,7 @@ def supports_load(self):
return self.capabilities.load_session

def starting(self):
"""Start admission clears readiness before any asynchronous work."""
"""Starting clears readiness before any asynchronous work."""
self.connected = False
self.settled.clear()
self.agent.controller.reset_configuration()
Expand Down Expand Up @@ -269,20 +279,14 @@ async def observe_owner(self, presentation) -> None:
if self.agent.coordination is None:
return
binding = presentation.binding
if not self.load_admission.superseded_by(binding):
if not self.load_plan.superseded_by(binding):
return
await self.reconnect(ExistingSessionLoadAdmission(binding))
await self.reconnect(ExistingSessionLoadPlan(binding))

async def reconnect(self, admission: SessionLoadAdmission | None = None) -> None:
async def reconnect(self, plan: SessionLoadPlan | None = None) -> None:
"""Reattach the existing view after login or an explicit owner start."""
if self.reconnecting:
return
if self.agent.controller.session.bound and not self.supports_load:
raise ValueError("This agent cannot resume its session.")
from pathlib import Path
from agent_comms.acp_ingress import AcpIngress
from agent_comms.route_selection import RouteSelection

requested_cwd = str(self.agent.project_root_path.resolve())
self.reconnecting = True
try:
Expand All @@ -291,17 +295,23 @@ async def reconnect(self, admission: SessionLoadAdmission | None = None) -> None
RouteSelection.for_child, os.environ.copy(), requested_cwd)
await AcpIngress(selection, Path(requested_cwd),
self.agent.process.attached_root(self.agent)).preflight()
except Exception as error:
raise ValueError(
f"Reconnect not attempted: maintenance admission denied: {error}"
) from error
except (RouteChanged, RelationViolationError) as error:
self.startup_failed(f"Reconnect blocked before restart: {error}")
return
await self.agent.stop()
self.load_admission = admission or EnsuringSessionLoadAdmission()
self.load_plan = plan or EnsuringSessionLoadPlan()
self.settled.clear()
await self.agent.start((self.agent.process,))
await asyncio.wait_for(self.settled.wait(), timeout=30)
if not self.connected:
raise ValueError("The agent could not reconnect.")
try:
await asyncio.wait_for(
self.settled.wait(), timeout=self.reconnect_timeout_seconds)
except TimeoutError:
self.startup_failed(
f"Reconnect timed out after {self.reconnect_timeout_seconds} seconds "
"waiting for the session handshake to settle."
)
except (OSError, jsonrpc.APIError) as error:
self.startup_failed(f"Reconnect failed: {type(error).__name__}: {error}")
finally:
self.reconnecting = False

Expand Down Expand Up @@ -413,17 +423,17 @@ async def load(self) -> None:
if agent_data := session.meta_json.agent_data:
self.agent.definition = agent_data

admission, self.load_admission = self.load_admission, EnsuringSessionLoadAdmission()
plan, self.load_plan = self.load_plan, EnsuringSessionLoadPlan()
with self.agent.request():
session_load_response = api.session_load(cwd, [], request_session_id,
admission.metadata())
plan.metadata())
try:
response = await session_load_response.wait()
except jsonrpc.APIError as error:
authority.require()
if self.agent.queue_attachment.is_current_request(queue_token):
if failed_command := FailedSessionLoadAdmission.from_failure(error.data):
self.load_admission = failed_command
if failed_command := FailedSessionLoadPlan.from_failure(error.data):
self.load_plan = failed_command
raise
authority.require()
if (
Expand Down Expand Up @@ -496,7 +506,7 @@ async def set_name(self, name: str) -> None:


def publish_configuration(self, response):
self.load_admission = SessionLoadAdmission.at_response(response.field_meta)
self.load_plan = SessionLoadPlan.at_response(response.field_meta)
if (modes := response.modes) is not None:
self.agent.controller.publish_modes(modes)
self.agent.configuration.receive(response.config_options)
2 changes: 1 addition & 1 deletion src/toad/acp/tool_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass, replace

from acp.schema import ToolCall, ToolCallStart, ToolCallUpdate
from agent_comms.transcript_receipts import AssignedSourceIdentity
from agent_comms.transcript_sources import AssignedSourceIdentity
from toad.core import events
from toad.acp.client_session import ClientSessionRequest
from toad.acp.status import ToolCallStatus
Expand Down
Loading
Loading