Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c30556b
P301 checkpoint: observe live native order progress without claiming …
trissim Oct 20, 2026
bbec965
Exercise copied native creation assignment evidence and strikes; repa…
trissim Oct 20, 2026
9a3fa0d
Own candidate worker retirement and measure native calls through Comm…
trissim Oct 20, 2026
fdaf3b1
Let parity declarations own their comparison callable
trissim Oct 20, 2026
13e0647
Declare comparisons once and keep lifecycle observations typed at the…
trissim Oct 20, 2026
65998b3
Select lifecycle units from the caller or canonical held orders, not …
trissim Oct 20, 2026
6cf69c4
Route native closing through the existing commander invocation owner
trissim Oct 20, 2026
b133760
Let the existing lifecycle copier include real receiver threads for t…
trissim Oct 20, 2026
1335050
Read commander authority through its existing configured context inst…
trissim Oct 20, 2026
46d8481
Accept read and write request capabilities in lifecycle comparison de…
trissim Oct 20, 2026
b365563
Compare real current and native refusals to hand delivered checked wo…
trissim Oct 20, 2026
165d7a9
Admit both read and thread-only tools in lifecycle observations
trissim Oct 20, 2026
5c74c47
Let BoundedRun own native comparison process outcomes and retirement …
trissim Oct 20, 2026
9f3c32e
Preserve caller-owned cwd and sanitized environments in the bounded p…
trissim Oct 20, 2026
cb7ecb0
Confirm real child environment and project preservation through the s…
trissim Oct 20, 2026
78bab16
Place checked-commit worktrees under the configured checkout owner
trissim Oct 20, 2026
122955a
Exercise current and native delivered-commit outcomes on the copied bus
trissim Oct 20, 2026
418e7f9
Keep every copied delivery order failing until its actual run
trissim Oct 20, 2026
0d2d8d0
State the checked main commit and migrate the explicit check proof
trissim Oct 20, 2026
b6751ac
Compare real command process exits without swallowing native refusals
trissim Oct 20, 2026
31eb284
Keep child timeout and retirement outcomes in the delivery comparison
trissim Oct 20, 2026
d687f45
Use the lifecycle-owned candidate process launcher for delivered-comm…
trissim Oct 20, 2026
416cc64
Compare deletion-intent refusal and acceptance through both real deli…
trissim Oct 20, 2026
6286f26
Exercise current command and native entrypoints with exact delivered-…
trissim Oct 20, 2026
8d178ce
Let declared requests compose argument projection and let the CLI own…
trissim Oct 20, 2026
ae0ccaa
Use caller-owned identity and declared native CLI projection in delet…
trissim Oct 20, 2026
5f9571d
Bind the native CLI module at its ordinary module import boundary
trissim Oct 20, 2026
9c01307
Delete the other hand-built native tool argv and envelope projection
trissim Oct 20, 2026
5f578b5
Use the shared bounded lifecycle producer and typed delivery outcomes
trissim Oct 20, 2026
999a312
Remove codec imports made obsolete by native declaration ownership
trissim Oct 20, 2026
11d8947
Pass candidate Python environment through the shared process owner ex…
trissim Oct 20, 2026
f0ef11b
Require explicit project and environment at the common process boundary
trissim Oct 20, 2026
b9257f3
Use the current recorded-time owner throughout native lifecycle compa…
trissim Oct 20, 2026
0ef4b2b
Drop the deleted commander CLI field from the lifecycle constructor
trissim Oct 20, 2026
9243f58
Keep generated deadline input in the actual CLI HH:MM grammar
trissim Oct 20, 2026
31d9f3a
Compare judge and native Verdict on actual copied-session edits
trissim Oct 20, 2026
e60f414
Verify persisted verdict call identity and the strike event
trissim Oct 20, 2026
abd953d
Check the concrete verdict kind and its committed Goal transition
trissim Oct 20, 2026
0430570
Prepare the real copied-root rule review before invoking judge
trissim Oct 20, 2026
bac04db
Require the send-back reason in the derived Goal and test the verdict…
trissim Oct 20, 2026
1549f62
Use LifecycleObservation for verdict proof and retain control input w…
trissim Oct 20, 2026
c2ab0ed
Use the shared bounded producer for real native edit execution
trissim Oct 20, 2026
f090006
Keep verdict proof on committed Goal history and correct its owner de…
trissim Oct 20, 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
4 changes: 2 additions & 2 deletions plans/commander-native-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
| 5 | Board, overdue, silent, spinning | `order.py board`, `overdue`; `fleet-board.md` | `CommsTodoListTool` and the fleet tool, derived | `one_order_owner` |
| 6 | Branch hygiene | `branches-*.txt` by hand | `CommsWorkBranchesTool` | `one_order_owner` |
| 7 | Strikes and the strike limit | `order.py strike`; `strikes.jsonl` | `Strike` rows, author-declared `StrikeLimit`, one `comms_fire` | `record_plane` |
| 8 | Verdicts on a unit's tool calls | `judge.py ok`, `back`; `verdicts.jsonl` | Order `Verdict`; send-back revises the Goal and records its strike atomically | `record_plane` |
| 8 | Verdicts on a unit's tool calls | `judge.py ok`, `back`; both invoke `CommsVerdictTool` | Order `Verdict`; send-back revises the Goal and records its strike atomically | `record_plane` |
| 9 | Plain words in verdicts and strikes | judge and order checks | `PlainText` | `record_plane` |
| 10 | Refuse soft or vague orders | `order.py` | `OrderText` on every order action | `record_plane` |
| 11 | What a unit did during its order | `tool_watch.py`; `edits.jsonl`; `judge.py show` | `comms_order_work`: both transcript families by `tool_call_id` | `review_queue` |
Expand All @@ -61,7 +61,7 @@
| 28 | Measured load and self-report | `capacity.py` unit count | `commander.capacity.Load.of` reports orders, overdue, edits and forks; self-report comparison unverified | `control_plane` |
| 29 | No handoff of finished, checked work | none; scratch transfer moved P507 after its fix was pushed and checked | Transfer refuses delivered checked work; that owner merges; check is reused | `record_plane` |
| 30 | A proof is bound to the build it ran on | none; screenshots and logs are bare paths | Build run records runtime and Core/Toad revisions; close requires delivered commit | `record_plane` |
| 31 | An order states its intent as a deletion target | none; P520 renamed a decision rather than deleting it | Order owns deletion family/site/count; delivery refuses nonzero or renamed decisions | `record_plane` |
| 31 | An order states its intent as a deletion target | Current order command with a declared FamilyDeletion | CommsTodoCreateTool and CommsOrderDoneTool check the delivered family count | `record_plane` |
| 32 | Where units' time goes, classified | none; activity.log lacks durations and turn boundaries | `UnitTime`: model, build/check/test/shell, dependency/commander waits and idle; classify large waits | `commander_page`, `control_plane` |
| 33 | Order progress beside its deadline; the unit never sees a due time | `order.py board`, `overdue`, `due`, `send`; `rotate.py` | Derived `OrderProgress`; merged work draws no strike; first commit 10 min, later progress 15 min STALL; units see no due time | `one_order_owner`, `control_plane` |
| 34 | Several commanders on one machine and bus, one per project | none; a second commander.json install overwrites the first | Registry commander/project ownership; caller from --from; scoped orders, strikes, load, board, alarms and forks; raw-send guard; duplicate project commander refused | `one_order_owner`, `cutover` |
Expand Down
120 changes: 62 additions & 58 deletions src/agent_comms/candidate_maintenance.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,22 @@
"""Best-effort indexing of successful appends after the outer locks release.

Losing a hint or crashing the daemon leaves the optional index unavailable;
canonical bus and sealed SQL still drive original delivery. Byte positions
include observations as well as messages, without inventing message sequences.
"""
"""Index successful appends after outer locks release, within the owning root's lifetime."""

from __future__ import annotations

import logging
import sqlite3
import threading
from contextlib import ExitStack
from pathlib import Path

from .bus_route_counts import BusRouteCounts
from .coordination_errors import CoordinationError
from .routing import DeliveryMessage
from .wire_log import CurrentSnapshotOpening, WireLog
from .wake_candidate_index import FirstBuild, ProjectionUnavailableError, WakeCandidateIndex
from .wire_log import CurrentSnapshotOpening, WireLog

_LOG = logging.getLogger(__name__)
_MAX_ACTIVE_ROOTS = 32
_guard = threading.Lock()
_pending: dict[Path, tuple[WireLog, int]] = {}
_pending: dict[Path, CandidateMaintenance] = {}


class CandidateMaintenanceFailed(RuntimeError): # noqa: N818 - named worker failure
Expand All @@ -33,72 +27,82 @@ def __init__(self, root: Path) -> None:
super().__init__(f"Candidate maintenance failed for {root}")


def schedule_candidate_catchup(log: WireLog, through_bytes: int) -> None:
"""Prepare writer indexes, then signal their existing maintenance worker."""
if through_bytes <= 0:
raise ValueError("candidate notification needs an actual committed byte position")
BusRouteCounts(log.path).prepare()
root = log.path.parent
with _guard:
current = _pending.get(root)
if current is not None:
_pending[root] = (current[0], max(current[1], through_bytes))
return
if len(_pending) >= _MAX_ACTIVE_ROOTS:
raise CandidateMaintenanceFailed(root)
_pending[root] = (log, through_bytes)
try:
threading.Thread(
target=_drain_candidate,
args=(root,),
name="agent-comms-candidate-maintenance",
daemon=True,
).start()
except RuntimeError as error:
_pending.pop(root, None)
raise CandidateMaintenanceFailed(root) from error


