fix: improve codex exec error rendering (#18)
This commit is contained in:
@@ -409,7 +409,7 @@ async def handle_message(
|
||||
if resume_token_value is None:
|
||||
resume_token_value = progress_renderer.resume_token
|
||||
progress_renderer.resume_token = resume_token_value
|
||||
err_body = f"Error:\n{error}"
|
||||
err_body = str(error)
|
||||
final_md = progress_renderer.render_final(elapsed, err_body, status="error")
|
||||
logger.debug("[error] markdown: %s", final_md)
|
||||
final_msg, edited = await _send_or_edit_markdown(
|
||||
@@ -463,9 +463,9 @@ async def handle_message(
|
||||
final_answer = answer
|
||||
if run_ok is False and run_error:
|
||||
if final_answer.strip():
|
||||
final_answer = f"{final_answer}\n\nError:\n{run_error}"
|
||||
final_answer = f"{final_answer}\n\n{run_error}"
|
||||
else:
|
||||
final_answer = f"Error:\n{run_error}"
|
||||
final_answer = str(run_error)
|
||||
|
||||
status = (
|
||||
"error" if run_ok is False else ("done" if final_answer.strip() else "error")
|
||||
|
||||
@@ -39,6 +39,25 @@ _ACTION_KIND_MAP: dict[str, ActionKind] = {
|
||||
}
|
||||
|
||||
_RESUME_RE = re.compile(r"(?im)^\s*`?codex\s+resume\s+(?P<token>[^`\s]+)`?\s*$")
|
||||
_ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
|
||||
_TRUSTED_DIR_RE = re.compile(r"not inside a trusted directory", re.IGNORECASE)
|
||||
|
||||
|
||||
def _strip_ansi(text: str) -> str:
|
||||
return _ANSI_ESCAPE_RE.sub("", text)
|
||||
|
||||
|
||||
def _extract_stderr_reason(stderr_tail: str) -> str | None:
|
||||
if not stderr_tail:
|
||||
return None
|
||||
cleaned = _strip_ansi(stderr_tail)
|
||||
lines = [line.strip() for line in cleaned.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
return None
|
||||
for line in lines:
|
||||
if _TRUSTED_DIR_RE.search(line):
|
||||
return line
|
||||
return lines[-1]
|
||||
|
||||
|
||||
def _started_event(token: ResumeToken, *, title: str) -> StartedEvent:
|
||||
@@ -558,6 +577,10 @@ class CodexRunner(ResumeTokenMixin, JsonlSubprocessRunner):
|
||||
stderr_tail: str,
|
||||
state: CodexRunState,
|
||||
) -> list[TakopiEvent]:
|
||||
reason = _extract_stderr_reason(stderr_tail)
|
||||
if reason:
|
||||
message = f"codex exec failed (rc={rc}).\n\n{reason}"
|
||||
else:
|
||||
message = f"codex exec failed (rc={rc})."
|
||||
resume_for_completed = found_session or resume
|
||||
return [
|
||||
|
||||
@@ -243,6 +243,30 @@ async def test_codex_runner_preserves_warning_order(tmp_path) -> None:
|
||||
assert seen[2].answer == "ok"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_codex_runner_includes_stderr_reason(tmp_path) -> None:
|
||||
codex_path = tmp_path / "codex"
|
||||
codex_path.write_text(
|
||||
"#!/usr/bin/env python3\n"
|
||||
"import sys\n"
|
||||
"\n"
|
||||
"sys.stderr.write('Not inside a trusted directory and --skip-git-repo-check was not specified.\\n')\n"
|
||||
"sys.stderr.flush()\n"
|
||||
"sys.exit(1)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
codex_path.chmod(0o755)
|
||||
|
||||
runner = CodexRunner(codex_cmd=str(codex_path), extra_args=[])
|
||||
events = [evt async for evt in runner.run("hi", None)]
|
||||
|
||||
completed = next(evt for evt in events if isinstance(evt, CompletedEvent))
|
||||
assert completed.ok is False
|
||||
assert completed.error is not None
|
||||
assert "codex exec failed (rc=1)." in completed.error
|
||||
assert "\n\nNot inside a trusted directory" in completed.error
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_run_serializes_two_new_sessions_same_thread(
|
||||
tmp_path, monkeypatch
|
||||
|
||||
Reference in New Issue
Block a user