Skip to content

Conversation

@Kitenite
Copy link
Collaborator

@Kitenite Kitenite commented Jan 20, 2026

Summary

  • Fix killAll bug that could leave sessions stuck in "terminating" state
  • Add development mode detection for stale daemon (auto-restart when code changes)
  • Add isAlive check after spawn to detect sessions that die immediately
  • Add "Restart Daemon" button in Terminal Settings for manual recovery
  • Add DaemonTerminalManager.reset() method to properly clear state on restart

Problem

Terminals could get into a "Session not attachable" state where all write operations failed. This happened because:

  1. killAll called session.kill() directly without fail-safe timers, leaving sessions stuck
  2. In dev mode, code changes didn't restart the daemon (stale code)
  3. No user-accessible way to recover without switching terminal modes

Solution

Defense-in-depth approach:

  1. Prevent: Fix killAll to use proper kill method with fail-safe timers
  2. Detect: Add mtime tracking in dev mode to detect stale daemon
  3. Recover: Add "Restart Daemon" button in settings and tray menu

Test plan

  • Enable terminal persistence
  • Create terminals, verify they work
  • Click "Restart Daemon" in settings or tray
  • Verify new terminals work after restart
  • In dev mode: change daemon code, restart app, verify daemon auto-restarts

Summary by CodeRabbit

  • New Features
    • Added daemon restart functionality accessible from terminal settings with a confirmation dialog to prevent accidental restarts.
    • Enhanced terminal daemon management with improved session handling during restart operations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Warning

Rate limit exceeded

@Kitenite has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 55 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 73d1a59 and 1810887.

📒 Files selected for processing (2)
  • apps/desktop/src/lib/trpc/routers/terminal/terminal.stream.test.ts
  • apps/desktop/src/main/lib/terminal/daemon-manager.ts
📝 Walkthrough

Walkthrough

This change introduces a complete terminal daemon restart capability, including a new restartDaemon mutation in the terminal router, daemon script staleness detection for development rebuilds, reset methods for clearing daemon manager state, and corresponding UI controls in terminal settings to initiate and confirm restarts.

Changes

Cohort / File(s) Summary
Terminal Router Restart Mutation
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Added restartDaemon public mutation that connects to Terminal Host, lists and terminates sessions, shuts down daemon, resets manager state, and returns success status.
Daemon Client Script Staleness Detection
apps/desktop/src/main/lib/terminal-host/client.ts
Introduced isDaemonScriptStale() and saveDaemonScriptMtime() helpers to detect development-time daemon rebuilds via file modification time tracking; kills and restarts daemon if script has changed.
Daemon Manager Reset Infrastructure
apps/desktop/src/main/lib/terminal/daemon-manager.ts
Added reset() method to DaemonTerminalManager that clears timers, resets session maps, reinitializes client; added reset() to PrioritySemaphore to clear concurrency state; made client field non-null asserted with new initialization flow.
Terminal State Cleanup
apps/desktop/src/main/lib/terminal/dev-reset.ts
Added "terminal-host.mtime" to cleanup paths so daemon script mtime file is removed during development resets.
Tray Daemon Restart Flow
apps/desktop/src/main/lib/tray/index.ts
Updated restart logic with user confirmation dialog, guarded daemon connection and shutdown, daemon manager reset, and tray menu refresh on completion.
Terminal Host Lifecycle Updates
apps/desktop/src/main/terminal-host/terminal-host.ts
Simplified comments and control flow; renamed killAll parameter from _request to request with updated internal session iteration.
Settings Terminal Restart UI
apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx
Added daemon restart confirmation dialog, mutation hook integration with success/error notifications, and "Restart daemon" button with session cleanup on completion.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as Settings UI
    participant Router as Terminal Router
    participant Client as Terminal Host Client
    participant Manager as Daemon Manager
    participant Host as Terminal Host
    
    User->>UI: Click "Restart daemon"
    UI->>UI: Show confirmation dialog
    User->>UI: Confirm restart
    UI->>Router: Call restartDaemon mutation
    Router->>Client: Connect and authenticate
    Client-->>Router: Connected
    Router->>Client: List sessions
    Client-->>Router: Sessions list
    Router->>Client: Shutdown daemon (killSessions: true)
    Client->>Host: Kill all sessions
    Host-->>Client: Shutdown
    Router->>Manager: Reset daemon manager
    Manager->>Manager: Clear timers & reset state
    Manager->>Manager: Reinitialize client
    Router-->>UI: success: true
    UI->>UI: Show success notification
    UI->>UI: Invalidate session list
    UI->>UI: Update tray menu
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A restart button hops with glee,
Daemon resets, fresh as can be,
Script stale? Detected with flair!
Sessions swept clean through the air,
Daemon reborn—hopping affairs!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: fixing a terminal daemon stuck state bug and adding a restart button UI feature.
Description check ✅ Passed The description provides a clear summary, problem statement, solution approach, and test plan. It follows the template structure with all required sections addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 20, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
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

Apply clean code principles - remove verbose comments where code is
self-explanatory, consolidate debug logging behind DEBUG flags, and
simplify function bodies.
- Add reset() method to PrioritySemaphore to clear stuck permits
- Call limiter reset in DaemonTerminalManager.reset()
- Move markTerminalKilledByUser to onSuccess callback to avoid
  stale markers if restart fails
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/lib/trpc/routers/terminal/terminal.ts (1)

294-332: Align restart/kill-all log prefixes and hoist polling constants.

New log tags like [killAllDaemonSessions] / [restartDaemon] don’t match the [domain/operation] convention, and the retry constants are defined inside the mutation. Consider hoisting to module-level constants and updating log prefixes for consistency. As per coding guidelines, use the [domain/operation] log pattern and avoid inline magic numbers.

♻️ Suggested adjustment
 const DEBUG_TERMINAL = process.env.SUPERSET_TERMINAL_DEBUG === "1";
 let createOrAttachCallCounter = 0;
+const KILL_ALL_MAX_RETRIES = 10;
+const KILL_ALL_RETRY_DELAY_MS = 100;

@@
-			console.log(
-				"[killAllDaemonSessions] Before kill:",
+			console.log(
+				"[terminal/killAllDaemonSessions] Before kill:",
 				beforeIds.length,
 				"sessions",
 				beforeIds,
 			);

-			// Poll until sessions are actually dead
-			const MAX_RETRIES = 10;
-			const RETRY_DELAY_MS = 100;
+			// Poll until sessions are actually dead
 			let remainingCount = before.sessions.length;
 			let afterIds: string[] = [];

-			for (let i = 0; i < MAX_RETRIES && remainingCount > 0; i++) {
-				await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
+			for (let i = 0; i < KILL_ALL_MAX_RETRIES && remainingCount > 0; i++) {
+				await new Promise((resolve) =>
+					setTimeout(resolve, KILL_ALL_RETRY_DELAY_MS),
+				);
 				const after = await client.listSessions();
@@
-					console.log(
-						`[killAllDaemonSessions] Retry ${i + 1}/${MAX_RETRIES}: ${remainingCount} sessions still alive`,
+					console.log(
+						`[terminal/killAllDaemonSessions] Retry ${i + 1}/${KILL_ALL_MAX_RETRIES}: ${remainingCount} sessions still alive`,
 						afterIds,
 					);
 				}
 			}

-			console.log(
-				"[killAllDaemonSessions] Complete:",
+			console.log(
+				"[terminal/killAllDaemonSessions] Complete:",
 				killedCount,
 				"killed,",
 				remainingCount,
 				"remaining",
 				remainingCount > 0 ? afterIds : [],
 			);

@@
-			console.log("[restartDaemon] Starting daemon restart...");
+			console.log("[terminal/restartDaemon] Starting daemon restart...");

@@
-					console.log(
-						`[restartDaemon] Shutting down daemon with ${aliveCount} alive sessions`,
+					console.log(
+						`[terminal/restartDaemon] Shutting down daemon with ${aliveCount} alive sessions`,
 					);
@@
-					console.log("[restartDaemon] Daemon was not running");
+					console.log("[terminal/restartDaemon] Daemon was not running");
@@
-				console.warn(
-					"[restartDaemon] Error during shutdown (continuing):",
+				console.warn(
+					"[terminal/restartDaemon] Error during shutdown (continuing):",
 					error,
 				);
@@
-			console.log("[restartDaemon] Complete");
+			console.log("[terminal/restartDaemon] Complete");

Also applies to: 368-399

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/terminal/daemon-manager.ts`:
- Around line 1226-1242: In reset(), before clearing this.historyWriters,
perform a best-effort close of each writer like cleanup()/forceKillAll() do:
iterate this.historyWriters.values(), and for each writer call its
close/end/destroy API inside a try/catch (and await if the writer exposes a
promise-based close), flush or write an endedAt timestamp if the writer API used
elsewhere does that, swallow errors and then clear this.historyWriters; this
prevents FD leaks and ensures endedAt is recorded.
- Around line 1292-1295: PrioritySemaphore.reset() currently clears this.queue
allowing pending acquire() promises to hang; change the queue to store both
resolve and reject callbacks and in reset() iterate the queued waiters to call
their reject(...) (e.g., with an Error like "semaphore reset") before clearing
the queue and resetting inUse, so callers awaiting acquire() (such as
createOrAttach()/releaseCreateOrAttach()) receive an immediate rejection instead
of hanging.
🧹 Nitpick comments (1)
apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx (1)

935-943: Good pattern: marking sessions only on success.

Placing markTerminalKilledByUser in the onSuccess callback is the correct approach—if the restart fails, sessions won't be incorrectly marked as user-killed. This aligns with the commit message about preventing stale state on failure.

Consider applying the same pattern to killAllDaemonSessions (lines 790-795) for consistency, where sessions are currently marked before calling mutate().

Store reject callback in PrioritySemaphore queue so reset() can
reject pending acquire() promises instead of leaving them hanging.
@Kitenite Kitenite force-pushed the session-not-attachable branch from 92a4b80 to 1810887 Compare January 20, 2026 07:41
@Kitenite Kitenite merged commit 8d17373 into main Jan 20, 2026
12 checks passed
@Kitenite Kitenite deleted the session-not-attachable branch January 20, 2026 07:45
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.

2 participants