Skip to content

fix(desktop): start the terminal reaper in the desktop host-service entry - #5438

Merged
Kitenite merged 7 commits into
mainfrom
fix-desktop-host-service-reaper
Jul 3, 2026
Merged

Kitenite merged 7 commits into
mainfrom
fix-desktop-host-service-reaper

Conversation

@Kitenite

@Kitenite Kitenite commented Jul 3, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Wire startTerminalReaper(db) into the desktop host-service entry point. It was only called from packages/host-service/src/serve.ts (the standalone/CLI entry), so the packaged desktop app never ran the terminal reaper at all.
  • This is the actual fix for "ports don't show up after an update until the workspace is selected" — and it also turns on the 5-minute orphan reap (fix(host-service): make workspace delete reliably tear down terminal sessions #5168), which has likewise never run in the desktop app.

Why / Context

#5325 added the reaper-driven port-scan reconcile (register daemon sessions no renderer has attached, e.g. after a host-service restart) and #5430 added 2–90s warm-up retries. Both are correct — but startTerminalReaper was never invoked by apps/desktop/src/main/host-service/index.ts, the entry the desktop app actually spawns. In the shipped app the only portManager.upsertSession caller was the renderer terminal-attach path, so ports for surviving sessions stayed invisible until the user opened the workspace.

Confirmed against a production install (v1.13.0): the asar contains the #5430 code, the daemon adopts within seconds, valid terminal_sessions rows exist — and the host-service log contains zero reaper lines across every run.

Supersedes the revert stack in #5437 (kept as a draft safety net; merging that would not fix this symptom).

How It Works

startTerminalReaper is exported from the @superset/host-service root, and each serving entry (serve.ts and the desktop entry) calls it once in its listen callback — the same per-entry pattern as installProcessSafetyNet, initTerminalBaseEnv, and connectRelay. All reaper logic stays in the package.

Two single-call-site alternatives were tried and rejected during review discussion: starting it inside createApp requires a test-only escape hatch (the reaper is a destructive background actor that reaps integration tests' staged daemon sessions mid-test — 5 failures), and a startHostRuntime wrapper was pure pass-through indirection.

Manual QA

End-to-end against the freshly built dist/main/host-service.js (run under Electron-as-Node, exactly as the app spawns it):

  • Started a scratch pty-daemon, opened one session running python3 -m http.server 18923, and disconnected — an alive, unattached daemon session, the post-update state.
  • Started the built host-service against a scratch DB with SUPERSET_PTY_DAEMON_SOCKET pointed at the scratch daemon, and inserted the session's terminal_sessions row (status: "active" + workspace id).
  • Warm-up sync logged port-scan sync: registered 1 unattached daemon session(s) for scanning within seconds — no renderer ever attached.
  • ports.getAll for that workspace returned { port: 18923, processName: "Python", ... }.
  • Verified the built bundle contains the startTerminalReaper(db) call.
  • Re-ran the scratch harness against the restructured bundle (reaper started from createApp): same result — registered 1 unattached daemon session(s) and ports.getAll returning the planted port.

Full-app verification over CDP (RENDERER_REMOTE_DEBUG_PORT=9223 bun dev, this branch):

  • Planted an unattached daemon session running python3 -m http.server 18933 (row bound to a workspace that was never selected), no renderer attach.
  • The dev app's host-service reaper logged port-scan sync: registered 1 unattached daemon session(s) for scanning.
  • The sidebar then rendered the 18933 port badge inside that workspace's item while a different workspace was selected — confirmed via CDP DOM inspection.
  • Dev-mode caveat: the dev daemon is not detached (dies with host-service by design), so the restart leg was covered by the scratch harness above; the dev app covered reaper registration + renderer display with no attach.

Known Limitations

  • The desktop entry (unlike serve.ts) doesn't kick startDaemonBootstrap at startup — the daemon boots on demand. The reaper's earliest warm-up (2s) can beat it and log port-scan warm-up sync failed: pty-daemon is not available once; later warm-ups (5–90s) and the 5-min reap succeed. Pre-existing ordering, observed once in dev; harmless but noisy.

Testing

  • bun run typecheck --filter=@superset/host-service --filter=@superset/desktop
  • bun run lint (exit 0)
  • bun test src/terminal/reaper in packages/host-service (9 pass)
  • bun turbo run build --filter=@superset/desktop

Risks / Rollout

  • Risk: the desktop app now also runs the orphan reap it was always meant to run. Rowless alive daemon sessions are killed only after two consecutive 5-minute passes, and status: "exited"/"disposed" rows only describe already-dead PTYs, so healthy sessions are unaffected — but this is behavior the desktop app has never exercised in production.
  • Rollback: revert this commit; behavior returns to today's (no reaper, ports on attach only).

Open in Stage

Summary by cubic

Start the terminal reaper from the desktop host-service entry with startTerminalReaper(db). This cleans up orphaned sessions and reconciles ports on boot, fixing ports not showing until a workspace is opened.

  • Bug Fixes
    • Start the reaper in the desktop entry and export it from @superset/host-service; both serving entries run it on boot.
    • Remove startHostRuntime and keep reaper startup outside createApp.

Written for commit bcf499d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved cleanup of desktop terminal sessions during app startup, reducing stuck or orphaned terminals.
    • Enhanced startup reliability for the desktop host service by adding an additional terminal reaping step once the service is ready.

…ntry

startTerminalReaper was only wired into packages/host-service serve.ts
(the standalone/CLI entry), so the packaged desktop app never ran the
orphan reap (#5168) or the port-scan reconcile for daemon-adopted
sessions (#5325/#5430). Ports for terminals that survived a host-service
restart stayed invisible until the workspace was selected.
@ghost

ghost commented Jul 3, 2026 •

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 2 individual chapters for you:

Title
1 Export startTerminalReaper from host-service package
2 Wire terminal reaper into desktop host-service
Open in Stage

Chapters generated by Stage for commit bcf499d on Jul 3, 2026 9:25pm UTC.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds startTerminalReaper as a public re-export from packages/host-service/src/index.ts. The desktop host-service entry point imports it, captures db from createApp(...), and invokes startTerminalReaper(db) in the server's onListening callback after installProcessSafetyNet().

Changes

Terminal reaper startup wiring

Layer / File(s) Summary
Export startTerminalReaper
packages/host-service/src/index.ts
Re-exports startTerminalReaper from ./terminal/reaper as a public API entry.
Wire into desktop startup
apps/desktop/src/main/host-service/index.ts
Imports startTerminalReaper, destructures db from createApp(...), and calls startTerminalReaper(db) after installProcessSafetyNet() in the onListening callback.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • superset-sh/superset#5168: Introduces the terminal reaper startup and db plumbing that this PR wires into the desktop host-service.
  • superset-sh/superset#5325: Extends the startTerminalReaper implementation with port-scan synchronization logic, which this PR's startup hook now triggers.
  • superset-sh/superset#5430: Adds port-scan warm-up scheduling inside startTerminalReaper, activated by this PR's new startup call.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: starting the terminal reaper in the desktop host-service entry.
Description check ✅ Passed The description is detailed and covers summary, context, testing, risks, and rollout, with only the template-specific sections not fully formatted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-desktop-host-service-reaper

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.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a missing startTerminalReaper(db) call in the desktop host-service entry point, where it was only wired into the standalone CLI entry (serve.ts) but never into the Electron-spawned entry (apps/desktop/src/main/host-service/index.ts). As a result, the packaged desktop app never ran the orphan reap or the port-scan warm-up syncs, causing daemon sessions that survived a host-service restart to remain invisible until the user opened the workspace.

  • Exports startTerminalReaper from packages/host-service/src/index.ts so the desktop entry can import it from the package root.
  • Destructures db from the already-returned createApp(...) result and calls startTerminalReaper(db) inside the listen callback, exactly mirroring the pattern in serve.ts.

Confidence Score: 5/5

Safe to merge; the change is a one-liner that wires an already-tested component into the desktop entry, exactly mirroring the standalone CLI path.

Both changed files are minimal. The db object was already available from createApp(...); it just wasn't being destructured. startTerminalReaper is fully unit-tested, resilient to daemon unavailability (errors are caught and logged), and all internal timers call .unref() so process exit behaviour is unchanged. The orphan reaper will now run in the desktop app for the first time in production, but the two-pass rowless guard protects healthy sessions from accidental reaping.

No files require special attention.

Important Files Changed

Filename Overview
apps/desktop/src/main/host-service/index.ts Destructures db from createApp(...) and calls startTerminalReaper(db) in the listen callback — a clean one-to-one mirror of the existing pattern in serve.ts.
packages/host-service/src/index.ts Adds startTerminalReaper to the public package exports so the desktop entry can import it from @superset/host-service.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Electron as Electron Main
    participant Entry as host-service/index.ts
    participant Hono as Hono Server
    participant Reaper as startTerminalReaper
    participant Daemon as pty-daemon
    participant PM as portManager

    Electron->>Entry: spawn host-service child
    Entry->>Entry: "createApp(...) -> {app, db, ...}"
    Entry->>Hono: serve(app.fetch, port)
    Hono-->>Entry: listen callback (server ready)
    Entry->>Entry: installProcessSafetyNet()
    Entry->>Reaper: startTerminalReaper(db) NEW
    Note over Reaper: immediate reap pass + warm-up timers
    loop Every 5 minutes
        Reaper->>Daemon: daemon.list()
        Daemon-->>Reaper: live sessions
        Reaper->>PM: upsertSession / unregisterSession
        Note over PM: unattached sessions now visible in ports UI
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Electron as Electron Main
    participant Entry as host-service/index.ts
    participant Hono as Hono Server
    participant Reaper as startTerminalReaper
    participant Daemon as pty-daemon
    participant PM as portManager

    Electron->>Entry: spawn host-service child
    Entry->>Entry: "createApp(...) -> {app, db, ...}"
    Entry->>Hono: serve(app.fetch, port)
    Hono-->>Entry: listen callback (server ready)
    Entry->>Entry: installProcessSafetyNet()
    Entry->>Reaper: startTerminalReaper(db) NEW
    Note over Reaper: immediate reap pass + warm-up timers
    loop Every 5 minutes
        Reaper->>Daemon: daemon.list()
        Daemon-->>Reaper: live sessions
        Reaper->>PM: upsertSession / unregisterSession
        Note over PM: unattached sessions now visible in ports UI
    end
Loading

Reviews (1): Last reviewed commit: "fix(desktop): start the terminal reaper ..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/desktop/src/main/host-service/index.ts (1)

123-123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Returned teardown function is discarded.

startTerminalReaper returns a dispose function that clears its interval and warm-up timers, but it's not captured here. Since the timers are .unref()'d and shutdown() ultimately calls process.exit(0), this has no practical runtime impact in this single-shot process entry, but wiring it into shutdown() would be more symmetric with the explicit cleanup pattern used elsewhere (e.g. installProcessSafetyNet).

♻️ Optional: capture and invoke teardown on shutdown
+			const stopReaper = startTerminalReaper(db);
-			startTerminalReaper(db);

Then call stopReaper() inside shutdown(...) alongside server.close().

🤖 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 `@apps/desktop/src/main/host-service/index.ts` at line 123, The teardown from
startTerminalReaper is currently ignored, so its interval and warm-up timers are
never explicitly cleaned up. Capture the dispose function returned by
startTerminalReaper(db) in host-service/index.ts, then invoke it inside
shutdown() alongside server.close() to match the cleanup pattern already used by
installProcessSafetyNet.
🤖 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.

Nitpick comments:
In `@apps/desktop/src/main/host-service/index.ts`:
- Line 123: The teardown from startTerminalReaper is currently ignored, so its
interval and warm-up timers are never explicitly cleaned up. Capture the dispose
function returned by startTerminalReaper(db) in host-service/index.ts, then
invoke it inside shutdown() alongside server.close() to match the cleanup
pattern already used by installProcessSafetyNet.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f1ad474-96e2-4d77-b934-a1f9abf9e862

📥 Commits

Reviewing files that changed from the base of the PR and between 292fb46 and f6933ee.

📒 Files selected for processing (2)
  • apps/desktop/src/main/host-service/index.ts
  • packages/host-service/src/index.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 2 files

Architecture diagram
sequenceDiagram
    participant Desktop as Desktop App (main process)
    participant HostService as Host Service Entry
    participant CreateApp as createApp()
    participant Reaper as startTerminalReaper(db)
    participant Daemon as PTY Daemon
    participant DB as Database

    Note over Desktop,DB: Desktop Host-Service Startup Flow

    Desktop->>HostService: Spawn as Node process
    HostService->>HostService: initTerminalBaseEnv()
    HostService->>CreateApp: createApp(config)
    CreateApp-->>HostService: { app, injectWebSocket, api, db }

    HostService->>HostService: Start HTTP server (listen callback)

    Note over HostService,DB: Port-scan registration daemon sessions

    HostService->>Reaper: startTerminalReaper(db)

    alt Warm-up sync (immediate)
        Reaper->>DB: Query unattached daemon sessions
        DB-->>Reaper: Sessions with status:"active" + no renderer
        Reaper->>Daemon: Register sessions for port scanning
        Daemon-->>Reaper: Port scan results
        Reaper->>DB: upsertSession() for scanned ports
        Note over Reaper: Ports visible via ports.getAll()<br/>without workspace open
    end

    loop Every 5 minutes
        Reaper->>DB: Query orphaned daemon sessions
        alt Two consecutive reap passes
            Reaper->>Daemon: Kill orphaned PTY
            Daemon-->>Reaper: Session cleaned
            Reaper->>DB: Update session status
        end
    end

    Note over Desktop,DB: Server WebSocket (unchanged)
    CreateApp->>Desktop: injectWebSocket()
    Desktop->>Desktop: registerServeTerminalType()
Loading

Re-trigger cubic

@github-actions

github-actions Bot commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) ✅ View Branch
Vercel API (Vercel) ✅ Open Preview
Vercel Web (Vercel) ✅ Open Preview
Vercel Marketing (Vercel) ✅ Open Preview
Vercel Admin (Vercel) ✅ Open Preview
Vercel Docs (Vercel) ✅ Open Preview

Preview updates automatically with new commits

@Kitenite

Kitenite commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

re: coderabbit's nitpick about the discarded startTerminalReaper teardown — intentionally skipping. The reaper's interval and warm-up timers are all unref()'d, and this entry's shutdown() ends in process.exit(0) after a 3s drain, so there's nothing for the dispose to release in practice. serve.ts (the pattern this mirrors) also discards it; capturing it only in the desktop entry would make the two call sites asymmetric. If we ever add a real graceful-shutdown path that doesn't exit the process, wiring the teardown in belongs in both entries.

Both real entry points (serve.ts and the desktop entry) consume
createApp, so owning the reaper there removes the per-entry wiring that
let the desktop app ship without it. Tests opt out via createTestHost:
the reaper's warm-up timers reach the real pty-daemon through the
process-wide daemon-client singleton and reap other tests' sessions
(observed as 5 integration failures with an unconditional start). The
reaper's stop function joins createApp's dispose chain.
@Kitenite

Kitenite commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Restructured per discussion: the reaper now starts inside createApp (next to eventBus.start()), so both entry points get it without per-entry wiring, and serve.ts's own call is removed — no double-start. Its stop function is wired into createApp's dispose chain, which also properly resolves the earlier teardown nitpick. Tests opt out via createTestHost (startTerminalReaper: false) because the reaper's warm-up timers reach the real pty-daemon through the process-wide daemon-client singleton and were reaping other tests' sessions (5 integration failures with an unconditional start). Full host-service suite: 746 pass / 0 fail; scratch E2E re-verified on the rebuilt bundle.

…ostRuntime

createApp goes back to building the request-serving surface only — no
reaper and no test-only escape hatch. Process-lifetime background work
now lives in startHostRuntime (runtime/host-runtime.ts), which both
serving entries call once after binding. Tests consume createApp and
are untouched.
@Kitenite

Kitenite commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Final shape: dropped the startTerminalReaper: false test override — production APIs shouldn't carry test escape hatches. Instead the package now separates app construction from process runtime: createApp is untouched from main, and a new startHostRuntime(db) (runtime/host-runtime.ts) owns process-lifetime background work; both serving entries call it once after binding. Tests are byte-identical to main and pass 746/0 with no accommodations. Scratch E2E re-verified on the rebuilt bundle (unattached daemon session registered, port returned by ports.getAll).

The wrapper only delegated to startTerminalReaper. Entries call the
reaper directly, same as installProcessSafetyNet and connectRelay —
one invocation per serving entry, logic stays in the package.
@Kitenite

Kitenite commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Settled on the direct shape: dropped the startHostRuntime pass-through (it only delegated). startTerminalReaper is exported from the package root and each serving entry calls it once in its listen callback — same pattern as installProcessSafetyNet / connectRelay, which both entries already invoke individually. Net diff vs main is back to the original two-file change plus the root export.

@Kitenite
Kitenite merged commit a6c9de4 into main Jul 3, 2026
11 checks passed

This branch was previously deployed

1 inactive deployment
preview — bcf499dc Deployed Jul 3, 2026 by Kitenite via Deploy Web #6149
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