forgexa-cli 1.13.1__tar.gz → 1.13.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.13.1
3
+ Version: 1.13.2
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: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.13.1"
2
+ __version__ = "1.13.2"
@@ -523,7 +523,7 @@ except (ImportError, ModuleNotFoundError):
523
523
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
524
524
  # Kept in sync with pyproject.toml version via bump-version.sh.
525
525
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
526
- DAEMON_VERSION = "1.13.1"
526
+ DAEMON_VERSION = "1.13.2"
527
527
 
528
528
 
529
529
  def _detect_client_type() -> str:
@@ -2792,8 +2792,11 @@ class ProcessManager:
2792
2792
  msg_data = data.get("data") or {}
2793
2793
  if isinstance(msg_data.get("content"), str) and msg_data["content"].strip():
2794
2794
  has_result = True
2795
+ elif ev_type == "assistant.message_start":
2796
+ has_assistant_events = True
2795
2797
  elif ev_type == "assistant.message_delta":
2796
2798
  has_meaningful_content = True
2799
+ has_assistant_events = True
2797
2800
  # ── Generic / Claude "result" event ────────────────────────────
2798
2801
  elif ev_type == "result":
2799
2802
  # Copilot result format: {"type":"result","exitCode":0,"usage":{...}}
@@ -2916,6 +2919,19 @@ class ProcessManager:
2916
2919
  or signals["has_meaningful_content"]
2917
2920
  )
2918
2921
 
2922
+ @staticmethod
2923
+ def _copilot_completed_without_result(stdout: str) -> bool:
2924
+ """Detect Copilot runs that completed a turn but omitted the final result event."""
2925
+ signals = ProcessManager._extract_output_signals(stdout)
2926
+ return bool(
2927
+ signals["has_turn_completed"]
2928
+ and signals["has_assistant_events"]
2929
+ and signals["has_meaningful_content"]
2930
+ and not signals["has_result"]
2931
+ and not signals["has_turn_failed"]
2932
+ and not signals["error_messages"]
2933
+ )
2934
+
2919
2935
  @staticmethod
2920
2936
  def is_rate_limited(result: "TaskResult") -> bool:
2921
2937
  """Check if an agent failure warrants trying a different agent.
@@ -3930,6 +3946,7 @@ class ProcessManager:
3930
3946
  # Copilot always exits 0 on normal completion; check result.exitCode
3931
3947
  # from the JSONL "result" event for a true success signal.
3932
3948
  copilot_exit = self._extract_copilot_exit_code(stdout)
3949
+ completed_without_result = self._copilot_completed_without_result(stdout)
3933
3950
  effective_rc = copilot_exit if copilot_exit is not None else returncode
3934
3951
 
3935
3952
  if effective_rc == 0 and returncode == 0:
@@ -3940,6 +3957,21 @@ class ProcessManager:
3940
3957
  stderr=stderr[-10000:],
3941
3958
  metrics=metrics,
3942
3959
  )
3960
+ elif copilot_exit is None and returncode != 0 and completed_without_result:
3961
+ logger.warning(
3962
+ "Copilot exited with return code %s for task %s despite a completed assistant turn and no result event; recovering to validation path",
3963
+ returncode,
3964
+ task_id,
3965
+ )
3966
+ metrics["recovered_missing_result_event"] = True
3967
+ metrics["recovered_process_returncode"] = returncode
3968
+ return TaskResult(
3969
+ status="success",
3970
+ exit_code=0,
3971
+ stdout=stdout[-settings.AGENT_MAX_OUTPUT_SIZE:],
3972
+ stderr=stderr[-10000:],
3973
+ metrics=metrics,
3974
+ )
3943
3975
  else:
3944
3976
  return TaskResult(
3945
3977
  status="failed",
@@ -247,6 +247,31 @@ def _is_virtualenv() -> bool:
247
247
  )
248
248
 
249
249
 
250
+ def _is_pep668_environment() -> bool:
251
+ """Return True if the active Python is governed by PEP 668 (externally-managed).
252
+
253
+ Debian 12, Ubuntu 22.10+/24.04+, Fedora 38+ and other modern distros place
254
+ an EXTERNALLY-MANAGED marker in their stdlib directory. pip refuses both
255
+ user-site and system-wide installs on these environments unless
256
+ --break-system-packages is explicitly passed — even ``pip install --user``
257
+ is blocked. We detect the marker up-front so ``forgexa upgrade`` can add
258
+ the flag automatically rather than failing and asking the user to do it.
259
+ """
260
+ try:
261
+ import sysconfig as _sysconfig
262
+ stdlib = _sysconfig.get_path("stdlib")
263
+ if stdlib and (Path(stdlib) / "EXTERNALLY-MANAGED").exists():
264
+ return True
265
+ except Exception:
266
+ pass
267
+ try:
268
+ if (Path(sys.prefix) / "EXTERNALLY-MANAGED").exists():
269
+ return True
270
+ except Exception:
271
+ pass
272
+ return False
273
+
274
+
250
275
  def _is_user_site_install(dist_root: Path | None) -> bool:
251
276
  if dist_root is None:
252
277
  return False
@@ -355,6 +380,12 @@ def _build_upgrade_plan(target_version: str | None) -> UpgradePlan:
355
380
  command = [python, "-m", "pip", "install", "--upgrade"]
356
381
  if _is_user_site_install(dist_root) and not _is_virtualenv():
357
382
  command.append("--user")
383
+ if _is_pep668_environment():
384
+ # PEP 668 (Ubuntu 24.04+, Debian 12+, Fedora 38+, …) blocks pip
385
+ # installs — including --user — unless this flag is passed. The
386
+ # original installation on this machine must have used it too, so
387
+ # adding it here is consistent and safe.
388
+ command.append("--break-system-packages")
358
389
  command.append(_package_spec_for_version(target_version))
359
390
  return UpgradePlan(
360
391
  installer="pip",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.13.1
3
+ Version: 1.13.2
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: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.13.1"
3
+ version = "1.13.2"
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 = { text = "MIT" }
@@ -244,6 +244,7 @@ def test_build_upgrade_plan_for_pip_user_site(monkeypatch: pytest.MonkeyPatch) -
244
244
  monkeypatch.setattr(main.importlib.util, "find_spec", lambda name: object() if name == "pip" else None)
245
245
  monkeypatch.setattr(main, "_is_user_site_install", lambda root: True)
246
246
  monkeypatch.setattr(main, "_is_virtualenv", lambda: False)
247
+ monkeypatch.setattr(main, "_is_pep668_environment", lambda: False)
247
248
  monkeypatch.setattr(main.sys, "executable", "/usr/bin/python3")
248
249
 
249
250
  plan = main._build_upgrade_plan("1.12.3")
@@ -260,6 +261,41 @@ def test_build_upgrade_plan_for_pip_user_site(monkeypatch: pytest.MonkeyPatch) -
260
261
  ]
261
262
 
262
263
 
264
+ def test_build_upgrade_plan_adds_break_system_packages_on_pep668(monkeypatch: pytest.MonkeyPatch) -> None:
265
+ monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.13.1"))
266
+ monkeypatch.setattr(main, "_distribution_root", lambda dist: Path("/usr/lib/python3/dist-packages"))
267
+ monkeypatch.setattr(main, "_running_from_distribution_root", lambda root: True)
268
+ monkeypatch.setattr(main, "_direct_url_metadata", lambda dist: None)
269
+ monkeypatch.setattr(main, "_looks_like_pipx_environment", lambda python=None: False)
270
+ monkeypatch.setattr(main.importlib.util, "find_spec", lambda name: object() if name == "pip" else None)
271
+ monkeypatch.setattr(main, "_is_user_site_install", lambda root: True)
272
+ monkeypatch.setattr(main, "_is_virtualenv", lambda: False)
273
+ monkeypatch.setattr(main, "_is_pep668_environment", lambda: True)
274
+ monkeypatch.setattr(main.sys, "executable", "/usr/bin/python3")
275
+
276
+ plan = main._build_upgrade_plan(None)
277
+
278
+ assert "--break-system-packages" in plan.command
279
+ assert "--user" in plan.command
280
+ assert plan.command[-1] == "forgexa-cli"
281
+
282
+
283
+ def test_pep668_detection_returns_false_in_normal_venv(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
284
+ monkeypatch.setattr(main.sys, "prefix", str(tmp_path))
285
+ import sysconfig as _sc
286
+ monkeypatch.setattr(_sc, "get_path", lambda name, **kw: str(tmp_path))
287
+
288
+ assert main._is_pep668_environment() is False
289
+
290
+
291
+ def test_pep668_detection_returns_true_when_marker_present(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
292
+ (tmp_path / "EXTERNALLY-MANAGED").write_text("[externally-managed]\n")
293
+ import sysconfig as _sc
294
+ monkeypatch.setattr(_sc, "get_path", lambda name, **kw: str(tmp_path))
295
+
296
+ assert main._is_pep668_environment() is True
297
+
298
+
263
299
  def test_build_upgrade_plan_rejects_editable_install(monkeypatch: pytest.MonkeyPatch) -> None:
264
300
  monkeypatch.setattr(main, "_cli_distribution", lambda: SimpleNamespace(version="1.12.2"))
265
301
  monkeypatch.setattr(main, "_distribution_root", lambda dist: Path("/installed/site-packages"))
File without changes
File without changes