forgexa-cli 1.62.2__tar.gz → 1.62.3__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.62.2 → forgexa_cli-1.62.3}/PKG-INFO +1 -1
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/agent_core.py +63 -3
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/daemon.py +10 -8
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/pyproject.toml +1 -1
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/README.md +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/main.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/zcode_protocol.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/setup.cfg +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.62.
|
|
2
|
+
__version__ = "1.62.3"
|
|
@@ -3626,7 +3626,7 @@ def is_copilot_model_discovery_auth_error(*messages: str | None) -> bool:
|
|
|
3626
3626
|
|
|
3627
3627
|
|
|
3628
3628
|
def is_runtime_resource_exhaustion_error(*messages: str | None) -> bool:
|
|
3629
|
-
"""Return whether the OS refused
|
|
3629
|
+
"""Return whether the OS refused an agent process, thread, or file watch."""
|
|
3630
3630
|
detail = " ".join(str(message or "").lower() for message in messages)
|
|
3631
3631
|
return any(
|
|
3632
3632
|
pattern in detail
|
|
@@ -3635,6 +3635,9 @@ def is_runtime_resource_exhaustion_error(*messages: str | None) -> bool:
|
|
|
3635
3635
|
"failed to load ca certificates off thread",
|
|
3636
3636
|
"resource temporarily unavailable (os error 11)",
|
|
3637
3637
|
"pthread_create failed",
|
|
3638
|
+
"os file watch limit reached",
|
|
3639
|
+
"inotify watch limit reached",
|
|
3640
|
+
"inotify instance limit reached",
|
|
3638
3641
|
)
|
|
3639
3642
|
)
|
|
3640
3643
|
|
|
@@ -3644,7 +3647,7 @@ def collect_runtime_resource_metrics(
|
|
|
3644
3647
|
proc_root: str | Path = "/proc",
|
|
3645
3648
|
cgroup_root: str | Path = "/sys/fs/cgroup",
|
|
3646
3649
|
) -> dict[str, Any]:
|
|
3647
|
-
"""Collect Linux process/thread
|
|
3650
|
+
"""Collect Linux process/thread, memory, and file-watch capacity."""
|
|
3648
3651
|
metrics: dict[str, Any] = {}
|
|
3649
3652
|
if not sys.platform.startswith("linux"):
|
|
3650
3653
|
return metrics
|
|
@@ -3714,11 +3717,36 @@ def collect_runtime_resource_metrics(
|
|
|
3714
3717
|
break
|
|
3715
3718
|
except (OSError, ValueError, IndexError):
|
|
3716
3719
|
pass
|
|
3720
|
+
|
|
3721
|
+
probe_fd: int | None = None
|
|
3722
|
+
try:
|
|
3723
|
+
for name in ("max_user_watches", "max_user_instances"):
|
|
3724
|
+
metrics[f"inotify_{name}"] = int(
|
|
3725
|
+
(proc_path / "sys" / "fs" / "inotify" / name).read_text().strip()
|
|
3726
|
+
)
|
|
3727
|
+
import ctypes
|
|
3728
|
+
|
|
3729
|
+
library = ctypes.CDLL(None, use_errno=True)
|
|
3730
|
+
library.inotify_init1.argtypes = [ctypes.c_int]
|
|
3731
|
+
library.inotify_init1.restype = ctypes.c_int
|
|
3732
|
+
library.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32]
|
|
3733
|
+
library.inotify_add_watch.restype = ctypes.c_int
|
|
3734
|
+
probe_fd = library.inotify_init1(os.O_CLOEXEC | os.O_NONBLOCK)
|
|
3735
|
+
if probe_fd < 0:
|
|
3736
|
+
metrics["inotify_probe_errno"] = ctypes.get_errno()
|
|
3737
|
+
else:
|
|
3738
|
+
watch = library.inotify_add_watch(probe_fd, os.fsencode(proc_path), 0x00000004)
|
|
3739
|
+
metrics["inotify_probe_errno"] = ctypes.get_errno() if watch < 0 else 0
|
|
3740
|
+
except (AttributeError, OSError, ValueError):
|
|
3741
|
+
pass
|
|
3742
|
+
finally:
|
|
3743
|
+
if probe_fd is not None and probe_fd >= 0:
|
|
3744
|
+
os.close(probe_fd)
|
|
3717
3745
|
return metrics
|
|
3718
3746
|
|
|
3719
3747
|
|
|
3720
3748
|
def runtime_resource_exhaustion_reason(metrics: dict[str, Any]) -> str | None:
|
|
3721
|
-
"""Return a blocking reason when
|
|
3749
|
+
"""Return a blocking reason when the OS cannot safely start an agent."""
|
|
3722
3750
|
process_remaining = metrics.get("process_remaining")
|
|
3723
3751
|
if isinstance(process_remaining, int) and process_remaining < 32:
|
|
3724
3752
|
return (
|
|
@@ -3729,9 +3757,41 @@ def runtime_resource_exhaustion_reason(metrics: dict[str, Any]) -> str | None:
|
|
|
3729
3757
|
memory_available_mb = metrics.get("memory_available_mb")
|
|
3730
3758
|
if isinstance(memory_available_mb, int) and memory_available_mb < 256:
|
|
3731
3759
|
return f"Only {memory_available_mb} MiB memory is available"
|
|
3760
|
+
import errno
|
|
3761
|
+
|
|
3762
|
+
probe_errno = metrics.get("inotify_probe_errno")
|
|
3763
|
+
if probe_errno == errno.ENOSPC:
|
|
3764
|
+
return (
|
|
3765
|
+
"OS file watch limit reached (inotify ENOSPC; "
|
|
3766
|
+
f"fs.inotify.max_user_watches={metrics.get('inotify_max_user_watches')})"
|
|
3767
|
+
)
|
|
3768
|
+
if probe_errno in {errno.EMFILE, errno.ENFILE}:
|
|
3769
|
+
return (
|
|
3770
|
+
"Inotify instance or file descriptor limit reached "
|
|
3771
|
+
f"({errno.errorcode[probe_errno]}; "
|
|
3772
|
+
f"fs.inotify.max_user_instances={metrics.get('inotify_max_user_instances')})"
|
|
3773
|
+
)
|
|
3732
3774
|
return None
|
|
3733
3775
|
|
|
3734
3776
|
|
|
3777
|
+
def runtime_resource_exhaustion_remediation(metrics: dict[str, Any]) -> str:
|
|
3778
|
+
"""Suggest recovery for the specific resource rejected by the kernel."""
|
|
3779
|
+
import errno
|
|
3780
|
+
|
|
3781
|
+
probe_errno = metrics.get("inotify_probe_errno")
|
|
3782
|
+
if probe_errno == errno.ENOSPC:
|
|
3783
|
+
return (
|
|
3784
|
+
"Inspect and stop unneeded file watchers owned by the daemon user, "
|
|
3785
|
+
"or raise fs.inotify.max_user_watches, then retry"
|
|
3786
|
+
)
|
|
3787
|
+
if probe_errno in {errno.EMFILE, errno.ENFILE}:
|
|
3788
|
+
return (
|
|
3789
|
+
"Close unused watcher processes or file descriptors; check "
|
|
3790
|
+
"fs.inotify.max_user_instances and the file descriptor limits, then retry"
|
|
3791
|
+
)
|
|
3792
|
+
return "Stop orphaned agent processes or raise the user/cgroup PID limit, then retry"
|
|
3793
|
+
|
|
3794
|
+
|
|
3735
3795
|
def copilot_auth_state_signature(copilot_home: str | Path) -> str | None:
|
|
3736
3796
|
"""Return a content signature for Copilot's persisted authentication state."""
|
|
3737
3797
|
config_file = Path(copilot_home) / "config.json"
|
|
@@ -1196,7 +1196,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1196
1196
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1197
1197
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1198
1198
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1199
|
-
DAEMON_VERSION = "1.62.
|
|
1199
|
+
DAEMON_VERSION = "1.62.3"
|
|
1200
1200
|
|
|
1201
1201
|
|
|
1202
1202
|
def _detect_client_type() -> str:
|
|
@@ -5919,6 +5919,8 @@ class ProcessManager:
|
|
|
5919
5919
|
lower = (text or "").lower()
|
|
5920
5920
|
if any(p in lower for p in ProcessManager.RATE_LIMIT_PATTERNS):
|
|
5921
5921
|
return "Agent hit rate/usage limit or quota exhaustion"
|
|
5922
|
+
if agent_core.is_runtime_resource_exhaustion_error(lower):
|
|
5923
|
+
return "Agent host OS resources exhausted"
|
|
5922
5924
|
if any(p in lower for p in ProcessManager.AGENT_UNAVAILABLE_PATTERNS):
|
|
5923
5925
|
return "Agent backend unavailable or unreachable"
|
|
5924
5926
|
return None
|
|
@@ -6015,11 +6017,13 @@ class ProcessManager:
|
|
|
6015
6017
|
"all_agents_rate_limited", "all_agents_unavailable", "acceptance_criteria_uncovered",
|
|
6016
6018
|
}:
|
|
6017
6019
|
return None
|
|
6018
|
-
if result.failure_code
|
|
6020
|
+
if result.failure_code in {"agent_platform_unsupported", "runtime_resource_exhausted"}:
|
|
6019
6021
|
return "agent_unavailable"
|
|
6020
6022
|
error_text = ProcessManager._failure_pattern_channels(result).lower()
|
|
6021
6023
|
if any(p in error_text for p in ProcessManager.RATE_LIMIT_PATTERNS):
|
|
6022
6024
|
return "rate_limit"
|
|
6025
|
+
if agent_core.is_runtime_resource_exhaustion_error(error_text):
|
|
6026
|
+
return "agent_unavailable"
|
|
6023
6027
|
if any(p in error_text for p in ProcessManager.AGENT_UNAVAILABLE_PATTERNS):
|
|
6024
6028
|
return "agent_unavailable"
|
|
6025
6029
|
return None
|
|
@@ -6192,9 +6196,8 @@ class ProcessManager:
|
|
|
6192
6196
|
"blocking": True,
|
|
6193
6197
|
"failure_class": "resource_exhausted",
|
|
6194
6198
|
"detail": resource_reason,
|
|
6195
|
-
"remediation": (
|
|
6196
|
-
|
|
6197
|
-
"then retry"
|
|
6199
|
+
"remediation": agent_core.runtime_resource_exhaustion_remediation(
|
|
6200
|
+
resource_metrics,
|
|
6198
6201
|
),
|
|
6199
6202
|
}
|
|
6200
6203
|
logger.error(
|
|
@@ -8741,9 +8744,8 @@ class RuntimePreflightMonitor:
|
|
|
8741
8744
|
"blocking": True,
|
|
8742
8745
|
"failure_class": "resource_exhausted",
|
|
8743
8746
|
"detail": reason,
|
|
8744
|
-
"remediation": (
|
|
8745
|
-
|
|
8746
|
-
"then restart the daemon"
|
|
8747
|
+
"remediation": agent_core.runtime_resource_exhaustion_remediation(
|
|
8748
|
+
metrics,
|
|
8747
8749
|
),
|
|
8748
8750
|
}
|
|
8749
8751
|
if metrics:
|
|
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
|