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.
Files changed (29) hide show
  1. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/PKG-INFO +1 -1
  2. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/agent_core.py +63 -3
  4. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/daemon.py +10 -8
  5. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/PKG-INFO +1 -1
  6. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/pyproject.toml +1 -1
  7. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/README.md +0 -0
  8. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/_build_config.py +0 -0
  9. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/_local_bind.py +0 -0
  10. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/autoupgrade.py +0 -0
  11. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/main.py +0 -0
  12. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/py.typed +0 -0
  13. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/runtime_evidence.py +0 -0
  14. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli/zcode_protocol.py +0 -0
  15. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  16. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  17. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/entry_points.txt +0 -0
  18. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/requires.txt +0 -0
  19. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/forgexa_cli.egg-info/top_level.txt +0 -0
  20. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/setup.cfg +0 -0
  21. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_auth_and_runtime_commands.py +0 -0
  22. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_autoupgrade.py +0 -0
  23. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_check_command.py +0 -0
  24. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_expiry_warnings_and_revoke.py +0 -0
  25. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_local_bind_commands.py +0 -0
  26. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.62.2 → forgexa_cli-1.62.3}/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.62.2
3
+ Version: 1.62.3
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.62.2"
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 to create an agent process or thread."""
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 headroom without optional dependencies."""
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 an agent lacks safe process headroom."""
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.2"
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 == "agent_platform_unsupported":
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
- "Stop orphaned agent processes or raise the user/cgroup PID limit, "
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
- "Stop orphaned agent processes or raise the user/cgroup PID limit, "
8746
- "then restart the daemon"
8747
+ "remediation": agent_core.runtime_resource_exhaustion_remediation(
8748
+ metrics,
8747
8749
  ),
8748
8750
  }
8749
8751
  if metrics:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.62.2
3
+ Version: 1.62.3
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.62.2"
3
+ version = "1.62.3"
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