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.
Files changed (29) hide show
  1. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/daemon.py +48 -4
  4. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/pyproject.toml +1 -1
  6. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/README.md +0 -0
  7. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.1 → forgexa_cli-1.57.2}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.1
3
+ Version: 1.57.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.57.1"
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.1"
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
- await asyncio.sleep(next_delay)
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
- for process_id in list(self.process_manager.active_processes):
17867
- await self.process_manager.cancel(process_id)
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():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.1
3
+ Version: 1.57.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.57.1"
3
+ version = "1.57.2"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
File without changes
File without changes