Skip to content

fix(dashboard): supervise next-server for self-heal; sync plugin-root.txt#135

Closed
wenchanghan wants to merge 1 commit into
ReflexioAI:mainfrom
wenchanghan:fix/dashboard-supervisor-and-plugin-root-sync
Closed

fix(dashboard): supervise next-server for self-heal; sync plugin-root.txt#135
wenchanghan wants to merge 1 commit into
ReflexioAI:mainfrom
wenchanghan:fix/dashboard-supervisor-and-plugin-root-sync

Conversation

@wenchanghan

@wenchanghan wenchanghan commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What's broken

The claude-smart dashboard (Next.js on :3001) had no process supervisor. When next-server died — a crash, or a macOS jetsam pressure-kill — nothing restarted it until the next real SessionStart hook happened to run dashboard-service.sh start again, leaving the dashboard down for as long as Claude Code stayed closed (reported as a ~41-minute outage, PID 54172, with the log ending mid-stream and a pid-file tombstone).

Separately, the ~/.reflexio/plugin-root markers drift on macOS/Linux. ensure-plugin-root.sh refreshes the symlink on every SessionStart, but plugin-root.txt is written only by the installer, at a different time. After a version change the file can point at a stale — even deleted — root while the symlink is correct. That file is the fallback consulted by OpenCode's server.mts and by Windows junction consumers.

What changed

Dashboard self-heal — plugin/scripts/dashboard-supervise.sh (new)

  • Runs next start in a respawn loop so a silent death self-heals within seconds instead of waiting for the next SessionStart.
  • trap … TERM/INT breaks the loop so an intentional stop (process-group kill via claude_smart_kill_tree) tears the supervisor and its next-server child down together — no respawn after an intentional stop.
  • Crash-loop guard: exits faster than HEALTHY_SECS are counted; after MAX_FAILS consecutive fast failures the supervisor gives up and defers recovery to the next SessionStart, so a broken build can't tight-loop. All three thresholds are env-tunable.
  • dashboard-service.sh now spawns the supervisor detached as the session leader and records its pid in dashboard.pid. The previous retry-once block is removed — the supervisor owns retries, and re-spawning there would start a second supervisor racing for the port.
  • Does not fight an intentional stop: if next-server exits via SIGTERM (143) — the port reaper in stop/reinstall, when it can't see this supervisor's pid — the supervisor exits rather than resurrecting the dashboard. Only crashes and jetsam SIGKILLs are respawned. (A stop that does see our pid group-kills the supervisor via the TERM trap.)

Plugin-root marker sync — plugin/scripts/ensure-plugin-root.sh

  • Write plugin-root.txt on every symlink rewrite, and reconcile it from the link on the two no-rewrite exit paths (cache-tracking-unchanged and valid-link self-heal), so the file always matches the symlink.

Scope / relation to existing issues

Testing

  • New tests (isolated ports / fake binaries — they do not touch a real dashboard): supervisor respawn bounded by the crash-loop guard; exit-without-respawn on an intentional stop (child 143); plugin-root.txt reconcile from a stale value on a valid link. The log-redirection assertion is updated for the new spawn line.
  • tests/test_install_scripts.py runs green locally.
  • Live-verified on a running dashboard: killing next-server respawns it in ~2s and restores HTTP 200; a process-group TERM stops the supervisor with no respawn.

Heads-up (pre-existing, not fixed here): the node install/uninstall execution tests run dashboard-service.sh stop, whose marker/reap curl targets the hardcoded 127.0.0.1:3001 regardless of the test's isolated HOME — so they reap a developer's real dashboard if one is running on :3001. With this supervisor that reap is now cleanly honored (the child 143 → supervisor exits) instead of leaving a dead process, but the test-isolation gap itself is worth a separate fix (run those tests on a throwaway DASHBOARD_PORT).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved dashboard startup and resilience by supervising the Next.js dashboard process and automatically restarting it after unexpected exits.
    • Added crash-loop protection to stop repeated rapid restarts after sustained failures.
    • Ensured dashboard shutdown/TERM handling stops supervised processes without triggering unintended respawns.
    • Kept plugin-root metadata synchronized with the active installation link for consistency.
  • Tests
    • Expanded coverage for dashboard self-healing, shutdown behavior, crash-loop limits, and plugin-root synchronization.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard startup now uses a dedicated supervisor that respawns quickly failing dashboard processes and stops them cleanly. Startup timeout handling is simplified, and plugin-root metadata is reconciled from valid links across multiple paths.

Changes

Dashboard and plugin-root lifecycle