def _retire_candidate(root: Path) -> None:
with _guard:
_pending.pop(root, None)
class CandidateMaintenance(threading.Thread):
"""One root's coalesced cursor and actual worker; retirement joins this owner."""

def __init__(self, log: WireLog, through_bytes: int) -> None:
super().__init__(name="agent-comms-candidate-maintenance", daemon=True)
self.log = log
self.through_bytes = through_bytes

def _drain_candidate(root: Path) -> None:
with ExitStack() as cleanup:
cleanup.callback(_retire_candidate, root)
def run(self) -> None:
root = self.log.path.parent
try:
with _guard:
log, _bytes = _pending[root]
# Source ownership is checked off the producer path. Maintenance cannot
# create the protocol marker or authorize delivery.
with log.checked_read() as source:
with self.log.checked_read() as source:
metadata = source.marker
root_id = metadata.root_id
index = WakeCandidateIndex(log)
routes = BusRouteCounts(log.path)
index = WakeCandidateIndex(self.log)
routes = BusRouteCounts(self.log.path)
while True:
with _guard:
target = _pending[root][1]
target = self.through_bytes
hint = index.notify_committed_append(root_id=root_id, through_bytes=target)
result = index.catch_up_committed_append(
hint, start=FirstBuild()
)
with log.opened_wire_snapshot(opening=CurrentSnapshotOpening()) as snapshot:
result = index.catch_up_committed_append(hint, start=FirstBuild())
with self.log.opened_wire_snapshot(opening=CurrentSnapshotOpening()) as snapshot:
route_result = routes.sync(
snapshot,
lambda record: DeliveryMessage.from_wire(record, root_id=snapshot.metadata.root_id),
lambda record: DeliveryMessage.from_wire(
record, root_id=snapshot.metadata.root_id
),
)
if result.caught_up and route_result.complete:
with _guard:
if _pending[root][1] <= result.checkpoint_bytes:
# Disarm cleanup before a successor can own this root.
cleanup.pop_all()
if self.through_bytes <= result.checkpoint_bytes:
_pending.pop(root)
return
except (
OSError, ValueError, sqlite3.Error, CoordinationError, ProjectionUnavailableError,
OSError,
ValueError,
sqlite3.Error,
CoordinationError,
ProjectionUnavailableError,
) as error:
failure = CandidateMaintenanceFailed(root)
_LOG.exception("%s", failure)
raise failure from error
finally:
with _guard:
if root in _pending and _pending[root] is self:
_pending.pop(root)


