fix: honour gateway fatal-config exit code in installed daemons#3162
fix: honour gateway fatal-config exit code in installed daemons#3162praisonai-triage-agent[bot] wants to merge 3 commits into
Conversation
β¦#3160) Installed supervisor units restarted the gateway unconditionally, so the runtime's fatal-config exit (78, EX_CONFIG) was discarded and a broken config crash-looped forever. Map the existing core exit-code contract to per-OS restart directives: - systemd: Restart=on-failure + RestartPreventExitStatus=78 + StartLimitBurst=5 (bounded restarts). - launchd: KeepAlive dict (SuccessfulExit=false, Crashed=true) + ThrottleInterval so a clean/fatal exit is not relaunched every few seconds. - windows: wrap the scheduled task / startup script so exit 78 maps to a clean stop instead of a relaunch. Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
|
@coderabbitai review |
|
/review |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more β On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
β Action performedReview finished.
|
|
Important Review skippedBot user detected. To trigger a single review, invoke the βοΈ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
π WalkthroughWalkthroughDaemon generators now avoid unconditional restart loops. systemd, launchd, and Windows handle clean exits, crashes, and fatal configuration exit code 78 with platform-specific restart or stop policies, and unit tests verify the generated configurations. ChangesDaemon restart policy
Estimated code review effort: 3 (Moderate) | ~20 minutes π₯ Pre-merge checks | β 5β Passed checks (5 passed)
β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR makes installed gateway daemons honor the existing process exit-code contract. The main changes are:
Confidence Score: 5/5No additional blocking issue qualifies for this follow-up round.
Important Files Changed
Reviews (3): Last reviewed commit: "test: align windows scheduled-task test ..." | Re-trigger Greptile |
| WorkingDirectory={working_dir} | ||
| ExecStart={python} -m praisonai_bot gateway start --config {abs_config} | ||
| Restart=always | ||
| Restart=on-failure |
There was a problem hiding this comment.
Transient Failures Look Successful
The generated unit runs gateway start, whose command handler discards the status returned by GatewayHandler.start(). A transient failure classified as exit 75 therefore reaches systemd as exit 0, and this changed Restart=on-failure policy leaves the daemon stopped instead of restarting it.
|
@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task ββ View job Final Architecture Review β PR #3162 β
Critical root cause (fixed)The generated units all invoke
The entire PR was inert without this. The parallel Fixes pushed (commit
|
| File | Change |
|---|---|
daemon/../commands/gateway.py |
gateway_start now surfaces start()'s code via raise typer.Exit(code) so systemd/launchd/Windows actually see 0/75/78. (Greptile P1) |
daemon/windows.py |
Scheduled task now points at the generated .cmd wrapper (single, well-formed quoting) instead of the fragile inline cmd /c "<python> β¦ & if %ERRORLEVEL% β¦" string that nested quotes and misparsed paths under C:\Program Files. Wrapper already owns exit-78 translation; uninstall already removes it. (Greptile P1) |
tests/unit/cli/test_gateway_exit_codes.py |
+2 tests: praisonai_bot gateway start propagates fatal (78) and clean (0). |
tests/unit/test_bot_gap_features.py |
+1 test: Windows task command is the wrapper path (no cmd /c, no %ERRORLEVEL% nesting), wrapper written + owns exit-78. |
Tests: 19/19 pass (11 exit-code incl. 2 new + 8 daemon-generation incl. new Windows test). Remaining failures in the suite are unrelated env gaps (missing pytest-asyncio/other packages).
Reviewed but intentionally not changed
- Greptile P1 "launchd fatal restarts unbounded" β acknowledged limitation, not fixed. launchd has no exit-code-specific stop (unlike systemd/Windows). A truly bounded fatal-stop would need a persistent on-disk restart guard (the in-memory
RestartLoopGuardresets each process). That's meaningful new surface area / scope creep for a wrapper-only fix. Current mitigation (ThrottleInterval=30bounding to ~1 relaunch/30s + the runtime guard) is the pragmatic ceiling and is already documented in the plist comment. Flagging as a known trade-off rather than expanding scope.
Verdict β β Approve (with the launchd caveat noted)
Now that the exit code actually reaches the supervisor, the systemd RestartPreventExitStatus=78 and Windows exit-78 mapping deliver the fix's intent. Change is protocol-driven (reuses the existing core GATEWAY_FATAL_CONFIG_EXIT_CODE contract), wrapper-only, backward-compatible, no Agent-class bloat, no new deps. The launchd path is best-effort by OS constraint.
Scope: only Python praisonai-bot wrapper touched; no praisonaiagents core, praisonai-rust, or praisonai-ts changes.
Job run β’ branch claude/issue-3160-20260718-0914
There was a problem hiding this comment.
Actionable comments posted: 1
π€ Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/praisonai-bot/praisonai_bot/daemon/windows.py`:
- Around line 65-74: Update the task_command construction around inner and
GATEWAY_FATAL_CONFIG_EXIT_CODE to use a runtime-safe Windows exit-code check,
such as delayed expansion with cmd /V:ON or the adjacent if errorlevel range
checks. Preserve the behavior of mapping only the exact fatal-config exit code
to a clean exit while allowing other gateway exit codes through.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8002f17d-8171-4ef6-8eb1-9a01240a381e
π Files selected for processing (4)
src/praisonai-bot/praisonai_bot/daemon/launchd.pysrc/praisonai-bot/praisonai_bot/daemon/systemd.pysrc/praisonai-bot/praisonai_bot/daemon/windows.pysrc/praisonai-bot/tests/unit/test_bot_gap_features.py
| inner = subprocess.list2cmdline( | ||
| [python, "-m", "praisonai_bot", "gateway", "start", "--config", abs_config] | ||
| ) | ||
| # Wrap in cmd so the gateway fatal-config exit (78, EX_CONFIG) is mapped to a | ||
| # clean exit β the task then stays stopped instead of relaunching on a | ||
| # fatally broken config. | ||
| task_command = ( | ||
| f'cmd /c "{inner} & if %ERRORLEVEL% EQU ' | ||
| f'{GATEWAY_FATAL_CONFIG_EXIT_CODE} exit /b 0"' | ||
| ) |
There was a problem hiding this comment.
π― Functional Correctness | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
# Map the target file and inspect the relevant lines.
sed -n '1,140p' src/praisonai-bot/praisonai_bot/daemon/windows.py
echo
echo '---'
echo 'Search for related Windows task command usage and fatal exit handling.'
rg -n "GATEWAY_FATAL_CONFIG_EXIT_CODE|ERRORLEVEL|cmd /c|cmd /V:ON|list2cmdline|gateway start" src/praisonai-bot/praisonai_bot -SRepository: MervinPraison/PraisonAI
Length of output: 10438
π Web query:
Windows cmd /c %ERRORLEVEL% expansion before command execution delayed expansion errorlevel documentation
π‘ Result:
In the Windows Command Processor (cmd.exe), environment variables like %ERRORLEVEL% are expanded at the time the command line or block is read (parsed), not when it is executed [1][2]. When using cmd /c, the entire command string provided is parsed in a single pass [3]. Consequently, if you use %ERRORLEVEL% in a sequence of commands, it will expand to the value that existed before the current command line began execution, not the value produced by any preceding command in that same line [4][3]. To obtain the exit code produced by a command within the same line, you must delay the expansion of the variable until execution time [1][3]. There are two primary methods to achieve this: 1. Use Delayed Expansion: Enable delayed expansion by starting the cmd instance with the /V:ON switch [3][5][6]. Once enabled, use the exclamation point syntax (!ERRORLEVEL!) instead of percent signs [1][3][5]. Example: cmd /V:ON /C "my_command && echo!ERRORLEVEL!" [3][7] 2. Use the CALL command: The CALL command forces a second round of parsing on the provided string [3]. By escaping the percent sign (using a caret ^), you can prevent the first parse from expanding the variable, allowing CALL to evaluate it during the second parse after the preceding command has executed [3][7]. Example: cmd /C "my_command && call echo %^ERRORLEVEL%" [3][7] Note that %ERRORLEVEL% is a dynamic pseudo-environment variable [8][9]. It is maintained by cmd.exe to reflect the exit code of the most recently executed command [10][9]. While it behaves like a standard environment variable, it is technically a fallback mechanism; if a user manually creates an environment variable named ERRORLEVEL, that user variable will take precedence and hide the dynamic value [11][9]. Therefore, it is best practice to avoid manually setting an environment variable named ERRORLEVEL [10]. Furthermore, because %ERRORLEVEL% is an internal value, conditional execution operators like && and || are often more robust alternatives than checking the variable, as they do not rely on variable expansion at all [10].
Citations:
- 1: https://devblogs.microsoft.com/oldnewthing/20060823-00/?p=29993
- 2: https://superuser.com/questions/1618514/cmd-prompt-errorlevel-gets-the-correct-output-value-from-the-second-run
- 3: https://stackoverflow.com/questions/28318643/getting-the-exit-code-of-an-application-started-with-the-cmd-and-start-comma
- 4: https://stackoverflow.com/questions/66248276/difference-between-exit-b-and-exit-b-errorlevel
- 5: https://ss64.com/nt/delayedexpansion.html
- 6: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
- 7: https://stackoverflow.com/questions/41996740/cmd-command-line-one-command-plus-direct-output-of-the-errorlevel-into-a-txt
- 8: https://stackoverflow.com/questions/60459093/how-to-set-errorlevel-in-cmd-c
- 9: https://devblogs.microsoft.com/oldnewthing/20080926-00/?p=20743
- 10: https://ss64.com/nt/errorlevel.html
- 11: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/if
Fix the Windows cmd exit-code check
%ERRORLEVEL% is expanded when cmd /c parses the line, so this can read the value from before the gateway runs. Switch to delayed expansion (cmd /V:ON /C "... & if !ERRORLEVEL! EQU 78 exit /b 0") or use if errorlevel 78 if not errorlevel 79 exit /b 0 so fatal-config exits stop the task instead of crash-looping.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai-bot/praisonai_bot/daemon/windows.py` around lines 65 - 74,
Update the task_command construction around inner and
GATEWAY_FATAL_CONFIG_EXIT_CODE to use a runtime-safe Windows exit-code check,
such as delayed expansion with cmd /V:ON or the adjacent if errorlevel range
checks. Preserve the behavior of mapping only the exact fatal-config exit code
to a clean exit while allowing other gateway exit codes through.
β¦arden Windows task quoting (#3160) The installed daemons run python -m praisonai_bot gateway start, whose Typer command discarded GatewayHandler.start()'s int return. Typer ignores a returned int, so the process always exited 0 - the generated units' Restart=on-failure / RestartPreventExitStatus=78 / KeepAlive.SuccessfulExit directives never saw the real fatal-config (78) / transient (75) code, making the whole feature inert. Surface it via typer.Exit. Also replace the Windows scheduled-task's fragile inline cmd /c string (nested quotes that misparse paths under C:\Program Files) with a single quoted path to the generated .cmd wrapper, which already owns the exit-78 translation. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
|
@claude CI failed on HEAD Failed checks
Failures (extracted)
Critical review firstBefore changing code or tests, decide which side is wrong:
SDK guardrails (AGENTS.md):
What to do
|
|
Claude finished @MervinPraison's task ββ View job CI fix pushed β PR #3162 β
Verdict: legitimate feature change (test needed updating, not the code)The failing test This PR intentionally changed I updated the test to guard the new, intended contract rather than weaken it:
This keeps the test protecting SDK behaviour β it now verifies the well-formedness guarantee that motivated the change, and would fail if anyone reintroduced the nested inline command. Files modified
Not changed (with reason)
Tests
Scope: only the Job run β’ branch |
The pre-existing test_windows_scheduled_task_command_is_well_formed asserted `--config` appeared inline in the /TR value. This PR intentionally routes the scheduled task at the generated .cmd wrapper (single, well-formed quoting) to avoid the nested-quote hazard for paths with spaces, so /TR now points at the wrapper path and the wrapper owns the --config invocation. Assert the new, intended contract instead of the outdated implementation detail. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
Fixes #3160
Summary
Installed supervisor units (systemd/launchd/Windows) restarted the gateway unconditionally, so the runtime's fatal-config exit
78(EX_CONFIGβ "do not restart me") was discarded and a genuinely broken config produced an infinite crash-loop. This maps the existing core exit-code contract (GATEWAY_FATAL_CONFIG_EXIT_CODE,RestartLoopGuard) onto per-OS restart directives so the generated units honour the "stop" intent.Changes (wrapper only β
praisonai-bot)daemon/systemd.py):Restart=on-failure,SuccessExitStatus=0,RestartPreventExitStatus=78, plusStartLimitIntervalSec=60/StartLimitBurst=5to bound restart storms (mirrorsRestartLoopGuard).daemon/launchd.py):KeepAlivechanged from unconditional<true/>to a dict (SuccessfulExit=false,Crashed=true) +ThrottleInterval=30. launchd has no exit-code-specific stop, so a fatal-config exit is bounded by throttle + the runtime guard instead of relaunching every ~5s.daemon/windows.py): scheduled-task command and startup.cmdwrapped so exit78maps to a clean stop instead of a relaunch.Test
test_systemd_unit_generation/test_launchd_plist_generationand addedtest_windows_startup_script_honours_fatal_config_exit.No core changes β the contract already existed in
praisonaiagents/gateway/protocols.py; this only makes the wrapper's generated units consume it. Backward-compatible.Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests