Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
c229a0d
Prepare and launch one verified Neovim release artifact
trissim Oct 30, 2026
ebfceaf
Merge remote-tracking branch 'origin/main' into work/vim-neovim-prere…
trissim Oct 30, 2026
6222aaa
Declare Neovim release assets by platform and architecture
trissim Oct 30, 2026
8b62482
Merge remote-tracking branch 'origin/main' into work/vim-neovim-prere…
trissim Oct 30, 2026
85d3543
Give unscheduled sidebar work its own queryable value
trissim Oct 30, 2026
999edc4
Consume page-owned message evidence in wire fragments
trissim Oct 30, 2026
e26db26
Borrow the retained coordinator reader for context annotations
trissim Oct 30, 2026
43c1e26
Follow declared types in nine stale model tests
trissim Oct 30, 2026
e638d02
Bind observation and usage tests to their declared owners
trissim Oct 30, 2026
ff2f194
Pass retained owner incarnation to context requests
trissim Oct 30, 2026
ba1633e
Merge remote-tracking branch 'origin/main' into work/slot-activity--P…
trissim Oct 30, 2026
9ed84e4
Pass the launch root from the CLI into each Toad app
trissim Oct 30, 2026
c7a778e
Terminal owns printable keys through NativeTextKeys
trissim Oct 30, 2026
f26ab95
Relock textual to 832b0f6d so NativeTextKeys exists
trissim Oct 30, 2026
bcc8ce7
Use incarnation-bound owner requests and native response declarations
trissim Oct 30, 2026
519382b
Derive history Vim scrolling from inherited arrow identities
trissim Oct 30, 2026
3fbd6a0
Resolve app routes from held launch roots
trissim Oct 30, 2026
cab4ccc
P1022: follow Core read and session owners in Toad tests
trissim Oct 30, 2026
5db4598
P1022: migrate exposed unread, painted-read and context-page callers
trissim Oct 30, 2026
54142ff
P1022: complete unread and divider test caller migration
trissim Oct 30, 2026
3001ecc
P1022: use declared fixture identities and scroll the paged viewport
trissim Oct 30, 2026
3e7dac5
P1022: expose owned read work and migrate complete caller family
trissim Oct 30, 2026
fe37860
P1022: address retained source rows and finish retired history callers
trissim Oct 30, 2026
54db4ff
Own app route selection in CoordinationAccess
trissim Oct 30, 2026
4a7a415
P1032: repaint channel members only for changed pins
trissim Oct 30, 2026
fd683f3
Merge pull request #1363 from OpenHCSDev/work/slot-activity--P958-con…
trissim Oct 30, 2026
f2b22eb
P1022: restore mounted source ranges and migrate saved-view consumers
trissim Oct 30, 2026
021b3d3
Merge remote-tracking branch 'origin/main' into work/toad-sidebar--P1032
trissim Oct 30, 2026
2b4c760
Merge remote-tracking branch 'origin/main' into work/toad-render--P1015
trissim Oct 30, 2026
9df4c24
Import the route access owner at module scope
trissim Oct 30, 2026
a352a0f
Merge pull request #1364 from OpenHCSDev/hotfix/suite-timeout
trissim Oct 30, 2026
4475720
Merge pull request #1366 from OpenHCSDev/fix/terminal-owns-printable-…
trissim Oct 30, 2026
8549d15
Merge pull request #1367 from OpenHCSDev/work/commander-screen--P1010…
trissim Oct 30, 2026
3860cf8
Merge pull request #1356 from OpenHCSDev/fix/sidebar-worker-presence
trissim Oct 30, 2026
f3e8448
Merge pull request #1357 from OpenHCSDev/work/vim-neovim-prerequisites
trissim Oct 30, 2026
b4f29e0
Merge pull request #1369 from OpenHCSDev/work/toad-sidebar--P1032
trissim Oct 30, 2026
234454f
Merge pull request #1365 from OpenHCSDev/work/toad-render--P1015
trissim Oct 30, 2026
1d85311
Merge pull request #1360 from OpenHCSDev/work/producer-digest--P993--…
trissim Oct 30, 2026
cbcc720
Give Action one metaclass with both parents so Toad imports on Textua…
trissim Oct 30, 2026
c72556c
Merge pull request #1372 from OpenHCSDev/hotfix/action-key-family-meta
trissim Oct 30, 2026
fe5b3ae
P1022: follow Core read and session owners in Toad tests
trissim Oct 30, 2026
1fd68a0
P1022: migrate exposed unread, painted-read and context-page callers
trissim Oct 30, 2026
f0524cc
P1022: complete unread and divider test caller migration
trissim Oct 30, 2026
8cce6c5
P1022: use declared fixture identities and scroll the paged viewport
trissim Oct 30, 2026
1d7ae68
P1022: expose owned read work and migrate complete caller family
trissim Oct 30, 2026
cfea9c6
P1022: address retained source rows and finish retired history callers
trissim Oct 30, 2026
df54a5d
P1022: restore mounted source ranges and migrate saved-view consumers
trissim Oct 30, 2026
eaf09cc
P1022: own finished read state and exercise native message navigation
trissim Oct 30, 2026
e65569e
Preserve published P1022 ancestry after rebasing onto current main
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
22 changes: 9 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Local-equivalent verification
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
Expand All @@ -14,7 +16,7 @@ permissions:
contents: read
concurrency:
group: toad-verification-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.event_name != 'push' }}
jobs:
nra-gate:
name: NRA gate
Expand All @@ -24,7 +26,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || inputs.head }}
ref: ${{ github.event.pull_request.head.sha || inputs.head || github.sha }}
- uses: actions/setup-python@v5
with:
python-version: '3.14'
Expand All @@ -44,8 +46,8 @@ jobs:
# pull_request.base.sha is not refreshed when the base branch moves, so it can name a
# revision many commits behind and charge the PR for debt main introduced. The PR's own
# change starts at the merge-base with the base branch's current tip.
BASE_TIP: ${{ github.event.pull_request.base.ref && format('origin/{0}', github.event.pull_request.base.ref) || inputs.base }}
PR_HEAD: ${{ github.event.pull_request.head.sha || inputs.head }}
BASE_TIP: ${{ github.event.pull_request.base.ref && format('origin/{0}', github.event.pull_request.base.ref) || inputs.base || github.event.before }}
PR_HEAD: ${{ github.event.pull_request.head.sha || inputs.head || github.sha }}
run: PYTHONPATH="$PWD/.core-gate/src" PATH="$PWD/.venv/bin:$PATH" .venv/bin/python .core-gate/tools/nra_client_gate.py --root src/toad --base "$(git merge-base "$BASE_TIP" "$PR_HEAD")" --head "$PR_HEAD"
- name: Run surface guards
run: .venv/bin/pytest tests/guards
Expand All @@ -56,18 +58,10 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || inputs.head }}
ref: ${{ github.event.pull_request.head.sha || inputs.head || github.sha }}
- uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install Neovim
run: |
curl --fail --location --silent --show-error \
https://github.com/neovim/neovim/releases/download/v0.12.4/nvim-linux-x86_64.tar.gz \
--output "$RUNNER_TEMP/neovim.tar.gz"
tar --extract --gzip --file "$RUNNER_TEMP/neovim.tar.gz" --directory "$RUNNER_TEMP"
echo "$RUNNER_TEMP/nvim-linux-x86_64/bin" >> "$GITHUB_PATH"
"$RUNNER_TEMP/nvim-linux-x86_64/bin/nvim" --version
- uses: ./.github/actions/core-from-main
with:
path: .artifacts/core
Expand All @@ -88,6 +82,8 @@ jobs:
with open(os.environ['GITHUB_ENV'], 'a') as env:
env.write(f'AC_NATIVE_COPIED_PACKAGE={package.resolve()}\n')
PYCORE
- name: Prepare the declared Neovim requirement
run: .venv/bin/python -m toad.neovim_runtime
# xdist reports a collection error as a worker crash; collect alone first so it reads as one.
- run: .venv/bin/pytest --collect-only -q
# A test stuck past 120 s dumps every thread, so the red names where it hung.
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ textual-diff-view = { git = "https://github.com/OpenHCSDev/textual-diff-view.git
[project.scripts]
toad = "toad.cli:main"

[project.entry-points."agent_comms.runtime_preparation"]
neovim = "toad.neovim_runtime:Neovim"

[project.entry-points."agent_comms.writer_owned_stores"]
toad = "toad.db"
preferences = "toad.preferences"
Expand Down
6 changes: 3 additions & 3 deletions src/toad/about.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import platform
from string import Template

from agent_comms.launch_root import LaunchRoot
from pathlib import Path
from toad.preferences import SavedSettings
from toad.terminal_attention import TerminalAttention
from toad import paths
Expand Down Expand Up @@ -78,7 +78,7 @@
)