Layer / File(s) Summary
Dashboard supervisor and startup integration
plugin/scripts/dashboard-service.sh, plugin/scripts/dashboard-supervise.sh
Dashboard startup delegates to the supervisor, which manages next-server restarts, stop signals, healthy-runtime tracking, and crash-loop limits.
Plugin-root metadata reconciliation
plugin/scripts/ensure-plugin-root.sh
Valid link targets now synchronize plugin-root.txt during link creation, cache tracking, and self-healing.
Lifecycle behavior tests
tests/test_install_scripts.py
Tests cover supervisor wiring, bounded respawns, Windows launch redirection, and stale metadata synchronization.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant dashboard-service.sh
  participant dashboard-supervise.sh
  participant next-server
  dashboard-service.sh->>dashboard-supervise.sh: Start with next binary and port
  dashboard-supervise.sh->>next-server: Launch dashboard
  next-server-->>dashboard-supervise.sh: Exit
  dashboard-supervise.sh->>next-server: Respawn after fast failure
  dashboard-service.sh->>dashboard-supervise.sh: Stop process group
  dashboard-supervise.sh->>next-server: Terminate child
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: dashboard supervision for self-healing and syncing plugin-root.txt.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@plugin/scripts/dashboard-supervise.sh`:
- Around line 57-62: Replace the external date-based assignments for started and
ended in the dashboard supervision flow with Bash’s built-in SECONDS value,
preserving the elapsed-time calculation used after wait and preventing
zero-duration results when date is unavailable.
🪄 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 Plus

Run ID: cf9f49dc-740a-4a5f-927a-21e1394eba35

📥 Commits

Reviewing files that changed from the base of the PR and between 0a40ef7 and 598b322.

📒 Files selected for processing (4)
  • plugin/scripts/dashboard-service.sh
  • plugin/scripts/dashboard-supervise.sh
  • plugin/scripts/ensure-plugin-root.sh
  • tests/test_install_scripts.py

Comment on lines +57 to +62
started="$(date +%s 2>/dev/null || echo 0)"
"$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 &
child=$!
wait "$child"
code=$?
ended="$(date +%s 2>/dev/null || echo 0)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use Bash's built-in $SECONDS to prevent a potential crash-loop bug and avoid subshells.

If the environment's date command does not support +%s (e.g., in some lightweight environments), the fallback echo 0 causes both started and ended to evaluate to 0. Their difference will always be 0, forcing the supervisor to increment fails on every exit and permanently give up after hitting MAX_FAILS, regardless of how long the dashboard actually stayed up.

Using Bash's built-in $SECONDS tracks elapsed time natively, avoiding external process forks and completely eliminating this failure mode.

♻️ Proposed refactor
-  started="$(date +%s 2>/dev/null || echo 0)"
+  started=$SECONDS
   "$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 &
   child=$!
   wait "$child"
   code=$?
-  ended="$(date +%s 2>/dev/null || echo 0)"
+  ended=$SECONDS
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
started="$(date +%s 2>/dev/null || echo 0)"
"$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 &
child=$!
wait "$child"
code=$?
ended="$(date +%s 2>/dev/null || echo 0)"
started=$SECONDS
"$NEXT_BIN" start -p "$PORT" -H 127.0.0.1 &
child=$!
wait "$child"
code=$?
ended=$SECONDS
🤖 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 `@plugin/scripts/dashboard-supervise.sh` around lines 57 - 62, Replace the
external date-based assignments for started and ended in the dashboard
supervision flow with Bash’s built-in SECONDS value, preserving the elapsed-time
calculation used after wait and preventing zero-duration results when date is
unavailable.

….txt

The dashboard could silently die (a crash, or a macOS jetsam pressure-kill of
next-server) with no supervisor, staying down until the next *real* SessionStart
hook happened to run `start` again — observed as a ~41-minute outage. Run
next-server under a new dashboard-supervise.sh that respawns it within seconds,
with a crash-loop guard (bounded consecutive fast failures) and a TERM/INT trap
so an intentional `stop` (process-group kill) tears it down without respawning.
The now-redundant retry-once block in dashboard-service.sh is removed: the
supervisor owns retries, and re-spawning there would start a second supervisor
racing for the port. The supervisor also does not fight an intentional stop: a
child that exits via SIGTERM (143) — the port reaper in `stop`/reinstall — makes
it exit rather than resurrect the dashboard; only crashes and jetsam SIGKILLs
respawn. Symmetric in spirit to the backend gap in ReflexioAI#108; the flap predecessor
ReflexioAI#111 (EADDRINUSE) was fixed in ReflexioAI#119, and coexistence there is kept.

Also fix plugin-root marker drift: on macOS/Linux ensure-plugin-root.sh only
ever wrote the symlink, while plugin-root.txt was written by the installer at a
different time — so after a version change the file could point at a stale (even
deleted) root while the symlink was correct. plugin-root.txt is the fallback
consulted by OpenCode's server.mts and by Windows junction consumers. Write it
on every symlink rewrite and reconcile it from the link on the no-rewrite exits.

Verified: full install-scripts suite green; new tests cover the respawn
crash-loop guard and the marker reconcile; live-tested that killing next-server
respawns it within ~2s and that a process-group TERM stops the supervisor with
no respawn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wenchanghan wenchanghan force-pushed the fix/dashboard-supervisor-and-plugin-root-sync branch from 598b322 to fc9c6a5 Compare July 14, 2026 07:37
@wenchanghan

Copy link
Copy Markdown
Contributor Author

Closing in favor of an upstream Reflexio OSS fix.

Investigation showed the dashboard deaths that motivated this were test-induced (the install-script suite reaps the real :3001 dashboard via a hardcoded stop), not spontaneous. At the claude-smart shell layer, backend and dashboard already recover uniformly on SessionStart on main, so a dashboard-only supervisor was inconsistent and raised supervisor-resilience + cross-platform questions without a matching backend/embedding story.

The one real 'dead but not recovering' gap is the local embedding daemon (8072), owned by the Reflexio backend process: reflexio/cli/utils.py:run_services() monitors children for exit but never respawns them. That's the right, uniform place to fix resiliency, and it's being handed to a reflexio change. The bundled plugin-root.txt marker-sync is a separate concern and is dropped here.

@wenchanghan wenchanghan deleted the fix/dashboard-supervisor-and-plugin-root-sync branch July 15, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant