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.
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/PKG-INFO +1 -1
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/daemon.py +78 -8
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/pyproject.toml +1 -1
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/README.md +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/agent_core.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/setup.cfg +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.57.0 → forgexa_cli-1.57.2}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.57.0 → 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:
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17841
|
-
|
|
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():
|
|
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
|