Skip to content

fix(host-service): isolate subsystem crashes from main thread#3811

Merged
Kitenite merged 5 commits into
mainfrom
resilient-error-handling-2
Apr 29, 2026
Merged

fix(host-service): isolate subsystem crashes from main thread#3811
Kitenite merged 5 commits into
mainfrom
resilient-error-handling-2

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Apr 27, 2026

Summary

The host-service hosts long-running subsystems (EventBus, GitWatcher, PullRequestRuntimeManager, ChatRuntimeManager, terminal PTYs). A throw inside any of their async entry points — setInterval bodies, EventEmitter listeners, pty.onData/onExit, harness subscribe callbacks — used to escape into the process and could take the whole service down.

This PR adds a process-level safety net plus targeted inline guards at the two real cascade-failure points (fan-out loops). The pattern matches what equivalent apps do — opencode, tabby, hyper, mastra-code-ui all rely on a process-level handler + minimal inline guards rather than wrapping every callback.

What's added

  • packages/host-service/src/safety.ts — new module exporting installProcessSafetyNet(), which registers uncaughtException + unhandledRejection handlers that log and stay up. Idempotent (safe to call from both entry points).
  • Wired in at both entry points (packages/host-service/src/serve.ts, apps/desktop/src/main/host-service/index.ts) inside the serve() listening callback — so startup throws still propagate to main().catch → process.exit(1) and the supervisor can restart with a clean process.
  • EventBus.broadcast — wraps per-socket send in try/catch and drops dead sockets (matching the opencode pty fan-out pattern). Converts "log every broadcast forever" into "one log + clean state."
  • GitWatcher listener loop — inline try/catch around each listener invocation so one bad subscriber can't skip siblings.
  • EventBus.parseClientMessage — replaces silent malformed-message swallow with console.warn so caught errors are visible.

Why this approach

Node already routes throws from setInterval/setTimeout bodies, EventEmitter listeners, native callbacks (pty.onData/onExit), and orphaned promise continuations into uncaughtException / unhandledRejection. A single process-level handler covers all of those. The only places where wrapping genuinely earns its keep are loops over multiple subscribers, where a throw skips the rest of the iteration.

What's intentionally not changed

Startup process.exit(1) is preserved. The "never exit" goal applies to runtime — once the server is listening, subsystem crashes must not propagate. Startup exit is the supervisor's job: the desktop coordinator polls health and surfaces failures, and CLI deploys run under an external supervisor (systemd/fly) that restarts with a clean process.

Test plan

  • bun --filter @superset/host-service typecheck
  • bun --filter @superset/desktop typecheck
  • cd packages/host-service && bun test — 180/180 pass
  • bun run lint:fix — clean
  • Manual: kill an EventBus subscriber's emit path mid-broadcast — service stays up, dead socket dropped, log appears
  • Manual: throw inside a PR runtime poll — next interval still fires (relies on process net)
  • Manual: throw inside pty.onData — terminal session continues, other sessions unaffected (relies on process net)

Subsystem throws (timer callbacks, EventEmitter listeners, pty
data/exit handlers, harness subscribe callback) used to escape into
the process and could take the whole host-service down. Now they're
contained per-subsystem and a process-level safety net catches
anything that still slips through.

- Add safety.ts with installProcessSafetyNet (uncaughtException +
  unhandledRejection log without exiting) and safeSync/safeAsync
  helpers for wrapping callbacks.
- Wrap hot async entry points in EventBus, GitWatcher,
  PullRequestRuntimeManager, ChatRuntimeManager, and terminal PTY
  callbacks.
- Surface the previously-silent malformed-message swallow in
  EventBus.parseClientMessage.

Startup exit on init failure is intentionally preserved so the
coordinator / external supervisor can restart with a clean process.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds a process-level safety net and defensive error handling: installs idempotent handlers for uncaught exceptions/unhandled rejections, defers safety-net installation until the HTTP server is listening, and isolates exceptions in event parsing, broadcast fan-out, and git-watcher listeners.

Changes

