Skip to content

refactor(desktop): split daemon-manager into modular files - #852

Merged
Kitenite merged 2 commits into
mainfrom
refactor-daemon-manager
Jan 20, 2026
Merged

Kitenite merged 2 commits into
mainfrom
refactor-daemon-manager

Conversation

@Kitenite

@Kitenite Kitenite commented Jan 20, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Split monolithic daemon-manager.ts (~1325 lines) into 6 focused modules in daemon/ folder
  • Extracted HistoryManager class to encapsulate history persistence logic
  • Extracted PrioritySemaphore utility for concurrency limiting
  • Separated types and constants into dedicated files
  • Core DaemonTerminalManager class reduced to ~740 lines

Structure

daemon/
├── index.ts              # Exports + singleton management
├── daemon-manager.ts     # Core session lifecycle
├── history-manager.ts    # History persistence logic
├── priority-semaphore.ts # Concurrency limiter utility
├── types.ts              # SessionInfo, ColdRestoreInfo
└── constants.ts          # Configuration constants

Test plan

  • Verify typecheck passes
  • Verify terminal sessions work (create, attach, resize, kill)
  • Verify terminal persistence across app restart (warm restore)
  • Verify cold restore after daemon restart

Summary by CodeRabbit

  • New Features
    • Improved terminal backend with persistent session support, session listing, and detach/attach behavior.
    • Per-pane history persistence and cold-restore so terminals can resume prior scrollback and state.
  • Bug Fixes
    • More robust session lifecycle, cleanup, and crash/reconnect handling.
    • Safer resize, signal, and kill operations with better failure handling.
  • Refactor
    • Internal concurrency and prioritization for session creation to reduce race conditions.

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

Split the monolithic daemon-manager.ts (~1325 lines) into focused modules
for better maintainability:

- daemon/types.ts - SessionInfo, ColdRestoreInfo interfaces
- daemon/constants.ts - Configuration constants
- daemon/priority-semaphore.ts - Concurrency limiter utility
- daemon/history-manager.ts - History persistence logic
- daemon/daemon-manager.ts - Core session lifecycle (~740 lines)
- daemon/index.ts - Exports and singleton management

All imports remain backward compatible via the barrel export.
@coderabbitai

coderabbitai Bot commented Jan 20, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Monolithic daemon-based terminal manager file removed and functionality restructured into a new daemon/ subpackage introducing DaemonTerminalManager, HistoryManager, PrioritySemaphore, config constants, types, and a singleton accessor; root terminal import updated to use the new module.

Changes

Cohort / File(s) Summary
Monolith removed
apps/desktop/src/main/lib/terminal/daemon-manager.ts
Deleted the original monolithic daemon-backed terminal manager (~1325 LOC).
Daemon core
apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts
New DaemonTerminalManager class with session lifecycle, reconcile/startup, createOrAttach (with cold-restore), write/resize/signal/kill/detach/clearScrollback APIs, event wiring, and session maps.
History management
apps/desktop/src/main/lib/terminal/daemon/history-manager.ts
New HistoryManager coordinating per-pane HistoryWriter lifecycle, buffering during init, clear-scrollback reinit, and cleanup/reset operations.
Concurrency utility
apps/desktop/src/main/lib/terminal/daemon/priority-semaphore.ts
New PrioritySemaphore implementing priority-based acquire/release queueing and reset semantics.
Constants & types
apps/desktop/src/main/lib/terminal/daemon/constants.ts, apps/desktop/src/main/lib/terminal/daemon/types.ts
Added config constants (session cleanup delay, debug flag, concurrency limit, scrollback limits) and types SessionInfo and ColdRestoreInfo.
Module surface / singleton
apps/desktop/src/main/lib/terminal/daemon/index.ts
New index exporting manager and types; provides getDaemonTerminalManager() singleton accessor (created lazily).
Root import update
apps/desktop/src/main/lib/terminal/index.ts
Updated imports to point to ./daemon instead of ./daemon-manager.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client/UI
    participant Manager as DaemonTerminalManager
    participant Daemon as Terminal Host (daemon)
    participant History as HistoryManager
    participant Ports as PortManager

    UI->>Manager: createOrAttach(paneId, params)
    Manager->>Manager: acquire priority semaphore
    alt cold restore available
        Manager->>History: attemptColdRestore(paneId)
        History-->>Manager: cold restore snapshot
    end
    Manager->>Daemon: createOrAttach (env, cwd, cols, rows)
    Daemon-->>Manager: session metadata (pid, id) + stream events
    Manager->>Ports: register pid for port detection
    Manager->>History: initHistoryWriter(paneId, metadata, initialScrollback)
    History-->>Manager: writer ready
    Manager-->>UI: SessionResult (snapshot + metadata)
    Daemon-->>Manager: data / exit / error events
    Manager->>History: writeToHistory(paneId, data)
    Manager-->>UI: emit data/exit events
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • AviPeltz

