forgexa-cli 1.45.5__tar.gz → 1.46.0__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.
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/PKG-INFO +1 -1
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/agent_core.py +11 -6
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/daemon.py +49 -1
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/pyproject.toml +1 -1
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/README.md +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/setup.cfg +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.45.5 → forgexa_cli-1.46.0}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.46.0"
|
|
@@ -505,12 +505,12 @@ class AgentDiscovery:
|
|
|
505
505
|
cmd = custom_path or spec["commands"][0]
|
|
506
506
|
resolved = shutil.which(cmd)
|
|
507
507
|
if resolved:
|
|
508
|
-
version = await self._get_version(spec["detect"])
|
|
508
|
+
version = await self._get_version(spec["detect"], resolved)
|
|
509
509
|
if version is None and "detect_alt" in spec:
|
|
510
510
|
# Primary detect command failed; try the alternative.
|
|
511
511
|
# Example: new GitHub Copilot CLI 1.0.x uses `copilot version`
|
|
512
512
|
# as a subcommand while older releases used `copilot --version`.
|
|
513
|
-
version = await self._get_version(spec["detect_alt"])
|
|
513
|
+
version = await self._get_version(spec["detect_alt"], resolved)
|
|
514
514
|
if version is None:
|
|
515
515
|
# Binary found but version check failed — it is a stub or
|
|
516
516
|
# not properly installed (e.g. copilot prompts to install).
|
|
@@ -540,7 +540,9 @@ class AgentDiscovery:
|
|
|
540
540
|
logger.info("Discovered agent: %s v%s (%s)", agent_id, version, resolved)
|
|
541
541
|
return available
|
|
542
542
|
|
|
543
|
-
async def _get_version(
|
|
543
|
+
async def _get_version(
|
|
544
|
+
self, detect_cmd: str, executable: str | None = None,
|
|
545
|
+
) -> str | None:
|
|
544
546
|
"""Run <detect_cmd> and return the first line of output as a version string.
|
|
545
547
|
|
|
546
548
|
Returns ``None`` if the command exits with a non-zero code, times out,
|
|
@@ -551,9 +553,12 @@ class AgentDiscovery:
|
|
|
551
553
|
import re
|
|
552
554
|
try:
|
|
553
555
|
parts = detect_cmd.split()
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
556
|
+
if executable:
|
|
557
|
+
parts[0] = executable
|
|
558
|
+
else:
|
|
559
|
+
resolved = shutil.which(parts[0])
|
|
560
|
+
if resolved:
|
|
561
|
+
parts[0] = resolved
|
|
557
562
|
parts = _prepare_agent_command(parts, preserve_argv=False)
|
|
558
563
|
proc = await asyncio.create_subprocess_exec(
|
|
559
564
|
*parts,
|
|
@@ -1067,7 +1067,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1067
1067
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1068
1068
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1069
1069
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1070
|
-
DAEMON_VERSION = "1.
|
|
1070
|
+
DAEMON_VERSION = "1.46.0"
|
|
1071
1071
|
|
|
1072
1072
|
|
|
1073
1073
|
def _detect_client_type() -> str:
|
|
@@ -8593,6 +8593,8 @@ class RuntimeDaemon:
|
|
|
8593
8593
|
Task execution is shared across all servers with a single concurrent task limit.
|
|
8594
8594
|
"""
|
|
8595
8595
|
|
|
8596
|
+
AGENT_REFRESH_INTERVAL_SECONDS = 60
|
|
8597
|
+
|
|
8596
8598
|
def __init__(self):
|
|
8597
8599
|
# Compute stable hardware fingerprint from machine-id / MAC address.
|
|
8598
8600
|
# This allows the server to identify the same physical machine even when
|
|
@@ -8627,6 +8629,7 @@ class RuntimeDaemon:
|
|
|
8627
8629
|
|
|
8628
8630
|
self.connections: list[ServerConnection] = []
|
|
8629
8631
|
self.agent_discovery = AgentDiscovery()
|
|
8632
|
+
self._agent_refresh_task: asyncio.Task | None = None
|
|
8630
8633
|
self.workspace_manager = WorkspaceManager(self.workspaces_root)
|
|
8631
8634
|
self.process_manager = ProcessManager()
|
|
8632
8635
|
self._lock_file = None # File lock to prevent multiple daemon instances
|
|
@@ -8657,6 +8660,41 @@ class RuntimeDaemon:
|
|
|
8657
8660
|
self._project_execution_locks[project_key] = asyncio.Lock()
|
|
8658
8661
|
return self._project_execution_locks[project_key]
|
|
8659
8662
|
|
|
8663
|
+
async def _refresh_agents(self) -> None:
|
|
8664
|
+
"""Re-discover local CLIs so upgrades reach the runtime heartbeat."""
|
|
8665
|
+
try:
|
|
8666
|
+
refreshed_agents = await self.agent_discovery.discover()
|
|
8667
|
+
except Exception as exc:
|
|
8668
|
+
logger.warning("Agent inventory refresh failed: %s", exc)
|
|
8669
|
+
return
|
|
8670
|
+
|
|
8671
|
+
if refreshed_agents == self.agents:
|
|
8672
|
+
return
|
|
8673
|
+
|
|
8674
|
+
previous_agents = ", ".join(
|
|
8675
|
+
f"{agent.agent_id} v{agent.version}" for agent in self.agents
|
|
8676
|
+
) or "none"
|
|
8677
|
+
current_agents = ", ".join(
|
|
8678
|
+
f"{agent.agent_id} v{agent.version}" for agent in refreshed_agents
|
|
8679
|
+
) or "none"
|
|
8680
|
+
self.agents = refreshed_agents
|
|
8681
|
+
logger.info("Refreshed agent inventory: %s -> %s", previous_agents, current_agents)
|
|
8682
|
+
|
|
8683
|
+
active_tasks = len(self.active_tasks)
|
|
8684
|
+
agent_dicts = self._agents_as_dicts()
|
|
8685
|
+
for connection in self.connections:
|
|
8686
|
+
if connection.heartbeat:
|
|
8687
|
+
connection.heartbeat.update(active_tasks, agent_dicts)
|
|
8688
|
+
|
|
8689
|
+
async def _agent_refresh_loop(self) -> None:
|
|
8690
|
+
while not self._shutdown:
|
|
8691
|
+
try:
|
|
8692
|
+
await asyncio.sleep(self.AGENT_REFRESH_INTERVAL_SECONDS)
|
|
8693
|
+
except asyncio.CancelledError:
|
|
8694
|
+
return
|
|
8695
|
+
if not self._shutdown:
|
|
8696
|
+
await self._refresh_agents()
|
|
8697
|
+
|
|
8660
8698
|
def _publish_local_runtime_registration(self, server_url: str, runtime_id: str) -> None:
|
|
8661
8699
|
issuer = _normalize_issuer_url(server_url)
|
|
8662
8700
|
if not issuer or not runtime_id:
|
|
@@ -9130,6 +9168,8 @@ class RuntimeDaemon:
|
|
|
9130
9168
|
logger.info("Daemon ready. Connected to %d server(s). Polling for tasks...",
|
|
9131
9169
|
len(self.connections))
|
|
9132
9170
|
|
|
9171
|
+
self._agent_refresh_task = asyncio.create_task(self._agent_refresh_loop())
|
|
9172
|
+
|
|
9133
9173
|
# Clean up stale Copilot/Claude temp directories left by prior OOM-killed
|
|
9134
9174
|
# daemon processes. This runs once at startup so /tmp doesn't fill up.
|
|
9135
9175
|
await self._cleanup_stale_tmp_dirs()
|
|
@@ -14581,6 +14621,14 @@ class RuntimeDaemon:
|
|
|
14581
14621
|
logger.info("Shutting down daemon...")
|
|
14582
14622
|
self._shutdown = True
|
|
14583
14623
|
|
|
14624
|
+
if self._agent_refresh_task:
|
|
14625
|
+
self._agent_refresh_task.cancel()
|
|
14626
|
+
try:
|
|
14627
|
+
await self._agent_refresh_task
|
|
14628
|
+
except asyncio.CancelledError:
|
|
14629
|
+
pass
|
|
14630
|
+
self._agent_refresh_task = None
|
|
14631
|
+
|
|
14584
14632
|
# Cancel task coroutines, then terminate and reap every tracked process.
|
|
14585
14633
|
tasks = list(self.active_tasks.values())
|
|
14586
14634
|
for task in tasks:
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|