def render() -> str:
def render(root: Path) -> str:
"""Render about markdown.

Returns:
Expand All @@ -95,7 +95,7 @@ def render() -> str:
"COLORTERM": os.environ.get("COLORTERM", ""),
"COMMS_AGENT_ARGS": shlex.join(InheritedArguments().configured().argv),
"COMMS_AGENT_BIN": ConfiguredBinary().configured(),
"COMMS_ROOT": str(LaunchRoot.current().path),
"COMMS_ROOT": str(root),
"CONFIG": config,
"DATA_PATH": paths.get_data(),
"LOG_PATH": paths.get_log(),
Expand Down
9 changes: 3 additions & 6 deletions src/toad/acp/agent_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from agent_comms.acp_failure import ACPFailure, RequestFailure
from agent_comms.coordination_errors import Refused
from agent_comms.runtime import RuntimeConnection, socket_path
from agent_comms.transcripts import TranscriptReadIdentity
from agent_comms.ui_model.delivery import DeliveryView
from agent_comms.ui_model.observation import ReadDeliveryOverview
Expand Down Expand Up @@ -390,10 +391,6 @@ async def _cancel_prompt(self, authority: ClientSessionRequest):
async def request_owner(self, method: str, **params: JsonValue):
if self.coordination is None:
raise ValueError("This action requires an agent-comms thread.")
from agent_comms.runtime import RuntimeProxy, socket_path

from toad.owner_preparation import OwnerRequestContext

coordination = self.coordination
authority = ClientSessionRequest(self.agent, self.agent.session_id)
self.require_owner(coordination, authority)
Expand All @@ -403,8 +400,8 @@ def resolve():
owner = comms.registry.require(coordination.thread.name)
if owner.incarnation != coordination.thread:
raise ValueError("The owner incarnation changed while preparing the request.")
return RuntimeProxy(
OwnerRequestContext(comms), owner.name, socket_path(comms.root, owner.require_process().pid)
return RuntimeConnection(
comms, owner.incarnation, socket_path(comms.root, owner.require_process().pid)
)

proxy = await asyncio.to_thread(resolve)
Expand Down
2 changes: 1 addition & 1 deletion src/toad/acp/agent_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def start(self, agent, processes: tuple[AgentProcess, ...]=()):
agent.presentation.log_path.parent.mkdir, parents=True, exist_ok=True)
env = os.environ.copy()
cwd = str(agent.project_root_path.resolve())
selection = RouteSelection.capture(agent.controller.transcripts.access.launch_root.path)
selection = agent.controller.transcripts.access.selection
if not agent.session.accepts_updates:
return self
for connection in dict.fromkeys((self, *processes)):
Expand Down
3 changes: 1 addition & 2 deletions src/toad/acp/agent_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from agent_comms.errors import RouteChangedError
from agent_comms.input_attempt import NotSentInput
from agent_comms.owner_protocol import IncompatibleOwner
from agent_comms.route_selection import RouteSelection
from agent_comms.session_load import (
EnsuringSessionLoadPlan,
ExistingSessionLoadPlan,
Expand Down Expand Up @@ -362,7 +361,7 @@ async def reconnect(self, plan: SessionLoadPlan | None = None) -> None:
try:
try:
await asyncio.to_thread(
RouteSelection.capture, self.agent.controller.transcripts.access.launch_root.path)
lambda: self.agent.controller.transcripts.access.selection)
except RouteChangedError as error:
self.startup_failed(f"Reconnect blocked before restart: {error}")
return
Expand Down
6 changes: 1 addition & 5 deletions src/toad/acp/comms_updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

from agent_comms.acp_extension import (
Expand All @@ -29,7 +28,6 @@
TurnChangedUpdate,
)
from agent_comms.context_reading import UnmeasuredContext
from agent_comms.errors import RouteChangedError
from agent_comms.native_output import AssistantOutput, ToolOutput
from agent_comms.thread_presentation import NoThreadPresentation, ThreadPresentation
from agent_comms.transcripts import NativeCommittedPrefix
Expand Down Expand Up @@ -186,9 +184,7 @@ def _post_unidentified(self, update: InputStartedUpdate) -> None:
@handles(CoordinationChangedUpdate)
def coordination_changed(self, update: CoordinationChangedUpdate) -> None:
agent = self.agent
root = agent.controller.transcripts.access.launch_root.path
if Path(update.wire_root).resolve() != root.resolve():
raise RouteChangedError("ACP owner reported a different coordination root")
agent.controller.transcripts.access.selected(update.wire_root)
agent.coordination = update
agent.project_root_path = update.worktree.path
agent.context_measurement = ContextMeasurement.saved(update.context_usage)
Expand Down
3 changes: 1 addition & 2 deletions src/toad/acp/transcript_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from functools import partial

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,
Expand Down Expand Up @@ -44,7 +43,7 @@ async def read(self, request: AwaitedRead):

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

async def thread_presentation(
Expand Down
5 changes: 4 additions & 1 deletion src/toad/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing import TYPE_CHECKING, Callable, Never, TypeVar, cast

from agent_comms.acp_extension import TranscriptChangedUpdate
from agent_comms.launch_root import LaunchRoot
from agent_comms.repo import Checkout
from toad.core import input_events
from rich.console import RenderableType
Expand Down Expand Up @@ -150,6 +151,7 @@ def __init__(
self,
agent_binding: AgentBinding,
project_dir: Checkout,
launch_root: LaunchRoot,
mode: str | None = None,
agent_session_id: str | None = None,
) -> None:
Expand All @@ -158,6 +160,7 @@ def __init__(
Args:
agent_binding: The agent selected at startup.
project_dir: Project directory.
launch_root: Coordination root selected by the launch owner.
mode: Initial mode.
"""
from toad.render_processes import RenderProcessPool
Expand Down Expand Up @@ -189,7 +192,7 @@ def __init__(
db=DB(),
)
self._session_tracker = SessionTracker(self.tab_order)
self.coordination_access = CoordinationAccess(self)
self.coordination_access = CoordinationAccess(self, launch_root)

