Add MAIN-to-RX acknowledgement channel

This commit is contained in:
LittleSam129
2026-09-22 15:09:37 +03:00
parent a9317989a2
commit 168e03d9e7
9 changed files with 1330 additions and 30 deletions

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
import argparse
from dataclasses import asdict, dataclass
from dataclasses import asdict, dataclass, replace
from datetime import datetime, timezone
import json
import os
@@ -33,6 +33,14 @@ from tools.rf_handoff_channel import ( # noqa: E402
load_result,
result_digest,
)
from tools.tx_handoff_channel import ( # noqa: E402
CHANNEL_COMPLETE_OBSERVED,
CHANNEL_READY_OBSERVED,
RX_READY_OBSERVED,
TX_AUTHORIZATION_CREATED,
GitAcknowledgementChannel,
TxHandoffAcknowledgement,
)
DEFAULT_STATE_PATH = ROOT / ".local" / "tx_agent_watcher_state.json"
@@ -67,6 +75,13 @@ class TxExecutor(Protocol):
) -> Path: ...
class AcknowledgementPublisher(Protocol):
def publish(
self,
acknowledgement: TxHandoffAcknowledgement,
) -> object: ...
@dataclass
class TxWatcherState:
state_version: int = STATE_VERSION
@@ -410,6 +425,45 @@ def migrate_state_cursor(
return True
def acknowledgement_for_result(
result: RxHandoffResult,
digest: str,
result_head: str,
status: str,
*,
observed_at: datetime,
main_watcher_pid: int,
) -> TxHandoffAcknowledgement | None:
"""Build an acknowledgement only for an event handled successfully."""
acknowledgement_state: str | None = None
rf_tx_authorized = False
if status == "CHANNEL_TEST_READY":
acknowledgement_state = CHANNEL_READY_OBSERVED
elif status == "CHANNEL_TEST_COMPLETE":
acknowledgement_state = CHANNEL_COMPLETE_OBSERVED
elif status == "RED_APPROVAL_BLOCKED":
acknowledgement_state = RX_READY_OBSERVED
elif status == "TX_AUTHORIZATION_PREPARED":
acknowledgement_state = TX_AUTHORIZATION_CREATED
rf_tx_authorized = True
if acknowledgement_state is None:
return None
return TxHandoffAcknowledgement(
schema_version=1,
task_id=result.task_id,
state=acknowledgement_state,
source_rx_commit=result_head,
source_rx_digest=digest,
source_head=result.source_head,
observed_at=observed_at.astimezone(timezone.utc).isoformat(),
main_watcher_pid=main_watcher_pid,
rf_tx_authorized=rf_tx_authorized,
rf_tx_started=False,
rf_tx=0,
)
def process_history_cycle(
source: ResultSource,
state: TxWatcherState,
@@ -423,6 +477,8 @@ def process_history_cycle(
expected_source_head: str | None = None,
max_age_seconds: float = 30.0,
min_remaining_seconds: float = 10.0,
acknowledgement_publisher: AcknowledgementPublisher | None = None,
main_watcher_pid: int | None = None,
) -> list[tuple[str, str]]:
"""Process every new result commit in chronological order."""
@@ -432,11 +488,12 @@ def process_history_cycle(
outcomes: list[tuple[str, str]] = []
for commit in commits:
result, digest, result_head = source.read_commit(commit)
status, state = handle_result(
candidate_state = replace(state)
status, candidate_state = handle_result(
result,
digest,
result_head,
state,
candidate_state,
source,
executor,
now_function=now_function,
@@ -447,6 +504,26 @@ def process_history_cycle(
max_age_seconds=max_age_seconds,
min_remaining_seconds=min_remaining_seconds,
)
acknowledgement = acknowledgement_for_result(
result,
digest,
result_head,
status,
observed_at=now_function(),
main_watcher_pid=main_watcher_pid or os.getpid(),
)
if acknowledgement is not None and acknowledgement_publisher is not None:
publication = acknowledgement_publisher.publish(acknowledgement)
output_function(
"TX ACKNOWLEDGEMENT PUBLISH = "
f"{getattr(publication, 'status', 'PUBLISHED')}"
)
output_function(
"TX ACKNOWLEDGEMENT HEAD = "
f"{getattr(publication, 'commit', 'UNKNOWN')}"
)
for field_name in TxWatcherState.__dataclass_fields__:
setattr(state, field_name, getattr(candidate_state, field_name))
state.last_seen_commit = result_head
state.last_processed_rx_node_commit = result_head
state.state_version = STATE_VERSION
@@ -481,6 +558,7 @@ def run_watcher(arguments: argparse.Namespace) -> int:
stop_path.unlink(missing_ok=True)
executor = ManualTxExecutor(arguments.outbox.resolve())
acknowledgement_publisher: AcknowledgementPublisher | None = None
def input_function(prompt: str) -> str:
if arguments.approval == "authorize":
@@ -511,6 +589,11 @@ def run_watcher(arguments: argparse.Namespace) -> int:
remote=arguments.remote,
branch=arguments.result_branch,
)
acknowledgement_publisher = GitAcknowledgementChannel(
repository,
remote=arguments.remote,
branch=arguments.acknowledgement_branch,
)
if not _HEAD_RE.fullmatch(expected_source_head):
raise ValueError(
"expected source HEAD must be a full lowercase Git hash"
@@ -534,6 +617,8 @@ def run_watcher(arguments: argparse.Namespace) -> int:
expected_source_head=expected_source_head,
max_age_seconds=arguments.max_age,
min_remaining_seconds=arguments.min_remaining,
acknowledgement_publisher=acknowledgement_publisher,
main_watcher_pid=os.getpid(),
)
if arguments.once or arguments.dry_run:
break
@@ -569,6 +654,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--remote", default="origin")
parser.add_argument("--source-branch", default="video-development")
parser.add_argument("--result-branch", default="rx-node")
parser.add_argument("--acknowledgement-branch", default="tx-node")
parser.add_argument("--expected-source-head")
parser.add_argument("--poll-interval", type=float, default=5.0)
parser.add_argument("--max-age", type=float, default=30.0)