def schedule_candidate_catchup(log: WireLog, through_bytes: int) -> None:
"""Prepare writer indexes, then coalesce into the root's actual worker."""
if through_bytes <= 0:
raise ValueError("candidate notification needs an actual committed byte position")
BusRouteCounts(log.path).prepare()
root = log.path.parent
with _guard:
if root in _pending:
worker = _pending[root]
worker.through_bytes = max(worker.through_bytes, through_bytes)
return
if len(_pending) >= _MAX_ACTIVE_ROOTS:
raise CandidateMaintenanceFailed(root)
worker = CandidateMaintenance(log, through_bytes)
_pending[root] = worker
try:
worker.start()
except RuntimeError as error:
_pending.pop(root)
raise CandidateMaintenanceFailed(root) from error


def drain_candidate_catchup(root: Path) -> None:
"""A stopped root cannot be renamed or removed while its worker still writes."""
while True:
with _guard:
if root not in _pending:
return
worker = _pending[root]
worker.join()
2 changes: 2 additions & 0 deletions src/agent_comms/check_root.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import ClassVar

from .attempt_recovery import RecoveryMonitorCapability
from .candidate_maintenance import drain_candidate_catchup
from .child_process import NoProcess
from .cli_result import CliResult
from .comms import Comms
Expand Down Expand Up @@ -74,6 +75,7 @@ def open(self) -> Iterator[Comms]:
):
copied.owners.stop(thread.name)
finally:
drain_candidate_catchup(root)
RecoveryMonitorCapability.retire_check_root(root)

def _populate(self, copied: Comms) -> None:
Expand Down
7 changes: 3 additions & 4 deletions src/agent_comms/commander/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,7 @@ def problem(self, order_file, unit, due, where, disk=False,
def close(self, pid, works, artifact=''):
"""Core owns closing: the installed evidence, the artifact and the no-open-order rule live on the Todo."""
evidence = artifact or self.comms.goals.todos.get(pid).order.closing_artifact()
ThreadCaller(self.commander).invoke(CommsProblemCloseTool(problem=pid,
works=works, artifact=evidence), self.comms)
self.call(CommsProblemCloseTool(problem=pid, works=works, artifact=evidence))
self.write_board()
print(f'{pid} closed')

Expand Down Expand Up @@ -730,8 +729,8 @@ def due(self, pid: str, at: str, why: str) -> None:
moved = RecordedEventTime(time.time()).at_clock(at)
self.call(CommsChangeProblemTool(problem=pid, operation=AssignTodo(
todo.revision, todo.assignment, moved, todo.order, todo.goal)))
self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} due '
f'{todo.deadline.clock()} moved to {moved.clock()}): {why}', 'Deadline',
self.on_record(f'{time.strftime("%Y-%m-%d %H:%M")} {todo.holder} ({pid} order {todo.order.id} '
f'deadline changed): {why}', 'Deadline',
VerdictAudience.of(todo.held().owner, todo.goal))
print(f'{pid}: due {moved.clock()}')

Expand Down
5 changes: 3 additions & 2 deletions src/agent_comms/commander_parity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar
Expand Down Expand Up @@ -43,6 +43,7 @@ class ParityRow(
current: ClassVar[str]
native: ClassVar[str]
phases: ClassVar[tuple[type[PlanPhase], ...]]
comparison: ClassVar[Callable[[], ParityComparison]] = UnverifiedParity

@classmethod
def compare(cls, todos: Iterable[Todo]) -> ParityComparison:
Expand All @@ -51,7 +52,7 @@ def compare(cls, todos: Iterable[Todo]) -> ParityComparison:
@classmethod
def verify(cls) -> ParityComparison:
"""Run this declared workflow now; a run is not durable cutover authority."""
return UnverifiedParity()
return cls.comparison()

@classmethod
def rows(cls) -> tuple[type[ParityRow], ...]:
Expand Down
Loading