Poem

🐰 I hopped through files, split the span,

One monolith became a plan.
Managers, histories, semaphores in tow,
Sessions wake and data flows.
Hooray — the daemon dances on the go!

🚥 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 clearly and concisely summarizes the main change: splitting a monolithic daemon-manager file into modular files.
Description check ✅ Passed The description includes all required sections from the template (Summary, Related Issues, Type of Change, Testing, Additional Notes) with substantial detail about the refactoring structure and testing plan.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@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.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts`:
- Around line 568-576: The console.warn in the signal method is missing the
required "[domain/operation]" prefix; update the warning emitted when session is
missing or not alive in the signal(params: { paneId: string; signal?: string }):
void function to include the mandated prefix (e.g. "[terminal/daemon]") before
the existing message, referencing the session variable and paneId so the log
reads like: prefix + contextual message about session not found or not alive;
ensure you update the console.warn call in daemon-manager.ts where
sessions.get(paneId) is checked.
- Around line 504-517: The catch block in getCreateOrAttachPriority silently
swallows errors; change it to catch the error as a variable (e.g., err) and log
it with context (include params.workspaceId, params.tabId, params.paneId and any
relevant appState pieces) using the project's logger (e.g., processLogger.error
or existing logger) and then return the default priority (1); keep the rest of
getCreateOrAttachPriority unchanged.
- Around line 360-373: When creating/replacing a session for a paneId you must
clear any pending cleanup timeout so the old timeout doesn't delete the
newly-created session; before calling this.sessions.set(...) (inside the code
handling new session creation where paneId is available) cancel and clear the
existing timeout for that paneId (e.g. clearTimeout on a per-pane timer stored
on the instance, such as this.sessionCleanupTimers or this.cleanupTimeouts) and
remove its entry, then proceed to set the new session object; ensure that
timeouts scheduled with SESSION_CLEANUP_DELAY_MS are stored by paneId so they
can be canceled when reusing the same paneId.
- Around line 690-695: The loop that collects IDs to terminate uses
session.paneId but the daemon kill path expects session.sessionId; update the
collection to push session.sessionId instead and rename the container variable
from paneIdsToKill to sessionIdsToKill (or update all downstream references
where paneIdsToKill is used) to keep names consistent; locate this change around
the listSessions() usage in daemon-manager (and ensure the later kill() calls
still pass the sessionIds from session.sessionId).

In `@apps/desktop/src/main/lib/terminal/daemon/index.ts`:
- Around line 15-20: Make disposeDaemonManager async and, instead of calling
daemonManager.removeAllListeners(), await daemonManager.cleanup() so
DaemonTerminalManager.cleanup() is run to close HistoryWriter file handles,
clear cleanupTimeouts, and dispose the TerminalHostClient; check daemonManager
exists before awaiting, await daemonManager.cleanup(), then set daemonManager =
null (no need to only call removeAllListeners()), and surface or log any errors
from the awaited cleanup call.
🧹 Nitpick comments (3)
apps/desktop/src/main/lib/terminal/daemon/priority-semaphore.ts (1)

13-23: Consider documenting priority ordering convention.

The implementation is correct. Lower priority values get processed first (ascending sort). Consider adding a brief JSDoc comment to clarify this convention for future maintainers:

+	/**
+	 * Acquire a slot in the semaphore. Lower priority values are processed first.
+	 * Returns a release function that must be called when done.
+	 */
 	acquire(priority: number): Promise<() => void> {

Also, sorting on every acquire is O(n log n). For the expected small queue sizes in terminal concurrency limiting, this is fine, but if the queue grows large, a heap or sorted insertion would be more efficient.

apps/desktop/src/main/lib/terminal/index.ts (1)

26-26: Duplicate constant definition—consider importing from constants module.

DEBUG_TERMINAL is defined identically in daemon/constants.ts. Consider importing it to maintain DRY:

-const DEBUG_TERMINAL = process.env.SUPERSET_TERMINAL_DEBUG === "1";
+import { DEBUG_TERMINAL } from "./daemon/constants";

Or alternatively, export it from the daemon barrel if it's needed in both places.

apps/desktop/src/main/lib/terminal/daemon/history-manager.ts (1)

224-245: Consider consistency between forceCloseAll and cleanup.

forceCloseAll uses sequential await (Lines 225-231) while cleanup uses Promise.all (Lines 207-218). If the sequential behavior is intentional (e.g., for ordered shutdown), consider adding a comment. Otherwise, using Promise.all would be more efficient.

Also, closeAllSync silently swallows errors (Line 240: .catch(() => {})). Per coding guidelines about not swallowing errors silently, consider at minimum logging them, though this may be intentional for synchronous shutdown contexts where logging is not guaranteed to complete.

@Kitenite
Kitenite merged commit bd208b7 into main Jan 20, 2026
4 of 5 checks passed
@Kitenite
Kitenite deleted the refactor-daemon-manager branch January 20, 2026 17:30
@github-actions

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ⚠️ Neon database branch
  • ⚠️ Electric Fly.io app

Thank you for your contribution! 🎉

@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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts`:
- Around line 819-840: In forceKillAll, don’t swallow errors from
this.client.listSessions; change the catch to capture the error and log it
(e.g., this.logger.error or an existing module logger) with a clear prefix like
"Failed to list sessions in forceKillAll" before returning the fallback {
sessions: [] }. Update the call site where this.client.listSessions is used so
the catch handler receives the error variable and logs it, preserving the
current fallback behavior; reference forceKillAll and this.client.listSessions
when making the change.
♻️ Duplicate comments (2)
apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts (2)