from toad.file_kind import FileKind
FileKind.prepare_terminal()
Expand Down
18 changes: 12 additions & 6 deletions src/toad/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import click

from agent_comms.field_codec import FieldCodec
from agent_comms.launch_root import LaunchRoot
from agent_comms.repo import Checkout
from toad.session_scope import AgentAttached, AgentBinding, NoAgent

Expand Down Expand Up @@ -75,10 +76,9 @@ def parse_args(self, ctx, args):
if "--version" in args or "-v" in args:
return super().parse_args(ctx, args)
from agent_comms.active_route import ActiveRoute
from agent_comms.launch_root import LaunchRoot

# One selection, as in every Comms process: --comms-root if given, else the published route.
LaunchRoot.startup(args, ActiveRoute.published_launch)
# Select once at the process boundary; apps receive this captured value.
ctx.obj = LaunchRoot.startup(args, ActiveRoute.published_launch)
# Check if first arg is a known subcommand
if not args or args[0] not in self.commands:
# If not a subcommand, prepend the default command name
Expand Down Expand Up @@ -123,7 +123,8 @@ def main(ctx, version):
@main.command("run")
@click.argument("project_dir", metavar="PATH", required=True)
@click.option("-a", "--agent", metavar="AGENT", default="")
def run(project_dir: str, agent: str = "1"):
@click.pass_obj
def run(launch_root: LaunchRoot, project_dir: str, agent: str = "1"):
"""Run an installed agent."""

