forgexa-cli 1.57.1__tar.gz → 1.57.2__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.57.1 → forgexa_cli-1.57.2}/PKG-INFO +1 -1
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/daemon.py +48 -4
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/pyproject.toml +1 -1
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/README.md +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/setup.cfg +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.57.
|
|
2
|
+
__version__ = "1.57.2"
|
|
@@ -1150,7 +1150,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1150
1150
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1151
1151
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1152
1152
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1153
|
-
DAEMON_VERSION = "1.57.
|
|
1153
|
+
DAEMON_VERSION = "1.57.2"
|
|
1154
1154
|
|
|
1155
1155
|
|
|
1156
1156
|
def _detect_client_type() -> str:
|
|
@@ -10238,6 +10238,7 @@ class RuntimeDaemon:
|
|
|
10238
10238
|
self._project_execution_locks: dict[str, asyncio.Lock] = {}
|
|
10239
10239
|
self._project_execution_locks_mu: asyncio.Lock = asyncio.Lock()
|
|
10240
10240
|
self._shutdown = False
|
|
10241
|
+
self._shutdown_event = asyncio.Event()
|
|
10241
10242
|
self._local_runtime_instance_id = secrets.token_urlsafe(18)
|
|
10242
10243
|
self._local_runtime_registrations: dict[str, dict[str, int | str]] = {}
|
|
10243
10244
|
self._desktop_restart_requested = False
|
|
@@ -11188,7 +11189,16 @@ class RuntimeDaemon:
|
|
|
11188
11189
|
had_work,
|
|
11189
11190
|
next_delay,
|
|
11190
11191
|
)
|
|
11191
|
-
|
|
11192
|
+
# AGT-04 (audit): wake IMMEDIATELY on SIGTERM. A plain sleep
|
|
11193
|
+
# kept the loop parked for the whole poll delay while the
|
|
11194
|
+
# replacing process only waits 5s before SIGKILL — graceful
|
|
11195
|
+
# shutdown then never ran at all.
|
|
11196
|
+
try:
|
|
11197
|
+
await asyncio.wait_for(
|
|
11198
|
+
self._shutdown_event.wait(), timeout=next_delay,
|
|
11199
|
+
)
|
|
11200
|
+
except asyncio.TimeoutError:
|
|
11201
|
+
pass
|
|
11192
11202
|
except asyncio.CancelledError:
|
|
11193
11203
|
pass
|
|
11194
11204
|
finally:
|
|
@@ -17863,8 +17873,30 @@ class RuntimeDaemon:
|
|
|
17863
17873
|
tasks = list(self.active_tasks.values())
|
|
17864
17874
|
for task in tasks:
|
|
17865
17875
|
task.cancel()
|
|
17866
|
-
|
|
17867
|
-
|
|
17876
|
+
# AGT-04 (audit): cancel every process CONCURRENTLY under a single
|
|
17877
|
+
# 4s deadline. The serial loop spent up to 5s PER process, so with
|
|
17878
|
+
# two or more active tasks the replacing process's SIGKILL truncated
|
|
17879
|
+
# the drain and left orphaned agents writing into workspaces the new
|
|
17880
|
+
# daemon was already re-preparing.
|
|
17881
|
+
process_ids = list(self.process_manager.active_processes)
|
|
17882
|
+
if process_ids:
|
|
17883
|
+
try:
|
|
17884
|
+
await asyncio.wait_for(
|
|
17885
|
+
asyncio.gather(
|
|
17886
|
+
*(
|
|
17887
|
+
self.process_manager.cancel(process_id)
|
|
17888
|
+
for process_id in process_ids
|
|
17889
|
+
),
|
|
17890
|
+
return_exceptions=True,
|
|
17891
|
+
),
|
|
17892
|
+
timeout=4.0,
|
|
17893
|
+
)
|
|
17894
|
+
except asyncio.TimeoutError:
|
|
17895
|
+
logger.warning(
|
|
17896
|
+
"Process drain exceeded 4s deadline with %d process(es); "
|
|
17897
|
+
"continuing shutdown",
|
|
17898
|
+
len(process_ids),
|
|
17899
|
+
)
|
|
17868
17900
|
if tasks:
|
|
17869
17901
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
17870
17902
|
self.active_tasks.clear()
|
|
@@ -17885,6 +17917,18 @@ class RuntimeDaemon:
|
|
|
17885
17917
|
def handle_signal(self, sig):
|
|
17886
17918
|
logger.info("Received signal %s", sig)
|
|
17887
17919
|
self._shutdown = True
|
|
17920
|
+
# AGT-04: interrupt the poll-delay sleep so graceful shutdown starts
|
|
17921
|
+
# within milliseconds instead of after the full poll interval. The
|
|
17922
|
+
# Windows fallback invokes this from a signal handler off the loop
|
|
17923
|
+
# thread, so the wake must be scheduled thread-safely there.
|
|
17924
|
+
try:
|
|
17925
|
+
loop = asyncio.get_running_loop()
|
|
17926
|
+
except RuntimeError:
|
|
17927
|
+
loop = None
|
|
17928
|
+
if loop is not None and loop.is_running():
|
|
17929
|
+
loop.call_soon_threadsafe(self._shutdown_event.set)
|
|
17930
|
+
else:
|
|
17931
|
+
self._shutdown_event.set()
|
|
17888
17932
|
|
|
17889
17933
|
|
|
17890
17934
|
async def main():
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|