forgexa-cli 1.67.1__tar.gz → 1.68.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.67.1 → forgexa_cli-1.68.0}/PKG-INFO +1 -1
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/agent_core.py +44 -12
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/aws_logs_evidence.py +3 -1
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/daemon.py +17 -6
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/pyproject.toml +1 -1
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/README.md +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/aws_evidence.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/setup.cfg +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.0}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.67.1 → forgexa_cli-1.68.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.68.0"
|
|
@@ -3753,7 +3753,13 @@ def is_copilot_model_discovery_auth_error(*messages: str | None) -> bool:
|
|
|
3753
3753
|
def is_runtime_resource_exhaustion_error(*messages: str | None) -> bool:
|
|
3754
3754
|
"""Return whether the OS refused an agent process, thread, or file watch."""
|
|
3755
3755
|
detail = " ".join(str(message or "").lower() for message in messages)
|
|
3756
|
-
return
|
|
3756
|
+
return (
|
|
3757
|
+
any(pattern in detail for pattern in (
|
|
3758
|
+
"failed to spawn thread",
|
|
3759
|
+
"failed to start task transition watcher",
|
|
3760
|
+
))
|
|
3761
|
+
and "resource temporarily unavailable" in detail
|
|
3762
|
+
) or any(
|
|
3757
3763
|
pattern in detail
|
|
3758
3764
|
for pattern in (
|
|
3759
3765
|
"can't spawn worker thread",
|
|
@@ -3778,23 +3784,35 @@ def collect_runtime_resource_metrics(
|
|
|
3778
3784
|
return metrics
|
|
3779
3785
|
|
|
3780
3786
|
proc_path = Path(proc_root)
|
|
3781
|
-
cgroup_path = Path(cgroup_root)
|
|
3782
|
-
constraints: list[tuple[str, int, int]] = []
|
|
3787
|
+
cgroup_path = Path(cgroup_root).resolve()
|
|
3788
|
+
constraints: list[tuple[str, int, int, Path | None]] = []
|
|
3783
3789
|
|
|
3784
3790
|
try:
|
|
3785
3791
|
for line in (proc_path / "self" / "cgroup").read_text().splitlines():
|
|
3786
3792
|
hierarchy, controllers, relative_path = line.split(":", 2)
|
|
3787
3793
|
relative = relative_path.lstrip("/")
|
|
3788
3794
|
if hierarchy == "0" and not controllers:
|
|
3789
|
-
|
|
3795
|
+
controller_root = cgroup_path
|
|
3790
3796
|
elif "pids" in controllers.split(","):
|
|
3791
|
-
|
|
3797
|
+
controller_root = (cgroup_path / "pids").resolve()
|
|
3792
3798
|
else:
|
|
3793
3799
|
continue
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3800
|
+
pid_dir = (controller_root / relative).resolve()
|
|
3801
|
+
if not pid_dir.is_relative_to(controller_root):
|
|
3802
|
+
continue
|
|
3803
|
+
while True:
|
|
3804
|
+
try:
|
|
3805
|
+
current_text = (pid_dir / "pids.current").read_text().strip()
|
|
3806
|
+
maximum_text = (pid_dir / "pids.max").read_text().strip()
|
|
3807
|
+
if maximum_text != "max":
|
|
3808
|
+
constraints.append((
|
|
3809
|
+
"cgroup", int(current_text), int(maximum_text), pid_dir,
|
|
3810
|
+
))
|
|
3811
|
+
except (OSError, ValueError):
|
|
3812
|
+
pass
|
|
3813
|
+
if pid_dir == controller_root:
|
|
3814
|
+
break
|
|
3815
|
+
pid_dir = pid_dir.parent
|
|
3798
3816
|
break
|
|
3799
3817
|
except (OSError, ValueError):
|
|
3800
3818
|
pass
|
|
@@ -3819,12 +3837,12 @@ def collect_runtime_resource_metrics(
|
|
|
3819
3837
|
user_threads += threads
|
|
3820
3838
|
except (OSError, ValueError, IndexError):
|
|
3821
3839
|
continue
|
|
3822
|
-
constraints.append(("rlimit_nproc", user_threads, int(soft_limit)))
|
|
3840
|
+
constraints.append(("rlimit_nproc", user_threads, int(soft_limit), None))
|
|
3823
3841
|
except (ImportError, OSError, ValueError):
|
|
3824
3842
|
pass
|
|
3825
3843
|
|
|
3826
3844
|
if constraints:
|
|
3827
|
-
source, current, limit = min(
|
|
3845
|
+
source, current, limit, limit_path = min(
|
|
3828
3846
|
constraints,
|
|
3829
3847
|
key=lambda item: item[2] - item[1],
|
|
3830
3848
|
)
|
|
@@ -3834,6 +3852,20 @@ def collect_runtime_resource_metrics(
|
|
|
3834
3852
|
"process_limit": limit,
|
|
3835
3853
|
"process_remaining": max(0, limit - current),
|
|
3836
3854
|
})
|
|
3855
|
+
if limit_path is not None:
|
|
3856
|
+
metrics["process_limit_path"] = str(limit_path)
|
|
3857
|
+
try:
|
|
3858
|
+
for line in (limit_path / "pids.events").read_text().splitlines():
|
|
3859
|
+
name, value = line.split()
|
|
3860
|
+
if name == "max":
|
|
3861
|
+
metrics["process_limit_hits"] = int(value)
|
|
3862
|
+
break
|
|
3863
|
+
except (OSError, ValueError):
|
|
3864
|
+
pass
|
|
3865
|
+
try:
|
|
3866
|
+
metrics["process_peak"] = int((limit_path / "pids.peak").read_text().strip())
|
|
3867
|
+
except (OSError, ValueError):
|
|
3868
|
+
pass
|
|
3837
3869
|
|
|
3838
3870
|
try:
|
|
3839
3871
|
for line in (proc_path / "meminfo").read_text().splitlines():
|
|
@@ -3873,7 +3905,7 @@ def collect_runtime_resource_metrics(
|
|
|
3873
3905
|
def runtime_resource_exhaustion_reason(metrics: dict[str, Any]) -> str | None:
|
|
3874
3906
|
"""Return a blocking reason when the OS cannot safely start an agent."""
|
|
3875
3907
|
process_remaining = metrics.get("process_remaining")
|
|
3876
|
-
if isinstance(process_remaining, int) and process_remaining <
|
|
3908
|
+
if isinstance(process_remaining, int) and process_remaining < 64:
|
|
3877
3909
|
return (
|
|
3878
3910
|
f"Only {process_remaining} process/thread slots remain "
|
|
3879
3911
|
f"({metrics.get('process_current')}/{metrics.get('process_limit')} used via "
|
|
@@ -31,7 +31,7 @@ def select_attachment(evidence, event_ids, ranges, attachment_id):
|
|
|
31
31
|
available = {event["event_ref"]: event for event in evidence["events"]}
|
|
32
32
|
if len(event_ids) != len(set(event_ids)) or set(event_ids) - available.keys() or set(ranges) - set(event_ids):
|
|
33
33
|
raise ValueError("aws_logs_selection_invalid")
|
|
34
|
-
if not event_ids
|
|
34
|
+
if not event_ids:
|
|
35
35
|
raise ValueError("aws_logs_selection_required")
|
|
36
36
|
selected = []
|
|
37
37
|
for event_id in event_ids:
|
|
@@ -114,6 +114,8 @@ def validate_payload(reference, metadata, payload):
|
|
|
114
114
|
events = evidence.get("events")
|
|
115
115
|
if not isinstance(sources, list) or not 1 <= len(sources) <= 4 or not isinstance(events, list) or len(events) > 1000:
|
|
116
116
|
raise ValueError("aws_evidence_size_limit")
|
|
117
|
+
if not events:
|
|
118
|
+
raise ValueError("aws_logs_selection_required")
|
|
117
119
|
identities = set()
|
|
118
120
|
for source in sources:
|
|
119
121
|
if not isinstance(source, dict) or source.get("source_ref") in identities or not re.fullmatch(r"AWS-L1-S\d+", str(source.get("source_ref", ""))):
|
|
@@ -1234,7 +1234,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1234
1234
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1235
1235
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1236
1236
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1237
|
-
DAEMON_VERSION = "1.
|
|
1237
|
+
DAEMON_VERSION = "1.68.0"
|
|
1238
1238
|
|
|
1239
1239
|
|
|
1240
1240
|
def _detect_client_type() -> str:
|
|
@@ -7906,12 +7906,15 @@ class ProcessManager:
|
|
|
7906
7906
|
# CREATE_NO_WINDOW covers this secondary check so Copilot always sees a fully
|
|
7907
7907
|
# headless environment and processes the complete task specification.
|
|
7908
7908
|
proc: asyncio.subprocess.Process | None = None
|
|
7909
|
+
cleaned_process: asyncio.subprocess.Process | None = None
|
|
7909
7910
|
|
|
7910
7911
|
async def _terminate_copilot_process(
|
|
7911
7912
|
process: asyncio.subprocess.Process | None,
|
|
7912
7913
|
) -> None:
|
|
7913
|
-
|
|
7914
|
+
nonlocal cleaned_process
|
|
7915
|
+
if process is None or process is cleaned_process:
|
|
7914
7916
|
return
|
|
7917
|
+
cleaned_process = process
|
|
7915
7918
|
_kill_proc(process)
|
|
7916
7919
|
try:
|
|
7917
7920
|
await asyncio.wait_for(process.wait(), timeout=5)
|
|
@@ -7919,6 +7922,7 @@ class ProcessManager:
|
|
|
7919
7922
|
pass
|
|
7920
7923
|
|
|
7921
7924
|
try:
|
|
7925
|
+
resources_before = agent_core.collect_runtime_resource_metrics()
|
|
7922
7926
|
proc = await _create_subprocess_exec(
|
|
7923
7927
|
*cmd,
|
|
7924
7928
|
stdout=asyncio.subprocess.PIPE,
|
|
@@ -7947,6 +7951,15 @@ class ProcessManager:
|
|
|
7947
7951
|
# from the JSONL "result" event for a true success signal.
|
|
7948
7952
|
copilot_exit = self._extract_copilot_exit_code(stdout)
|
|
7949
7953
|
signals = self._extract_output_signals(stdout)
|
|
7954
|
+
resource_exhausted = agent_core.is_runtime_resource_exhaustion_error(
|
|
7955
|
+
stderr, *(signals.get("error_messages") or []),
|
|
7956
|
+
)
|
|
7957
|
+
if resource_exhausted:
|
|
7958
|
+
metrics["runtime_resources_before"] = resources_before
|
|
7959
|
+
metrics["runtime_resources_at_failure"] = (
|
|
7960
|
+
agent_core.collect_runtime_resource_metrics()
|
|
7961
|
+
)
|
|
7962
|
+
await _terminate_copilot_process(proc)
|
|
7950
7963
|
completed_without_result = bool(
|
|
7951
7964
|
signals["has_turn_completed"]
|
|
7952
7965
|
and signals["has_assistant_events"]
|
|
@@ -8226,10 +8239,7 @@ class ProcessManager:
|
|
|
8226
8239
|
error=failure_error,
|
|
8227
8240
|
failure_code=(
|
|
8228
8241
|
"runtime_resource_exhausted"
|
|
8229
|
-
if
|
|
8230
|
-
failure_error,
|
|
8231
|
-
stderr,
|
|
8232
|
-
)
|
|
8242
|
+
if resource_exhausted
|
|
8233
8243
|
else ""
|
|
8234
8244
|
),
|
|
8235
8245
|
metrics=metrics,
|
|
@@ -8278,6 +8288,7 @@ class ProcessManager:
|
|
|
8278
8288
|
error=f"Stream processing error: {exc}",
|
|
8279
8289
|
)
|
|
8280
8290
|
finally:
|
|
8291
|
+
await _terminate_copilot_process(proc)
|
|
8281
8292
|
self.active_processes.pop(task_id, None)
|
|
8282
8293
|
if _is_outer_call and _isolated_copilot_home:
|
|
8283
8294
|
if _master_copilot_home and _auth_state["persist"]:
|
|
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
|