506-519: Log priority derivation failures instead of swallowing.

The empty catch hides failures; log with a prefixed message and context before returning the default. As per coding guidelines, never swallow errors silently.

🐛 Proposed fix
-		} catch {
-			return 1;
-		}
+		} catch (error) {
+			console.warn(
+				"[DaemonTerminalManager/getCreateOrAttachPriority] Failed to compute priority:",
+				{
+					workspaceId: params.workspaceId,
+					tabId: params.tabId,
+					paneId: params.paneId,
+				},
+				error,
+			);
+			return 1;
+		}

570-587: Add required log prefix for missing-session warning.

The warning should include the mandated [domain/operation] prefix. As per coding guidelines, prefix console logs.

🪵 Proposed fix
-			console.warn(
-				`Cannot signal terminal ${paneId}: session not found or not alive`,
-			);
+			console.warn(
+				`[DaemonTerminalManager] Cannot signal terminal ${paneId}: session not found or not alive`,
+			);

Comment on lines +819 to +840
async forceKillAll(): Promise<void> {
const response = await this.client.listSessions().catch(() => ({
sessions: [],
}));
const sessionIds = response.sessions.map((s) => s.sessionId);

for (const timeout of this.cleanupTimeouts.values()) {
clearTimeout(timeout);
}
this.cleanupTimeouts.clear();

await this.historyManager.forceCloseAll();

await this.client.killAll({});
for (const paneId of sessionIds) {
portManager.unregisterDaemonSession(paneId);
}
this.daemonAliveSessionIds.clear();
this.daemonSessionIdsHydrated = true;
this.coldRestoreInfo.clear();
this.sessions.clear();
}

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.

⚠️ Potential issue | 🟡 Minor

Log listSessions failure in forceKillAll.

The catch currently swallows errors; log with prefix before falling back. As per coding guidelines, never swallow errors silently.

🐛 Proposed fix
-		const response = await this.client.listSessions().catch(() => ({
-			sessions: [],
-		}));
+		const response = await this.client.listSessions().catch((error) => {
+			console.warn(
+				"[DaemonTerminalManager/forceKillAll] Failed to list sessions:",
+				error,
+			);
+			return { sessions: [] };
+		});
🤖 Prompt for AI Agents
In `@apps/desktop/src/main/lib/terminal/daemon/daemon-manager.ts` around lines 819
- 840, In forceKillAll, don’t swallow errors from this.client.listSessions;
change the catch to capture the error and log it (e.g., this.logger.error or an
existing module logger) with a clear prefix like "Failed to list sessions in
forceKillAll" before returning the fallback { sessions: [] }. Update the call
site where this.client.listSessions is used so the catch handler receives the
error variable and logs it, preserving the current fallback behavior; reference
forceKillAll and this.client.listSessions when making the change.

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