Cohort / File(s) Summary
Process Safety Net Infrastructure
packages/host-service/src/safety.ts, packages/host-service/src/index.ts, packages/host-service/src/serve.ts, apps/desktop/src/main/host-service/index.ts
Adds installProcessSafetyNet() (idempotent) that registers handlers for uncaughtException and unhandledRejection. Re-exported from package index and invoked after the HTTP server starts listening in serve and desktop startup.
Event Bus Error Handling
packages/host-service/src/events/event-bus.ts
parseClientMessage now catches parse/validation errors and logs a warning; EventBus.broadcast wraps per-socket sendMessage calls in try/catch, logs failures, and defers socket cleanup until after iterating all clients.
Git Watcher Resilience
packages/host-service/src/events/git-watcher.ts
Debounced flush now invokes each GitChangedListener inside try/catch blocks and logs errors per-listener to prevent a single listener error from aborting the loop.

Sequence Diagram

sequenceDiagram
    participant Desktop as Desktop Main
    participant Serve as HTTP Serve
    participant Process as Host Process
    participant Logger as Console

    Desktop->>Serve: start host-service
    Serve->>Serve: create & bind HTTP server
    Serve->>Serve: server.listen callback
    Serve->>Process: installProcessSafetyNet()
    Process->>Process: register uncaughtException handler
    Process->>Process: register unhandledRejection handler

    Note over Process,Logger: runtime continues

    alt Exception or Rejection
        Process->>Logger: log structured incident
        Logger-->>Process: formatted output
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I twitch my nose and mend the fray,
A safety net to catch the stray,
When promises tumble, errors play,
I log them soft and keep things gay,
Hop on — the host-service stays! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(host-service): isolate subsystem crashes from main thread' directly and concisely summarizes the main change: preventing subsystem crashes from crashing the entire process through isolation mechanisms.
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 The pull request description is comprehensive and detailed, covering all key aspects including summary, changes, rationale, and test plan.

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

✨ 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 resilient-error-handling-2

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 27, 2026

Greptile Summary

This PR adds crash isolation to the host-service by introducing a safety.ts module with safeSync/safeAsync wrappers and a process-level uncaughtException/unhandledRejection backstop, then applies them at every fire-and-forget async boundary across the five long-running subsystems (EventBus, GitWatcher, PullRequestRuntimeManager, ChatRuntimeManager, terminal PTY). The design is sound — startup exits are preserved, runtime errors are logged and contained per-subsystem, and the idempotency guard prevents double handler registration in the desktop in-process case. All findings are P2 style/observability suggestions.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/observability suggestions with no correctness or reliability impact.

The core design is correct: per-subsystem guards prevent throw propagation, the process-level backstop covers anything that slips through, idempotency is handled, and startup exit semantics are intentionally preserved. The three comments are all P2 (misleading variable name, per-iteration wrapper allocation, low-value promise log field) and none affect runtime behavior.

No files require special attention beyond the three P2 style notes.

Important Files Changed

Filename Overview
packages/host-service/src/safety.ts New file introducing installProcessSafetyNet, safeSync, and safeAsync; idempotency guard and structured logging are correct, minor logging improvement possible for unhandledRejection
packages/host-service/src/runtime/pull-requests/pull-requests.ts Interval callbacks wrapped with safeAsync; local variable named safeSync clashes semantically with the sync utility, potentially confusing future maintainers
packages/host-service/src/events/event-bus.ts Git-changed listener, port add/remove handlers, and per-socket broadcast all wrapped with safeSync; wrapper is recreated on every broadcast iteration (minor allocation concern)
packages/host-service/src/events/git-watcher.ts Rescan interval and debounce-flush timer now catch and log rejections; per-listener invocation isolated so one bad subscriber cannot block others
packages/host-service/src/terminal/terminal.ts pty.onData and pty.onExit wrapped with safeSync; shellReadyPromise.then() now has an explicit .catch() — correct trade-off accepting possible partial state on error over a full process crash
packages/host-service/src/runtime/chat/chat.ts harness.subscribe callback wrapped with safeSync; purely structural, no logic changes
packages/host-service/src/serve.ts installProcessSafetyNet called at the top of main() before any async work; correct placement
apps/desktop/src/main/host-service/index.ts installProcessSafetyNet wired into the desktop entrypoint; idempotency guard ensures only one set of handlers is registered even if both entrypoints run in the same process
packages/host-service/src/index.ts Re-exports installProcessSafetyNet so the desktop package can import it without reaching into internals

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Process start] --> B[installProcessSafetyNet]
    B --> C{safetyNetInstalled?}
    C -- yes --> D[no-op]
    C -- no --> E[Register uncaughtException handler\nlog + stay up]
    E --> F[Register unhandledRejection handler\nlog + stay up]

    subgraph Subsystems
        G[GitWatcher\nrescan interval / debounce flush / listener fan-out]
        H[EventBus\ngit:changed / port add-remove / per-socket broadcast]
        I[PullRequestRuntimeManager\nbranch-sync / project-refresh intervals]
        J[ChatRuntimeManager\nharness.subscribe callback]
        K[Terminal PTY\nonData / onExit / shellReady.then]
    end

    G -- safeSync/catch --> L[log '— contained'\nreturn undefined]
    H -- safeSync --> L
    I -- safeAsync --> L
    J -- safeSync --> L
    K -- safeSync/catch --> L

    L -.->|escapes all guards| E
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/host-service/src/runtime/pull-requests/pull-requests.ts
Line: 212-218

Comment:
**Misleading local variable name shadows the async wrapper**

The local variable is named `safeSync` but it is produced by the `safeAsync` utility and wraps an async function. This directly conflicts with the `safeSync` export from `safety.ts` and will confuse future readers who expect `safeSync` to mean "synchronous, no-await wrapper". Rename it to something unambiguous like `safeBranchSync`.

```suggestion
		const safeBranchSync = safeAsync("pull-requests:branch-sync", () =>
			this.syncWorkspaceBranches(),
		);
		const safeRefresh = safeAsync(
			"pull-requests:project-refresh",
			(force?: boolean) => this.refreshEligibleProjects(force),
		);
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/host-service/src/events/event-bus.ts
Line: 149-152

Comment:
**Wrapper recreated on every broadcast call and every socket iteration**

`safeSync("event-bus:send", sendMessage)` constructs a new closure each time `broadcast` is called for every socket in `this.clients`. The wrapped function is stateless and identical on every call — hoist it once (e.g. as a private field or a module constant) to avoid the repeated allocation.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/host-service/src/safety.ts
Line: 27-32

Comment:
**`promise` object has low diagnostic value in the unhandledRejection log**

The `promise` argument in an `unhandledRejection` handler serialises as `Promise { <rejected> }` and carries no information beyond what `reason` already provides. Consider dropping it from the structured log.

```suggestion
	process.on("unhandledRejection", (reason) => {
		console.error(`[${label}] unhandledRejection — staying up`, {
			reason,
		});
	});
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(host-service): isolate subsystem cra..." | Re-trigger Greptile

Comment on lines +212 to +218
const safeSync = safeAsync("pull-requests:branch-sync", () =>
this.syncWorkspaceBranches(),
);
const safeRefresh = safeAsync(
"pull-requests:project-refresh",
(force?: boolean) => this.refreshEligibleProjects(force),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Misleading local variable name shadows the async wrapper

The local variable is named safeSync but it is produced by the safeAsync utility and wraps an async function. This directly conflicts with the safeSync export from safety.ts and will confuse future readers who expect safeSync to mean "synchronous, no-await wrapper". Rename it to something unambiguous like safeBranchSync.

Suggested change
const safeSync = safeAsync("pull-requests:branch-sync", () =>
this.syncWorkspaceBranches(),
);
const safeRefresh = safeAsync(
"pull-requests:project-refresh",
(force?: boolean) => this.refreshEligibleProjects(force),
);
const safeBranchSync = safeAsync("pull-requests:branch-sync", () =>
this.syncWorkspaceBranches(),
);
const safeRefresh = safeAsync(
"pull-requests:project-refresh",
(force?: boolean) => this.refreshEligibleProjects(force),
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/runtime/pull-requests/pull-requests.ts
Line: 212-218

Comment:
**Misleading local variable name shadows the async wrapper**

The local variable is named `safeSync` but it is produced by the `safeAsync` utility and wraps an async function. This directly conflicts with the `safeSync` export from `safety.ts` and will confuse future readers who expect `safeSync` to mean "synchronous, no-await wrapper". Rename it to something unambiguous like `safeBranchSync`.

```suggestion
		const safeBranchSync = safeAsync("pull-requests:branch-sync", () =>
			this.syncWorkspaceBranches(),
		);
		const safeRefresh = safeAsync(
			"pull-requests:project-refresh",
			(force?: boolean) => this.refreshEligibleProjects(force),
		);
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +149 to +152

private broadcast(message: ServerMessage): void {
for (const socket of this.clients.keys()) {
sendMessage(socket, message);
// One bad socket must not block the rest of the fan-out.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Wrapper recreated on every broadcast call and every socket iteration

safeSync("event-bus:send", sendMessage) constructs a new closure each time broadcast is called for every socket in this.clients. The wrapped function is stateless and identical on every call — hoist it once (e.g. as a private field or a module constant) to avoid the repeated allocation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/events/event-bus.ts
Line: 149-152

Comment:
**Wrapper recreated on every broadcast call and every socket iteration**

`safeSync("event-bus:send", sendMessage)` constructs a new closure each time `broadcast` is called for every socket in `this.clients`. The wrapped function is stateless and identical on every call — hoist it once (e.g. as a private field or a module constant) to avoid the repeated allocation.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread packages/host-service/src/safety.ts Outdated
Comment on lines +27 to +32
process.on("unhandledRejection", (reason, promise) => {
console.error(`[${label}] unhandledRejection — staying up`, {
reason,
promise,
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 promise object has low diagnostic value in the unhandledRejection log

The promise argument in an unhandledRejection handler serialises as Promise { <rejected> } and carries no information beyond what reason already provides. Consider dropping it from the structured log.

Suggested change
process.on("unhandledRejection", (reason, promise) => {
console.error(`[${label}] unhandledRejection — staying up`, {
reason,
promise,
});
});
process.on("unhandledRejection", (reason) => {
console.error(`[${label}] unhandledRejection — staying up`, {
reason,
});
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/safety.ts
Line: 27-32

Comment:
**`promise` object has low diagnostic value in the unhandledRejection log**

The `promise` argument in an `unhandledRejection` handler serialises as `Promise { <rejected> }` and carries no information beyond what `reason` already provides. Consider dropping it from the structured log.

```suggestion
	process.on("unhandledRejection", (reason) => {
		console.error(`[${label}] unhandledRejection — staying up`, {
			reason,
		});
	});
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (4)
packages/host-service/src/runtime/pull-requests/pull-requests.ts (1)

212-218: Local variable name safeSync shadows/confuses with the imported safeSync helper.

The safeSync name is the well-known sync wrapper from ../../safety used elsewhere in this PR (e.g., event-bus.ts, git-watcher.ts), but here it's bound to a safeAsync(...) result that returns a Promise. Anyone scanning this file may misread the call-site at line 220-221 as synchronous. Suggest renaming to reflect what it does (e.g., safeBranchSync).

♻️ Suggested rename
-		const safeSync = safeAsync("pull-requests:branch-sync", () =>
-			this.syncWorkspaceBranches(),
-		);
-		const safeRefresh = safeAsync(
+		const safeBranchSync = safeAsync("pull-requests:branch-sync", () =>
+			this.syncWorkspaceBranches(),
+		);
+		const safeRefresh = safeAsync(
 			"pull-requests:project-refresh",
 			(force?: boolean) => this.refreshEligibleProjects(force),
 		);

 		this.branchSyncTimer = setInterval(() => {
-			void safeSync();
+			void safeBranchSync();
 		}, BRANCH_SYNC_INTERVAL_MS);
 		this.projectRefreshTimer = setInterval(() => {
 			void safeRefresh();
 		}, PROJECT_REFRESH_INTERVAL_MS);

-		void safeSync();
+		void safeBranchSync();
 		void safeRefresh(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/runtime/pull-requests/pull-requests.ts` around
lines 212 - 218, The local variable safeSync shadows the imported safeSync
helper and is actually a Promise-producing wrapper created via safeAsync; rename
the local variable (e.g., safeBranchSync) to avoid confusion and clarify intent
where it's used, update its declaration that calls
safeAsync("pull-requests:branch-sync", () => this.syncWorkspaceBranches()) and
any subsequent references/invocations, and optionally do the same renaming
pattern for consistency with safeRefresh to make clear both are async wrappers.
packages/host-service/src/events/event-bus.ts (1)

150-155: Hoist the safeSync wrapper outside the per-socket loop.

broadcast runs on every fan-out event (git/port/agent/terminal lifecycle), and each iteration allocates a fresh wrapper closure. Construct it once per call:

♻️ Proposed change
 	private broadcast(message: ServerMessage): void {
+		const safeSend = safeSync("event-bus:send", sendMessage);
 		for (const socket of this.clients.keys()) {
 			// One bad socket must not block the rest of the fan-out.
-			safeSync("event-bus:send", sendMessage)(socket, message);
+			safeSend(socket, message);
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/events/event-bus.ts` around lines 150 - 155, In
broadcast, avoid allocating the safeSync wrapper per socket: inside the
broadcast method (which currently calls safeSync("event-bus:send",
sendMessage)(socket, message) in the loop) create the wrapped sender once (e.g.,
const safeSend = safeSync("event-bus:send", sendMessage)) and then call
safeSend(socket, message) for each socket in this.clients.keys(); this preserves
the same error-isolation behavior while eliminating per-iteration closure
allocation.
packages/host-service/src/safety.ts (2)

41-74: Optional: consider preserving a referentially stable name on the wrapped callback.

safeSync / safeAsync return anonymous arrow functions, which means downstream code that introspects .name (e.g., for diagnostics, EventEmitter listenerCount/removeListener parity, or pty/harness.subscribe unsubscription that compares function identity) gets back an unnamed wrapper. Identity-based unsubscription still works because callers retain the wrapper reference, but if any subsystem unsubscribes via name/string lookup or logs the listener name, this will read as empty.

If that's not a concern in any current call site, feel free to ignore.

♻️ Optional refinement
 export function safeSync<Args extends unknown[], R>(
 	label: string,
 	fn: (...args: Args) => R,
 ): (...args: Args) => R | undefined {
-	return (...args: Args) => {
+	const wrapped = (...args: Args) => {
 		try {
 			return fn(...args);
 		} catch (error) {
 			console.error(`[${label}] callback threw — contained`, { error });
 			return undefined;
 		}
 	};
+	Object.defineProperty(wrapped, "name", { value: `safeSync(${label})` });
+	return wrapped;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/safety.ts` around lines 41 - 74, safeSync and
safeAsync return anonymous wrappers which lose the original callback's .name;
update both functions (safeSync and safeAsync) so after creating the wrapper
function you set a stable name on it (e.g., derive from the original fn.name
and/or label) via Object.defineProperty(wrapper, "name", { value: desiredName,
configurable: true }) before returning, preserving referential behavior and
ensuring the wrapper still returns the same types and error-handling semantics.

27-32: Minor: unhandledRejection log includes the promise object.

Logging the promise reference adds little diagnostic value beyond reason and, if a JSON-formatting log transport is layered on top of console.error later, may produce noisy/circular output. The reason (typically the rejection error) plus label/origin is usually sufficient. Not blocking — console.error via Node's default util.inspect handles this fine today.

♻️ Optional simplification
-	process.on("unhandledRejection", (reason, promise) => {
-		console.error(`[${label}] unhandledRejection — staying up`, {
-			reason,
-			promise,
-		});
-	});
+	process.on("unhandledRejection", (reason) => {
+		console.error(`[${label}] unhandledRejection — staying up`, { reason });
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/safety.ts` around lines 27 - 32, The
unhandledRejection handler currently logs both reason and the promise object via
process.on("unhandledRejection", ...) which can produce noisy or circular
output; update the handler to stop including the promise in the console.error
payload and log only the reason (and label/origin) — e.g. call console.error or
your logger with the descriptive message and the rejection reason (use the
existing label variable) and remove the promise field from the logged object so
only useful diagnostic data is emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/host-service/src/events/event-bus.ts`:
- Around line 150-155: In broadcast, avoid allocating the safeSync wrapper per
socket: inside the broadcast method (which currently calls
safeSync("event-bus:send", sendMessage)(socket, message) in the loop) create the
wrapped sender once (e.g., const safeSend = safeSync("event-bus:send",
sendMessage)) and then call safeSend(socket, message) for each socket in
this.clients.keys(); this preserves the same error-isolation behavior while
eliminating per-iteration closure allocation.

In `@packages/host-service/src/runtime/pull-requests/pull-requests.ts`:
- Around line 212-218: The local variable safeSync shadows the imported safeSync
helper and is actually a Promise-producing wrapper created via safeAsync; rename
the local variable (e.g., safeBranchSync) to avoid confusion and clarify intent
where it's used, update its declaration that calls
safeAsync("pull-requests:branch-sync", () => this.syncWorkspaceBranches()) and
any subsequent references/invocations, and optionally do the same renaming
pattern for consistency with safeRefresh to make clear both are async wrappers.

In `@packages/host-service/src/safety.ts`:
- Around line 41-74: safeSync and safeAsync return anonymous wrappers which lose
the original callback's .name; update both functions (safeSync and safeAsync) so
after creating the wrapper function you set a stable name on it (e.g., derive
from the original fn.name and/or label) via Object.defineProperty(wrapper,
"name", { value: desiredName, configurable: true }) before returning, preserving
referential behavior and ensuring the wrapper still returns the same types and
error-handling semantics.
- Around line 27-32: The unhandledRejection handler currently logs both reason
and the promise object via process.on("unhandledRejection", ...) which can
produce noisy or circular output; update the handler to stop including the
promise in the console.error payload and log only the reason (and label/origin)
— e.g. call console.error or your logger with the descriptive message and the
rejection reason (use the existing label variable) and remove the promise field
from the logged object so only useful diagnostic data is emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86716a87-d1a9-442b-b99f-b5acfc42e524

📥 Commits

Reviewing files that changed from the base of the PR and between 50eb125 and b3a6c47.

📒 Files selected for processing (9)
  • apps/desktop/src/main/host-service/index.ts
  • packages/host-service/src/events/event-bus.ts
  • packages/host-service/src/events/git-watcher.ts
  • packages/host-service/src/index.ts
  • packages/host-service/src/runtime/chat/chat.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/safety.ts
  • packages/host-service/src/serve.ts
  • packages/host-service/src/terminal/terminal.ts

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 27, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/main/host-service/index.ts">

<violation number="1" location="apps/desktop/src/main/host-service/index.ts:26">
P1: Installing the process safety net before startup completes changes failure semantics: uncaught startup errors can be absorbed and the process may continue in a bad state. Install this only after the server has successfully started listening.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/desktop/src/main/host-service/index.ts Outdated
- Install process safety net only after server is listening so startup
  throws still reach main().catch and exit non-zero.
- Rename misleading `safeSync` local in pull-requests.ts to
  `runBranchSync`/`runProjectRefresh` — it wraps an async fn.
- Hoist `safeSync(event-bus:send, sendMessage)` to module scope to
  avoid per-broadcast-per-socket closure allocation.
- Use `safeAsync` in git-watcher rescan for consistency with the
  pull-requests interval pattern.
- Drop low-value `promise` field from unhandledRejection log.
Copy link
Copy Markdown
Contributor

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/host-service/src/events/git-watcher.ts`:
- Around line 83-87: The initial rescan in GitWatcher.start is wrapped in
safeAsync (runRescan = safeAsync("git-watcher:rescan", () => this.rescan()))
which swallows startup exceptions before installProcessSafetyNet is installed;
change the startup behavior so startup errors propagate: either move the call to
EventBus.start() so it's invoked after installProcessSafetyNet() is applied (so
the existing safeAsync is fine), or call this.rescan() directly (await or return
its Promise) for the first run without wrapping it in safeAsync so exceptions
bubble to main().catch(); keep the repeating setInterval using safeAsync as-is.
🪄 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

Run ID: 8f862085-766a-440e-98af-7bace077d7d6

📥 Commits

Reviewing files that changed from the base of the PR and between b3a6c47 and 6b66319.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • apps/desktop/src/main/host-service/index.ts
  • packages/host-service/src/events/event-bus.ts
  • packages/host-service/src/events/git-watcher.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/safety.ts
  • packages/host-service/src/serve.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/desktop/src/main/host-service/index.ts
  • packages/host-service/src/serve.ts
  • packages/host-service/src/runtime/pull-requests/pull-requests.ts
  • packages/host-service/src/events/event-bus.ts

Comment thread packages/host-service/src/events/git-watcher.ts Outdated
…-out guards

Drop safeSync/safeAsync wrappers entirely. The Node process-level handlers
already catch throws from setInterval/setTimeout bodies, EventEmitter
listeners, native pty callbacks, and orphaned promise continuations — so
wrapping each one individually was redundant.

The two cases that genuinely need inline error handling are loops over
multiple subscribers, where a throw skips the rest of the iteration:

- EventBus.broadcast — wrap per-socket send and drop dead sockets, matching
  the opencode pty fan-out pattern. Converts "log forever per broadcast"
  into "one log + clean state".
- GitWatcher.scheduleFlush listener loop — inline try/catch so one bad
  subscriber can't skip siblings.

Aligns with the field norm (opencode, tabby, hyper, mastra-code-ui all rely
on process-level handlers + targeted inline guards rather than per-seam
wrappers). Net -68 lines.
…nterval

- installProcessSafetyNet had a default label arg that both call sites
  passed identically; drop the param and inline "host-service" in the logs.
- git-watcher.ts start() reverts to its original setInterval one-liner
  shape (the multi-line form was leftover from an earlier refactor).
Copy link
Copy Markdown
Contributor

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

♻️ Duplicate comments (1)
packages/host-service/src/events/git-watcher.ts (1)

81-85: ⚠️ Potential issue | 🟠 Major

Initial rescan is still detached from startup failure flow.

At Line 82, void this.rescan() starts initialization work without awaiting it, so bootstrap cannot deterministically fail on first-scan errors. That keeps this path outside startup error propagation.

Suggested fix
-	start(): void {
-		void this.rescan();
+	async start(): Promise<void> {
+		await this.rescan();
 		this.rescanTimer = setInterval(() => {
 			void this.rescan();
 		}, RESCAN_INTERVAL_MS);
 	}

If you take this route, call sites should await gitWatcher.start() during startup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/events/git-watcher.ts` around lines 81 - 85, The
start() method currently calls this.rescan() without awaiting it, so
initialization errors are detached from startup; change start() to be async (or
return a Promise) and await this.rescan() so the first scan errors propagate to
bootstrap, then set this.rescanTimer = setInterval(() => void
this.rescan().catch(err => /* log/handle */), RESCAN_INTERVAL_MS) so subsequent
scans don't crash the process; update any callers to await gitWatcher.start()
during startup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/host-service/src/events/git-watcher.ts`:
- Around line 81-85: The start() method currently calls this.rescan() without
awaiting it, so initialization errors are detached from startup; change start()
to be async (or return a Promise) and await this.rescan() so the first scan
errors propagate to bootstrap, then set this.rescanTimer = setInterval(() =>
void this.rescan().catch(err => /* log/handle */), RESCAN_INTERVAL_MS) so
subsequent scans don't crash the process; update any callers to await
gitWatcher.start() during startup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f172284-2cc1-497e-856e-720a0aa2c104

📥 Commits

Reviewing files that changed from the base of the PR and between 6b66319 and 23bc763.

📒 Files selected for processing (3)
  • packages/host-service/src/events/event-bus.ts
  • packages/host-service/src/events/git-watcher.ts
  • packages/host-service/src/safety.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/host-service/src/events/event-bus.ts

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