forked from Rockachopa/Timmy-time-dashboard
Compare commits
1 Commits
claude/iss
...
fix/loop-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f13f540b3 |
@@ -30,7 +30,7 @@ IDLE_STATE_FILE = REPO_ROOT / ".loop" / "idle_state.json"
|
||||
CYCLE_RESULT_FILE = REPO_ROOT / ".loop" / "cycle_result.json"
|
||||
TOKEN_FILE = Path.home() / ".hermes" / "gitea_token"
|
||||
|
||||
GITEA_API = os.environ.get("GITEA_API", "http://localhost:3000/api/v1")
|
||||
GITEA_API = os.environ.get("GITEA_API", "http://143.198.27.163:3000/api/v1")
|
||||
REPO_SLUG = os.environ.get("REPO_SLUG", "rockachopa/Timmy-time-dashboard")
|
||||
|
||||
# Default cycle duration in seconds (5 min); stale threshold = 2× this
|
||||
|
||||
@@ -20,7 +20,7 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# ── Config ──────────────────────────────────────────────────────────────
|
||||
GITEA_API = os.environ.get("GITEA_API", "http://localhost:3000/api/v1")
|
||||
GITEA_API = os.environ.get("GITEA_API", "http://143.198.27.163:3000/api/v1")
|
||||
REPO_SLUG = os.environ.get("REPO_SLUG", "rockachopa/Timmy-time-dashboard")
|
||||
TOKEN_FILE = Path.home() / ".hermes" / "gitea_token"
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -327,7 +327,31 @@ def run_triage() -> list[dict]:
|
||||
not_ready = [s for s in scored if not s["ready"]]
|
||||
|
||||
QUEUE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
QUEUE_FILE.write_text(json.dumps(ready, indent=2) + "\n")
|
||||
backup_file = QUEUE_FILE.with_suffix(".json.bak")
|
||||
|
||||
# Backup existing queue before overwriting
|
||||
if QUEUE_FILE.exists():
|
||||
try:
|
||||
backup_file.write_text(QUEUE_FILE.read_text())
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Write and validate
|
||||
queue_json = json.dumps(ready, indent=2) + "\n"
|
||||
QUEUE_FILE.write_text(queue_json)
|
||||
|
||||
# Validate by re-reading — restore backup on corruption
|
||||
try:
|
||||
validated = json.loads(QUEUE_FILE.read_text())
|
||||
if not isinstance(validated, list):
|
||||
raise ValueError("queue.json is not a list")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
print(f"[triage] ERROR: queue.json validation failed: {e}", file=sys.stderr)
|
||||
if backup_file.exists():
|
||||
print("[triage] Restoring from backup", file=sys.stderr)
|
||||
QUEUE_FILE.write_text(backup_file.read_text())
|
||||
else:
|
||||
QUEUE_FILE.write_text("[]\n")
|
||||
|
||||
# Write retro entry
|
||||
retro_entry = {
|
||||
|
||||
@@ -7,7 +7,7 @@ without a running game server.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from infrastructure.world.interface import WorldInterface
|
||||
@@ -81,9 +81,7 @@ class MockWorldAdapter(WorldInterface):
|
||||
|
||||
def act(self, command: CommandInput) -> ActionResult:
|
||||
logger.debug("MockWorldAdapter.act(%s)", command.action)
|
||||
self.action_log.append(
|
||||
_ActionLog(command=command, timestamp=datetime.now(UTC))
|
||||
)
|
||||
self.action_log.append(_ActionLog(command=command, timestamp=datetime.now(UTC)))
|
||||
return ActionResult(
|
||||
status=ActionStatus.SUCCESS,
|
||||
message=f"Mock executed: {command.action}",
|
||||
@@ -92,8 +90,10 @@ class MockWorldAdapter(WorldInterface):
|
||||
|
||||
def speak(self, message: str, target: str | None = None) -> None:
|
||||
logger.debug("MockWorldAdapter.speak(%r, target=%r)", message, target)
|
||||
self.speech_log.append({
|
||||
"message": message,
|
||||
"target": target,
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
})
|
||||
self.speech_log.append(
|
||||
{
|
||||
"message": message,
|
||||
"target": target,
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -35,14 +35,10 @@ class TES3MPWorldAdapter(WorldInterface):
|
||||
# -- lifecycle ---------------------------------------------------------
|
||||
|
||||
def connect(self) -> None:
|
||||
raise NotImplementedError(
|
||||
"TES3MPWorldAdapter.connect() — wire up TES3MP server socket"
|
||||
)
|
||||
raise NotImplementedError("TES3MPWorldAdapter.connect() — wire up TES3MP server socket")
|
||||
|
||||
def disconnect(self) -> None:
|
||||
raise NotImplementedError(
|
||||
"TES3MPWorldAdapter.disconnect() — close TES3MP server socket"
|
||||
)
|
||||
raise NotImplementedError("TES3MPWorldAdapter.disconnect() — close TES3MP server socket")
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
@@ -51,9 +47,7 @@ class TES3MPWorldAdapter(WorldInterface):
|
||||
# -- core contract (stubs) ---------------------------------------------
|
||||
|
||||
def observe(self) -> PerceptionOutput:
|
||||
raise NotImplementedError(
|
||||
"TES3MPWorldAdapter.observe() — poll TES3MP for player/NPC state"
|
||||
)
|
||||
raise NotImplementedError("TES3MPWorldAdapter.observe() — poll TES3MP for player/NPC state")
|
||||
|
||||
def act(self, command: CommandInput) -> ActionResult:
|
||||
raise NotImplementedError(
|
||||
@@ -61,6 +55,4 @@ class TES3MPWorldAdapter(WorldInterface):
|
||||
)
|
||||
|
||||
def speak(self, message: str, target: str | None = None) -> None:
|
||||
raise NotImplementedError(
|
||||
"TES3MPWorldAdapter.speak() — send chat message via TES3MP"
|
||||
)
|
||||
raise NotImplementedError("TES3MPWorldAdapter.speak() — send chat message via TES3MP")
|
||||
|
||||
@@ -27,14 +27,14 @@ class WorldInterface(ABC):
|
||||
|
||||
# -- lifecycle (optional overrides) ------------------------------------
|
||||
|
||||
def connect(self) -> None:
|
||||
def connect(self) -> None: # noqa: B027
|
||||
"""Establish connection to the game world.
|
||||
|
||||
Default implementation is a no-op. Override to open sockets,
|
||||
authenticate, etc.
|
||||
"""
|
||||
|
||||
def disconnect(self) -> None:
|
||||
def disconnect(self) -> None: # noqa: B027
|
||||
"""Tear down the connection.
|
||||
|
||||
Default implementation is a no-op.
|
||||
|
||||
@@ -10,10 +10,10 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class ActionStatus(str, Enum):
|
||||
class ActionStatus(StrEnum):
|
||||
"""Outcome of an action dispatched to the world."""
|
||||
|
||||
SUCCESS = "success"
|
||||
|
||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from loop.phase1_gather import gather
|
||||
@@ -32,6 +32,7 @@ logger = logging.getLogger(__name__)
|
||||
# Cycle log entry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class CycleRecord:
|
||||
"""One observe → reason → act → reflect cycle."""
|
||||
@@ -50,6 +51,7 @@ class CycleRecord:
|
||||
# Heartbeat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Heartbeat:
|
||||
"""Manages the recurring cognitive loop with optional world adapter.
|
||||
|
||||
@@ -268,14 +270,17 @@ class Heartbeat:
|
||||
try:
|
||||
from infrastructure.ws_manager.handler import ws_manager
|
||||
|
||||
await ws_manager.broadcast("heartbeat.cycle", {
|
||||
"cycle_id": record.cycle_id,
|
||||
"timestamp": record.timestamp,
|
||||
"action": record.action_taken,
|
||||
"action_status": record.action_status,
|
||||
"reasoning_summary": record.reasoning_summary[:300],
|
||||
"observation": record.observation,
|
||||
"duration_ms": record.duration_ms,
|
||||
})
|
||||
await ws_manager.broadcast(
|
||||
"heartbeat.cycle",
|
||||
{
|
||||
"cycle_id": record.cycle_id,
|
||||
"timestamp": record.timestamp,
|
||||
"action": record.action_taken,
|
||||
"action_status": record.action_status,
|
||||
"reasoning_summary": record.reasoning_summary[:300],
|
||||
"observation": record.observation,
|
||||
"duration_ms": record.duration_ms,
|
||||
},
|
||||
)
|
||||
except (ImportError, AttributeError, ConnectionError, RuntimeError) as exc:
|
||||
logger.debug("Heartbeat broadcast skipped: %s", exc)
|
||||
|
||||
@@ -10,7 +10,6 @@ from infrastructure.world.types import (
|
||||
PerceptionOutput,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type construction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import pytest
|
||||
|
||||
from infrastructure.world.adapters.mock import MockWorldAdapter
|
||||
from infrastructure.world.interface import WorldInterface
|
||||
from infrastructure.world.registry import AdapterRegistry
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ Acceptance criteria:
|
||||
- WebSocket broadcasts include current action and reasoning summary
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -81,6 +80,7 @@ class TestHeartbeatWithAdapter:
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_cycle_callback(self, mock_adapter):
|
||||
received = []
|
||||
|
||||
async def callback(record):
|
||||
received.append(record)
|
||||
|
||||
@@ -145,9 +145,7 @@ class TestHeartbeatBroadcast:
|
||||
) as mock_ws:
|
||||
mock_ws.broadcast = AsyncMock()
|
||||
# Patch the import inside heartbeat
|
||||
with patch(
|
||||
"infrastructure.ws_manager.handler.ws_manager"
|
||||
) as ws_mod:
|
||||
with patch("infrastructure.ws_manager.handler.ws_manager") as ws_mod:
|
||||
ws_mod.broadcast = AsyncMock()
|
||||
hb = Heartbeat(world=mock_adapter)
|
||||
await hb.run_once()
|
||||
|
||||
Reference in New Issue
Block a user