fix: persist desktop training state so a restart survives the live run - #4510
Conversation
fixes #4492) The training subsystem kept the live run and history in memory only, so any engine restart lost the run: /train/runs returned empty, /train/stop had nothing to stop, and start() launched a second trainer beside a live one -- the OOM that "one GPU runs one job" exists to refuse. Each run now writes a run.json (state + pid) at spawn, on the RUNNING/STOPPING transition, and on finish. Trainer.__init__ reloads from disk: a run whose pid is still alive is adopted (current is set, so start still refuses and stop can reach it via its pid's process group); an interrupted run is marked failed. The engine stays stdlib-only. Adds SurvivingARestart tests: a live run reappears, a second fine-tune is refused, stop kills the adopted pid, an interrupted run reports failed, and a finished run is not re-adopted. 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? |
Greptile SummaryThe PR adds durable desktop training-run state, restart-time history reconstruction and process adoption, atomic state-file replacement, and stopping of adopted process groups. The initial-state fix remains incomplete for an engine termination immediately after successful process creation but before the PID reaches disk.
Confidence Score: 4/5The PR is not yet safe to merge because an engine exit between process creation and durable PID recording can still orphan a live trainer and permit a duplicate GPU run. Process creation precedes durable PID persistence, and reload deliberately marks a surviving pid-less nonterminal record failed without setting Files Needing Attention: src/praisonai-desktop/engine/training.py
|
| Filename | Overview |
|---|---|
| src/praisonai-desktop/engine/training.py | Adds restart-safe training state and adopted-process control, but durable PID recording still occurs after the child becomes live. |
| src/praisonai-desktop/engine/test_training.py | Adds substantial restart and persistence coverage, but the live-child/pid-less-record interruption window is not exercised. |
Reviews (2): Last reviewed commit: "fix: make desktop training state writes ..." | Re-trigger Greptile
| try: | ||
| with open(self._state_path(run.id), "w", encoding="utf-8") as fh: |
There was a problem hiding this comment.
When the engine stops while _persist is rewriting run.json, the direct open(..., "w") and json.dump can leave empty or partial JSON. _reload silently skips that record, causing the live run to disappear from history, become unreachable through stop, and no longer prevent a second trainer from starting.
Knowledge Base Used: Persistence, security, and configuration
|
@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: Review changes in this PR. Python SDK: praisonaiagents, praisonai. TypeScript SDK: src/praisonai-ts/. Do NOT modify src/praisonai-rust. 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 #4510 ✅
Phase 1 — AGENTS.md review
Phase 2 — Fixes implementedReviewed all bot findings (Greptile, Qodo, CodeRabbit). Qodo was paused; Greptile raised three, two valid:
Files modified
Skipped: PID-reuse hardening (documented tradeoff), corrupt-file fuzzing beyond the atomicity guarantee. Phase 3 — VerdictApprove. The two valid restart-recovery bugs are fixed with minimal, stdlib-only changes;
Job run · branch |
…n gap Two valid restart-recovery bugs from PR review: - _persist now writes a temp file, fsyncs and os.replaces it, so an engine killed mid write can never leave partial JSON that _reload silently skips (which would lose the live run from history, stop() and the single-GPU guard). - start() persists the run (pending) before the supervisor thread exists, and the spawn-failure branch persists its FAILED state, so a child spawned before the first post-spawn _persist is never left with no run.json. Adds two tests. PID reuse is left as the documented minimal tradeoff. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
| # Written now, before the RUNNING transition, so a crash in the next | ||
| # instruction still leaves a pid on disk for the next process to adopt | ||
| # or reap rather than a run that looks like it never spawned. | ||
| self._persist(run) |
There was a problem hiding this comment.
When the engine exits after _spawn returns but before _persist(run) records the PID, reload marks the pid-less record failed and leaves current unset while the child continues training. /train/stop therefore cannot reach the child, and another training run can start beside it, potentially causing GPU OOM.
|
Merge gate scan — eligible for assessment. Claude merge gate will assess and may auto-merge if |
|
Merged by Claude PR merge gate ( |
Fixes #4492
Problem
The desktop training subsystem (
src/praisonai-desktop/engine/training.py) held the live run and history in memory only. After any engine restart (crash, relaunch, kill):/train/runsreturned[]while the run directory sat on disk/train/stopanswered "nothing was running"start()launched a second trainer beside the live one — the OOM thatTrainer.start's own docstring says must be refusedOnly
config.yamlandtrain.logwere on disk; there was no state record at all.Fix (minimal, stdlib-only)
Run.pid— records the child's pid, which outlives the restart (_procdoes not).Trainer._persist(run)— writes a per-runrun.json(summary()+ pid) at spawn, on theRUNNING/STOPPINGtransition, and on finish.Trainer._reload()(called in__init__) — rebuildshistoryfrom disk. A run whose pid is still alive is adopted (currentset, sostartstill refuses andstopcan reach it); an interrupted run whose pid is gone is marked failed ("the engine restarted while this run was live").stop()— for an adopted run with no_proc, falls back to_terminate_pid_group(run.pid)and records the ending itself._pid_alive/_terminate_pid_group— POSIXos.kill(pid, 0)/killpg, WindowsOpenProcess/taskkill /T, both with the same "never signal our own group" guard as_terminate_group.Tests
New
SurvivingARestartclass (real subprocesses, real SIGTERM) — a secondTrainerover the same home stands in for the process after a restart:runningstop()actually kills the adopted pid →cancelledfailed, not missing, and is not adoptedAll 60 tests pass (
python -m unittest test_training).Generated with Claude Code