mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 20:27:56 +00:00
fix: preserve service readiness truth
Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
a849c9a6b5
commit
06339bb73c
2 changed files with 81 additions and 3 deletions
|
|
@ -32,6 +32,7 @@ from ouroboros.utils import append_jsonl, utc_now_iso
|
||||||
from ouroboros.workspace_executor import executor_ref_from_ctx
|
from ouroboros.workspace_executor import executor_ref_from_ctx
|
||||||
from ouroboros.workspace_executor import kill_all_services as executor_kill_all_services
|
from ouroboros.workspace_executor import kill_all_services as executor_kill_all_services
|
||||||
from ouroboros.workspace_executor import map_host_path as executor_map_host_path
|
from ouroboros.workspace_executor import map_host_path as executor_map_host_path
|
||||||
|
from ouroboros.workspace_executor import _read_local_service_marker
|
||||||
from ouroboros.workspace_executor import service_logs as executor_service_logs
|
from ouroboros.workspace_executor import service_logs as executor_service_logs
|
||||||
from ouroboros.workspace_executor import service_status as executor_service_status
|
from ouroboros.workspace_executor import service_status as executor_service_status
|
||||||
from ouroboros.workspace_executor import start_service as executor_start_service
|
from ouroboros.workspace_executor import start_service as executor_start_service
|
||||||
|
|
@ -52,6 +53,10 @@ class ServiceRecord:
|
||||||
started_at: float = field(default_factory=time.time)
|
started_at: float = field(default_factory=time.time)
|
||||||
readiness: Dict[str, Any] = field(default_factory=dict)
|
readiness: Dict[str, Any] = field(default_factory=dict)
|
||||||
ready: bool = False
|
ready: bool = False
|
||||||
|
ready_observed_at: str = ""
|
||||||
|
readiness_log_offset: int = 0
|
||||||
|
readiness_log_carry: bytes = b""
|
||||||
|
readiness_log_identity: tuple[int, int] | None = None
|
||||||
outputs: List[str] = field(default_factory=list)
|
outputs: List[str] = field(default_factory=list)
|
||||||
cwd_root: str = ""
|
cwd_root: str = ""
|
||||||
cwd_base: str = ""
|
cwd_base: str = ""
|
||||||
|
|
@ -353,15 +358,26 @@ def _refresh_ready(record: ServiceRecord) -> bool:
|
||||||
if record.proc.poll() is not None:
|
if record.proc.poll() is not None:
|
||||||
record.ready = False
|
record.ready = False
|
||||||
return False
|
return False
|
||||||
|
if record.ready:
|
||||||
|
return True
|
||||||
readiness = record.readiness or {}
|
readiness = record.readiness or {}
|
||||||
contains = str(readiness.get("stdout_contains") or readiness.get("log_contains") or "").strip()
|
contains = str(readiness.get("stdout_contains") or readiness.get("log_contains") or "").strip()
|
||||||
if not contains:
|
if not contains:
|
||||||
record.ready = True
|
record.ready = True
|
||||||
|
record.ready_observed_at = record.ready_observed_at or utc_now_iso()
|
||||||
return True
|
return True
|
||||||
record.ready = contains in _tail(record.log_path, 20_000)
|
record.ready = _readiness_marker_observed(record, contains)
|
||||||
|
if record.ready:
|
||||||
|
record.ready_observed_at = record.ready_observed_at or utc_now_iso()
|
||||||
return record.ready
|
return record.ready
|
||||||
|
|
||||||
|
|
||||||
|
def _readiness_marker_observed(record: ServiceRecord, marker: str) -> bool:
|
||||||
|
"""Scan only unseen service-log bytes without turning the display tail into truth."""
|
||||||
|
|
||||||
|
return _read_local_service_marker(record, record.log_path, marker)
|
||||||
|
|
||||||
|
|
||||||
def _start_service(
|
def _start_service(
|
||||||
ctx: ToolContext,
|
ctx: ToolContext,
|
||||||
cmd: List[str],
|
cmd: List[str],
|
||||||
|
|
@ -543,6 +559,7 @@ def _status_payload(record: ServiceRecord) -> Dict[str, Any]:
|
||||||
"pgid": record.pgid,
|
"pgid": record.pgid,
|
||||||
"state": state,
|
"state": state,
|
||||||
"ready": bool(record.ready),
|
"ready": bool(record.ready),
|
||||||
|
"ready_observed_at": record.ready_observed_at or None,
|
||||||
"returncode": rc,
|
"returncode": rc,
|
||||||
"uptime_sec": round(max(0.0, time.time() - record.started_at), 3),
|
"uptime_sec": round(max(0.0, time.time() - record.started_at), 3),
|
||||||
"cwd": record.cwd,
|
"cwd": record.cwd,
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,11 @@ def test_executor_local_service_lifecycle_hides_private_snapshot(tmp_path, monke
|
||||||
"start_service",
|
"start_service",
|
||||||
{
|
{
|
||||||
"name": "svc",
|
"name": "svc",
|
||||||
"cmd": [sys.executable, "-c", "import time; print('READY', flush=True); time.sleep(30)"],
|
"cmd": [
|
||||||
|
sys.executable,
|
||||||
|
"-c",
|
||||||
|
"import os,time; os.write(1, b'READY\\n' + b'x' * 25000); time.sleep(30)",
|
||||||
|
],
|
||||||
"readiness": {"log_contains": "READY", "timeout_sec": 5},
|
"readiness": {"log_contains": "READY", "timeout_sec": 5},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -582,13 +586,70 @@ def test_executor_local_service_lifecycle_hides_private_snapshot(tmp_path, monke
|
||||||
stopped = json.loads(stopped_raw)
|
stopped = json.loads(stopped_raw)
|
||||||
|
|
||||||
assert started["ready"] is True
|
assert started["ready"] is True
|
||||||
|
assert started["ready_observed_at"]
|
||||||
assert status["state"] == "running"
|
assert status["state"] == "running"
|
||||||
assert "READY" in logs["tail"]
|
assert "READY" not in logs["tail"]
|
||||||
|
assert "x" in logs["tail"]
|
||||||
assert stopped["state"] == "stopped"
|
assert stopped["state"] == "stopped"
|
||||||
assert "_before_outputs" not in stopped_raw
|
assert "_before_outputs" not in stopped_raw
|
||||||
assert bootstrap_calls
|
assert bootstrap_calls
|
||||||
|
|
||||||
|
|
||||||
|
def test_executor_readiness_scans_before_large_log_suffix(tmp_path, monkeypatch):
|
||||||
|
import ouroboros.workspace_executor as workspace_executor
|
||||||
|
|
||||||
|
log_path = tmp_path / "executor-service.log"
|
||||||
|
log_path.write_bytes(b"READY\n" + (b"x" * 25_000))
|
||||||
|
record = SimpleNamespace(
|
||||||
|
executor=SimpleNamespace(kind="local"),
|
||||||
|
backend_log_path=str(log_path),
|
||||||
|
local_proc=SimpleNamespace(poll=lambda: None),
|
||||||
|
ready=False,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(workspace_executor.time, "sleep", lambda _seconds: None)
|
||||||
|
|
||||||
|
workspace_executor._wait_readiness(
|
||||||
|
record,
|
||||||
|
{"log_contains": "READY", "timeout_sec": 0.05},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert record.ready is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_executor_terminal_payload_clears_readiness(tmp_path):
|
||||||
|
import ouroboros.workspace_executor as workspace_executor
|
||||||
|
|
||||||
|
record = SimpleNamespace(
|
||||||
|
service_id="task:svc",
|
||||||
|
name="svc",
|
||||||
|
task_id="task",
|
||||||
|
executor=SimpleNamespace(
|
||||||
|
executor_id="local-service",
|
||||||
|
kind="local",
|
||||||
|
network="host",
|
||||||
|
),
|
||||||
|
backend_pid="4321",
|
||||||
|
backend_cwd="/workspace",
|
||||||
|
host_cwd=tmp_path,
|
||||||
|
cwd_root="active_workspace",
|
||||||
|
cwd_base=str(tmp_path),
|
||||||
|
cwd_source="active_workspace",
|
||||||
|
skill_name="",
|
||||||
|
cmd=["service"],
|
||||||
|
outputs=[],
|
||||||
|
keep_alive=False,
|
||||||
|
backend_log_path=str(tmp_path / "service.log"),
|
||||||
|
started_at=workspace_executor.time.time(),
|
||||||
|
ready=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = workspace_executor._service_payload(record, state="exited")
|
||||||
|
|
||||||
|
assert payload["state"] == "exited"
|
||||||
|
assert payload["ready"] is False
|
||||||
|
assert record.ready is False
|
||||||
|
|
||||||
|
|
||||||
def test_start_service_with_executor_ref_uses_local_for_unmapped_task_drive_cwd(tmp_path, monkeypatch):
|
def test_start_service_with_executor_ref_uses_local_for_unmapped_task_drive_cwd(tmp_path, monkeypatch):
|
||||||
import ouroboros.safety as safety_mod
|
import ouroboros.safety as safety_mod
|
||||||
from ouroboros.tool_access import resource_root_path
|
from ouroboros.tool_access import resource_root_path
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue