forgexa-cli 1.57.0__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.0 → forgexa_cli-1.57.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/daemon.py +78 -8
  4. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  5. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/pyproject.toml +1 -1
  6. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/README.md +0 -0
  7. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/_build_config.py +0 -0
  8. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/_local_bind.py +0 -0
  9. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/agent_core.py +0 -0
  10. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/setup.cfg +0 -0
  21. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.57.0 → 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.0
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.0"
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.0"
1153
+ DAEMON_VERSION = "1.57.2"
1154
1154
 
1155
1155
 
1156
1156
  def _detect_client_type() -> str:
@@ -6926,7 +6926,7 @@ class ProcessManager:
6926
6926
  task_id,
6927
6927
  stdin_input=agent_core.qoder_stdin_prompt(prompt),
6928
6928
  on_chunk=on_chunk,
6929
- env=agent_core.qoder_base_env(os.environ.copy()),
6929
+ env=await asyncio.to_thread(agent_core.qoder_base_env, os.environ.copy()),
6930
6930
  )
6931
6931
  finally:
6932
6932
  agent_core.release_file_lock(profile_lock)
@@ -6991,7 +6991,7 @@ class ProcessManager:
6991
6991
  task_id,
6992
6992
  stdin_input=prompt,
6993
6993
  on_chunk=on_chunk,
6994
- env=agent_core.trae_code_base_env(os.environ.copy()),
6994
+ env=await asyncio.to_thread(agent_core.trae_code_base_env, os.environ.copy()),
6995
6995
  )
6996
6996
  finally:
6997
6997
  agent_core.release_file_lock(profile_lock)
@@ -7052,6 +7052,9 @@ class ProcessManager:
7052
7052
  if on_chunk is None or event.get("type") not in {"model.streaming", "tool.updated"}:
7053
7053
  return
7054
7054
  payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
7055
+ # AGT-11 (audit): zcode 0.16.x never emits "model.streaming" —
7056
+ # final text is read back from session/messages after the turn
7057
+ # terminates. This branch is forward-compatibility only.
7055
7058
  if event.get("type") == "model.streaming":
7056
7059
  text = payload.get("text") or payload.get("delta") or payload.get("content")
7057
7060
  line = text.strip() if isinstance(text, str) else ""
@@ -7069,7 +7072,7 @@ class ProcessManager:
7069
7072
  client = ZCodeProtocolSession(
7070
7073
  [agent.command, "app-server"],
7071
7074
  cwd=cwd,
7072
- env=agent_core.zcode_base_env(os.environ.copy()),
7075
+ env=await asyncio.to_thread(agent_core.zcode_base_env, os.environ.copy()),
7073
7076
  on_event=on_event,
7074
7077
  permission_handler=agent_core.zcode_permission_response,
7075
7078
  )
@@ -7127,7 +7130,10 @@ class ProcessManager:
7127
7130
  stdout="",
7128
7131
  stderr=client.stderr_tail[-10000:] if client is not None else "",
7129
7132
  error=f"Timed out after {timeout}s (absolute limit)",
7130
- failure_code="agent_idle_timeout",
7133
+ # AGT-12 (audit): this is the run_turn absolute cap, not an
7134
+ # idle gap — a distinct code keeps retry/degrade policies
7135
+ # from misreading a too-long task as a stuck one.
7136
+ failure_code="zcode_turn_timeout",
7131
7137
  )
7132
7138
  except ZCodeProtocolError as exc:
7133
7139
  return TaskResult(
@@ -10232,6 +10238,7 @@ class RuntimeDaemon:
10232
10238
  self._project_execution_locks: dict[str, asyncio.Lock] = {}
10233
10239
  self._project_execution_locks_mu: asyncio.Lock = asyncio.Lock()
10234
10240
  self._shutdown = False
10241
+ self._shutdown_event = asyncio.Event()
10235
10242
  self._local_runtime_instance_id = secrets.token_urlsafe(18)
10236
10243
  self._local_runtime_registrations: dict[str, dict[str, int | str]] = {}
10237
10244
  self._desktop_restart_requested = False
@@ -11182,7 +11189,16 @@ class RuntimeDaemon:
11182
11189
  had_work,
11183
11190
  next_delay,
11184
11191
  )
11185
- 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
11186
11202
  except asyncio.CancelledError:
11187
11203
  pass
11188
11204
  finally:
@@ -15617,6 +15633,26 @@ class RuntimeDaemon:
15617
15633
  )
15618
15634
  return {"push_error": f"Would push to default branch {current_branch}"}
15619
15635
 
15636
+ # AGT-10 (audit): last line of defense after a collision was
15637
+ # manually released at gate review — the executed branch must be
15638
+ # this requirement's canonical branch (or the workflow's recorded
15639
+ # analysis branch), never another requirement's.
15640
+ if task.requirement_key and current_branch and expected_branch:
15641
+ if current_branch != expected_branch:
15642
+ logger.error(
15643
+ "CRITICAL: requirement %s is about to push branch '%s' "
15644
+ "which is not its canonical branch '%s' — aborting",
15645
+ task.requirement_key, current_branch, expected_branch,
15646
+ )
15647
+ return {
15648
+ "push_error": (
15649
+ f"Push blocked: branch '{current_branch}' does not belong to "
15650
+ f"requirement {task.requirement_key} (expected '{expected_branch}'). "
15651
+ "A cross-requirement branch collision was likely released at review; "
15652
+ "re-run the fix on the correct branch."
15653
+ ),
15654
+ }
15655
+
15620
15656
  if task.requirement_key and current_branch:
15621
15657
  forbidden_merges = await self._list_unpushed_merge_commits(
15622
15658
  workspace_path, current_branch, default_branch,
@@ -17837,8 +17873,30 @@ class RuntimeDaemon:
17837
17873
  tasks = list(self.active_tasks.values())
17838
17874
  for task in tasks:
17839
17875
  task.cancel()
17840
- for process_id in list(self.process_manager.active_processes):
17841
- 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
+ )
17842
17900
  if tasks:
17843
17901
  await asyncio.gather(*tasks, return_exceptions=True)
17844
17902
  self.active_tasks.clear()
@@ -17859,6 +17917,18 @@ class RuntimeDaemon:
17859
17917
  def handle_signal(self, sig):
17860
17918
  logger.info("Received signal %s", sig)
17861
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()
17862
17932
 
17863
17933
 
17864
17934
  async def main():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.57.0
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.0"
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