project = FieldCodec.decode(Checkout, project_dir).resolved(strict=True)
Expand All @@ -137,6 +138,7 @@ def run(project_dir: str, agent: str = "1"):
mode=binding.startup_mode,
agent_binding=binding,
project_dir=project,
launch_root=launch_root,
)
run_terminal(app)

Expand All @@ -158,7 +160,9 @@ def run(project_dir: str, agent: str = "1"):
default=None,
)
@click.option("-d", "--project-dir", "project_option", metavar="PATH", default=None)
@click.pass_obj
def acp(
launch_root: LaunchRoot,
command: str,
title: str | None,
project_dir: str | None,
Expand Down Expand Up @@ -195,6 +199,7 @@ def acp(
app = ToadApp(
agent_binding=AgentAttached(agent_data),
project_dir=FieldCodec.decode(Checkout, project_dir).resolved(strict=True),
launch_root=launch_root,
agent_session_id=session_id,
)
run_terminal(app)
Expand Down Expand Up @@ -269,12 +274,13 @@ def serve(port: int, host: str, toad_args: tuple[str, ...]) -> None:


@main.command("about")
def about() -> None:
@click.pass_obj
def about(launch_root: LaunchRoot) -> None:
"""Show about information."""

from toad import about

print(about.render())
print(about.render(launch_root.path))


if __name__ == "__main__":
Expand Down
26 changes: 19 additions & 7 deletions src/toad/comms_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from agent_comms.comms import Comms
from agent_comms.display_references import PendingReferences, UnavailableReferences
from agent_comms.errors import RouteChangedError
from agent_comms.launch_root import LaunchRoot
from agent_comms.launch_runtime import LaunchRuntime
from metaclass_registry.mro_dispatch import MroDispatch, handles
from agent_comms.presentation import MessageNotification, NoRevision, ReadState
Expand Down Expand Up @@ -329,11 +330,9 @@ def observe_commander(self, wanted: ObserveCommanderHud | StopCommanderHud):
class CoordinationAccess(MroDispatch):
"""A validated Core route owns cached access, observations and UI write checks."""

def __init__(self, app: ToadApp) -> None:
from agent_comms.launch_root import LaunchRoot

def __init__(self, app: ToadApp, launch_root: LaunchRoot) -> None:
self.app = app
self.launch_root = LaunchRoot.current()
self.launch_root = launch_root
from toad.acp.transcript_reader import CoordinationTranscriptReader

self.transcripts = CoordinationTranscriptReader(self)
Expand Down Expand Up @@ -361,18 +360,31 @@ def __init__(self, app: ToadApp) -> None:
self.commander_pages: WeakSet[CommanderPage] = WeakSet()
self.commander_interest: ObserveCommanderHud | StopCommanderHud = StopCommanderHud()

@property
def selection(self) -> RouteSelection:
"""The current route resolved from this app's held launch root."""
return RouteSelection.of(self.launch_root)

def selected(self, source: str | Path) -> RouteSelection:
"""Select this app's route and refuse a foreign source."""
return self.selection.require_source(source)

def current(self, source: str | Path) -> bool:
"""Whether a source still belongs to this app's current route."""
return self.selection.root_is_current(source)

def open_current(self) -> Observing | FailedObservation:
"""Capture the current route and open its service, or expose the opening error."""
with self.opening:
selected: RouteSelection | UnselectedRoute = UnselectedRoute()
try:
selected = RouteSelection.capture(self.launch_root.path)
selected = self.selection
return self.require(selected)
except (OSError, ValueError, RuntimeError) as error:
return self.observation.failed(error, selected)

def confirm(self, selected: RouteSelection) -> None:
if RouteSelection.capture() != selected:
if self.selection != selected:
raise RouteChangedError("Comms route changed before the operation")

def require(self, selected: RouteSelection) -> Observing:
Expand Down Expand Up @@ -649,5 +661,5 @@ async def observe_route(self) -> None:
def write(self, selected: RouteSelection, operation: Callable[..., T], *args: object, **kwargs: object) -> T:
# This method runs in the same worker as the actual sink. No route lock: the
# store refuses a write an install holds, inside its own write transaction.
RouteSelection.capture(selected.root)
self.confirm(selected)
return operation(*args, **kwargs)
6 changes: 3 additions & 3 deletions src/toad/core/context_inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from agent_comms.field_codec import FieldCodec
from agent_comms.importing import ImportedSessionMetadata
from metaclass_registry.mro_dispatch import handles
from agent_comms.native_turn_context import NativeContextData
from agent_comms.pi_payloads import NativeContextData
from agent_comms.pi_payloads import PiMessage
from agent_comms.presentation import ReadState
from agent_comms.registry_document import RegistryEntry
Expand Down Expand Up @@ -539,7 +539,7 @@ def read(
page = comms.contexts.page(owner, comms.registry,
cursor=cursor, previous=previous)
annotations = WorkingMemoryAnnotations.for_context(
comms.root / "coordination.sqlite3",
comms.registry.store.coordinator_reader,
tuple(resource.value for resource in page.resources), JevClassifier.version())
imported = ImportedSessionMetadata.sources_for_owner(comms.registry, thread)
inspection_type = ThreadInspectionSource().project_value(entry.status)
Expand Down Expand Up @@ -622,7 +622,7 @@ async def _request(self, action: str, **parameters: JsonValue | tuple[int, ...])
if owner.incarnation != self.owner.incarnation:
raise ValueError("Selected context thread incarnation changed")
process = owner.require_process()
connection = RuntimeConnection(self.service, owner.name, socket_path(self.service.root, process.pid))
connection = RuntimeConnection(self.service, owner.incarnation, socket_path(self.service.root, process.pid))
try:
return await connection.request(action, **parameters)
finally:
Expand Down
Loading
Loading