forgexa-cli 1.67.0__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.
Files changed (31) hide show
  1. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/PKG-INFO +1 -1
  2. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/agent_core.py +44 -12
  4. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/aws_logs_evidence.py +3 -1
  5. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/daemon.py +17 -6
  6. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/PKG-INFO +1 -1
  7. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/pyproject.toml +1 -1
  8. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/README.md +0 -0
  9. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/_build_config.py +0 -0
  10. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/_local_bind.py +0 -0
  11. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/autoupgrade.py +0 -0
  12. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/aws_evidence.py +0 -0
  13. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/main.py +0 -0
  14. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/py.typed +0 -0
  15. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/runtime_evidence.py +0 -0
  16. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli/zcode_protocol.py +0 -0
  17. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  18. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  19. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/entry_points.txt +0 -0
  20. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/requires.txt +0 -0
  21. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/forgexa_cli.egg-info/top_level.txt +0 -0
  22. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/setup.cfg +0 -0
  23. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_auth_and_runtime_commands.py +0 -0
  24. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_autoupgrade.py +0 -0
  25. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_check_command.py +0 -0
  26. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_expiry_warnings_and_revoke.py +0 -0
  27. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_local_bind_commands.py +0 -0
  28. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_runtime_credentials.py +0 -0
  29. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_session_credentials.py +0 -0
  30. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/tests/test_silent_install.py +0 -0
  31. {forgexa_cli-1.67.0 → forgexa_cli-1.68.0}/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.67.0
3
+ Version: 1.68.0
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.67.0"
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 any(
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
- pid_dir = cgroup_path / relative
3795
+ controller_root = cgroup_path
3790
3796
  elif "pids" in controllers.split(","):
3791
- pid_dir = cgroup_path / "pids" / relative
3797
+ controller_root = (cgroup_path / "pids").resolve()
3792
3798
  else:
3793
3799
  continue
3794
- current_text = (pid_dir / "pids.current").read_text().strip()
3795
- maximum_text = (pid_dir / "pids.max").read_text().strip()
3796
- if maximum_text != "max":
3797
- constraints.append(("cgroup", int(current_text), int(maximum_text)))
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 < 32:
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 and evidence["events"]:
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.67.0"
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
- if process is None:
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 agent_core.is_runtime_resource_exhaustion_error(
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"]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.67.0
3
+ Version: 1.68.0
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.67.0"
3
+ version = "1.68.0"
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