opencode-agent-hub 1.3.2__tar.gz → 1.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/PKG-INFO +1 -1
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/pyproject.toml +1 -1
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/src/opencode_agent_hub/__init__.py +1 -1
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/src/opencode_agent_hub/daemon.py +103 -14
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/.gitignore +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/LICENSE +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/README.md +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/coordinator/AGENTS.md +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/coordinator/opencode.json +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/launchd/com.xnoto.agent-hub-daemon.plist +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/aur/PKGBUILD +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/changelog +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/control +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/copyright +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/docs +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/install +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/postinst +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/rules +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/source/format +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/rpm/opencode-agent-hub.spec +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/systemd/agent-hub-daemon.service +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/src/opencode_agent_hub/py.typed +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/src/opencode_agent_hub/watch.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/__init__.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_config.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_coordinator.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_coordinator_cost.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_orientation_retry.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_placeholder.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_rate_limiting.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_session_agents.py +0 -0
- {opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/tests/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-agent-hub
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
4
4
|
Summary: Multi-agent coordination daemon and tools for OpenCode
|
|
5
5
|
Project-URL: Homepage, https://github.com/xnoto/opencode-agent-hub
|
|
6
6
|
Project-URL: Repository, https://github.com/xnoto/opencode-agent-hub
|
|
@@ -78,6 +78,41 @@ import requests
|
|
|
78
78
|
from watchdog.events import FileSystemEvent, FileSystemEventHandler
|
|
79
79
|
from watchdog.observers import Observer
|
|
80
80
|
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# Atomic File Operations
|
|
83
|
+
# =============================================================================
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def atomic_write_json(path: Path, data: Any, indent: int | None = 2) -> None:
|
|
87
|
+
"""Write JSON to file atomically using temp file + rename.
|
|
88
|
+
|
|
89
|
+
This prevents readers from seeing partial/empty files during writes.
|
|
90
|
+
POSIX guarantees rename() is atomic - readers see old or new, never partial.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: Target file path
|
|
94
|
+
data: JSON-serializable data
|
|
95
|
+
indent: JSON indentation (None for compact, 2 for pretty-print)
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
OSError: If write fails
|
|
99
|
+
"""
|
|
100
|
+
json_str = json.dumps(data, indent=indent)
|
|
101
|
+
temp_path = path.with_suffix(f".tmp.{os.getpid()}")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Write to temp file first
|
|
105
|
+
temp_path.write_text(json_str, encoding="utf-8")
|
|
106
|
+
|
|
107
|
+
# Atomic rename - readers see either old or new, never partial
|
|
108
|
+
temp_path.rename(path)
|
|
109
|
+
except Exception:
|
|
110
|
+
# Clean up temp file on any error
|
|
111
|
+
with suppress(OSError):
|
|
112
|
+
temp_path.unlink()
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
|
|
81
116
|
# =============================================================================
|
|
82
117
|
# Configuration
|
|
83
118
|
# =============================================================================
|
|
@@ -684,7 +719,7 @@ def save_oriented_sessions() -> None:
|
|
|
684
719
|
"""Save oriented sessions to disk."""
|
|
685
720
|
try:
|
|
686
721
|
AGENT_HUB_DIR.mkdir(parents=True, exist_ok=True)
|
|
687
|
-
ORIENTED_SESSIONS_FILE
|
|
722
|
+
atomic_write_json(ORIENTED_SESSIONS_FILE, list(ORIENTED_SESSIONS), indent=None)
|
|
688
723
|
except OSError as e:
|
|
689
724
|
log.warning(f"Failed to save oriented sessions: {e}")
|
|
690
725
|
|
|
@@ -693,7 +728,7 @@ def save_session_agents() -> None:
|
|
|
693
728
|
"""Save session-to-agent mapping to disk."""
|
|
694
729
|
try:
|
|
695
730
|
AGENT_HUB_DIR.mkdir(parents=True, exist_ok=True)
|
|
696
|
-
SESSION_AGENTS_FILE
|
|
731
|
+
atomic_write_json(SESSION_AGENTS_FILE, SESSION_AGENTS, indent=2)
|
|
697
732
|
except OSError as e:
|
|
698
733
|
log.warning(f"Failed to save session agents: {e}")
|
|
699
734
|
|
|
@@ -784,19 +819,73 @@ def record_message_sent(agent_id: str) -> None:
|
|
|
784
819
|
|
|
785
820
|
|
|
786
821
|
def load_agents() -> dict[str, dict[str, Any]]:
|
|
787
|
-
"""Load all registered agents, keyed by agent ID.
|
|
822
|
+
"""Load all registered agents, keyed by agent ID.
|
|
823
|
+
|
|
824
|
+
Retries on JSON decode errors to handle transient file states
|
|
825
|
+
when external writers are updating agent files.
|
|
826
|
+
"""
|
|
788
827
|
agents: dict[str, dict[str, Any]] = {}
|
|
789
828
|
if not AGENTS_DIR.exists():
|
|
790
829
|
return agents
|
|
791
830
|
for f in AGENTS_DIR.glob("*.json"):
|
|
792
|
-
|
|
793
|
-
|
|
831
|
+
agent = _load_agent_with_retry(f)
|
|
832
|
+
if agent:
|
|
794
833
|
agents[agent["id"]] = agent
|
|
795
|
-
except (json.JSONDecodeError, KeyError) as e:
|
|
796
|
-
log.warning(f"Failed to load agent {f}: {e}")
|
|
797
834
|
return agents
|
|
798
835
|
|
|
799
836
|
|
|
837
|
+
def _load_agent_with_retry(
|
|
838
|
+
path: Path, max_retries: int = 3, base_delay: float = 0.05
|
|
839
|
+
) -> dict[str, Any] | None:
|
|
840
|
+
"""Load a single agent file with retry for transient errors.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
path: Path to agent JSON file
|
|
844
|
+
max_retries: Maximum number of retry attempts
|
|
845
|
+
base_delay: Initial delay between retries (doubles each attempt)
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
Agent dict or None if loading failed after all retries
|
|
849
|
+
"""
|
|
850
|
+
for attempt in range(max_retries):
|
|
851
|
+
try:
|
|
852
|
+
content = path.read_text()
|
|
853
|
+
if not content.strip():
|
|
854
|
+
# File is empty - writer hasn't finished yet
|
|
855
|
+
if attempt < max_retries - 1:
|
|
856
|
+
delay = base_delay * (2**attempt)
|
|
857
|
+
log.debug(
|
|
858
|
+
f"Agent file {path.name} empty, retrying in {delay:.0f}ms (attempt {attempt + 1}/{max_retries})"
|
|
859
|
+
)
|
|
860
|
+
time.sleep(delay)
|
|
861
|
+
continue
|
|
862
|
+
log.warning(f"Agent file {path.name} is empty after {max_retries} attempts")
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
agent = json.loads(content)
|
|
866
|
+
if "id" not in agent:
|
|
867
|
+
log.warning(f"Agent file {path.name} missing 'id' field")
|
|
868
|
+
return None
|
|
869
|
+
return agent
|
|
870
|
+
|
|
871
|
+
except json.JSONDecodeError:
|
|
872
|
+
if attempt < max_retries - 1:
|
|
873
|
+
delay = base_delay * (2**attempt)
|
|
874
|
+
log.debug(
|
|
875
|
+
f"Failed to parse agent {path.name}, retrying in {delay:.0f}ms (attempt {attempt + 1}/{max_retries})"
|
|
876
|
+
)
|
|
877
|
+
time.sleep(delay)
|
|
878
|
+
else:
|
|
879
|
+
log.warning(
|
|
880
|
+
f"Failed to load agent {path.name}: JSON decode error after {max_retries} attempts"
|
|
881
|
+
)
|
|
882
|
+
except OSError as e:
|
|
883
|
+
log.warning(f"Failed to read agent {path.name}: {e}")
|
|
884
|
+
return None
|
|
885
|
+
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
|
|
800
889
|
def is_agent_active(agent: dict[str, Any]) -> bool:
|
|
801
890
|
"""Check if agent has been seen within the stale threshold."""
|
|
802
891
|
last_seen = cast(float, agent.get("lastSeen", 0))
|
|
@@ -825,7 +914,7 @@ def save_thread(thread: dict[str, Any]) -> None:
|
|
|
825
914
|
"""Save a thread."""
|
|
826
915
|
THREADS_DIR.mkdir(parents=True, exist_ok=True)
|
|
827
916
|
path = THREADS_DIR / f"{thread['id']}.json"
|
|
828
|
-
path
|
|
917
|
+
atomic_write_json(path, thread, indent=2)
|
|
829
918
|
|
|
830
919
|
|
|
831
920
|
def create_thread(msg: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1793,10 +1882,10 @@ def start_coordinator() -> bool:
|
|
|
1793
1882
|
"status": "active",
|
|
1794
1883
|
"lastSeen": int(time.time() * 1000),
|
|
1795
1884
|
}
|
|
1796
|
-
# Write agent file directly
|
|
1885
|
+
# Write agent file directly (atomic write)
|
|
1797
1886
|
AGENTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
1798
1887
|
agent_file = AGENTS_DIR / "coordinator.json"
|
|
1799
|
-
agent_file
|
|
1888
|
+
atomic_write_json(agent_file, coordinator_agent, indent=2)
|
|
1800
1889
|
log.info("Registered coordinator as agent 'coordinator'")
|
|
1801
1890
|
|
|
1802
1891
|
# Send bootstrap prompt synchronously before waiting for READY.
|
|
@@ -2246,10 +2335,10 @@ def get_or_create_agent_for_directory(
|
|
|
2246
2335
|
"autoCreated": True,
|
|
2247
2336
|
}
|
|
2248
2337
|
|
|
2249
|
-
# Save to disk
|
|
2338
|
+
# Save to disk (atomic write to prevent readers seeing partial files)
|
|
2250
2339
|
agent_file = AGENTS_DIR / f"{agent_id}.json"
|
|
2251
2340
|
try:
|
|
2252
|
-
agent_file
|
|
2341
|
+
atomic_write_json(agent_file, agent, indent=2)
|
|
2253
2342
|
agents[agent_id] = agent
|
|
2254
2343
|
metrics.inc("agent_hub_agents_auto_created_total")
|
|
2255
2344
|
metrics.set_gauge("agent_hub_active_agents", len(agents))
|
|
@@ -2332,10 +2421,10 @@ def get_or_create_agent_for_session(
|
|
|
2332
2421
|
}
|
|
2333
2422
|
save_session_agents()
|
|
2334
2423
|
|
|
2335
|
-
# Save agent to disk
|
|
2424
|
+
# Save agent to disk (atomic write to prevent readers seeing partial files)
|
|
2336
2425
|
agent_file = AGENTS_DIR / f"{agent_id}.json"
|
|
2337
2426
|
try:
|
|
2338
|
-
agent_file
|
|
2427
|
+
atomic_write_json(agent_file, agent, indent=2)
|
|
2339
2428
|
agents[agent_id] = agent
|
|
2340
2429
|
metrics.inc("agent_hub_agents_auto_created_total")
|
|
2341
2430
|
metrics.set_gauge("agent_hub_active_agents", len(agents))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/debian/source/format
RENAMED
|
File without changes
|
{opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/packaging/rpm/opencode-agent-hub.spec
RENAMED
|
File without changes
|
{opencode_agent_hub-1.3.2 → opencode_agent_hub-1.3.3}/contrib/systemd/agent-hub-daemon.service
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|