Skip to content

fix: close two silent-exit vectors around unhandled rejections - #1758

Merged
RealKai42 merged 3 commits into
mainfrom
fix/silent-crash-vectors2
Jul 15, 2026
Merged

fix: close two silent-exit vectors around unhandled rejections#1758
RealKai42 merged 3 commits into
mainfrom
fix/silent-crash-vectors2

Conversation

@RealKai42

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problem is explained below (from the same user report that led to #1757).

Problem

A user on 0.24.2 hit repeated "the CLI exited by itself" events — twice in ~80 minutes, once mid-stream, once idle — with zero diagnostics anywhere: nothing in the log, nothing in crash telemetry, no shell signal message, non-zero exit code. Analysis showed every graceful-exit and signal path is ruled out, leaving the TUI's uncaughtException / unhandledRejection crash path as the only consistent mechanism. That path leaves no evidence by design: the log line is dropped on exit (fixed in #1757), and crash telemetry only listens on uncaughtExceptionMonitor, which never fires for a rejection that has a listener — and the TUI always registers one, so unhandledRejection exits are completely invisible.

While auditing always-on async callbacks that can reject in both streaming and idle states, we also found a concrete crash vector: the Ctrl+V clipboard-image dispatch chains off the async handler with no rejection branch, so any failure inside it becomes an unhandled rejection and takes the whole CLI down via the path above.

What changed

  • Crash telemetry now observes unhandledRejection directly. Registering a real listener would suppress Node's default crash-on-rejection in modes that have no handler of their own (print / server), so when the crash handler is the only listener it rethrows — preserving the default non-zero exit, deduped so the monitor does not double-report. When another handler owns the lifecycle (the TUI), it records the crash without interfering.
  • Ctrl+V image paste: a rejecting handler now falls back to the normal text-paste path (same as "no image available") instead of leaking an unhandled rejection.
  • Tests: rejection is recorded when another handler exists; AbortError rejections are ignored; sole-listener rethrow keeps Node's default exit semantics and is recorded exactly once; the existing duplicate-registration test now also covers the new listener; editor test proves a rejecting paste handler produces no unhandled rejection and still pastes text.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

The crash handler only listened on uncaughtExceptionMonitor, which never
fires for a rejection that has a listener — and the TUI always registers
one, converting rejections into a silent exit(1) with no telemetry at
all. Observe unhandledRejection directly so those crashes still leave a
trace. Since registering a real listener suppresses Node's default
crash-on-rejection, rethrow when we are the only listener (print /
server modes), deduped so the monitor does not double-report.
The Ctrl+V image-paste dispatch chained off the async handler without a
rejection branch, so any failure inside it became an unhandled
rejection — which the CLI's crash path turns into a silent exit(1).
Treat a rejection like "no image available" and paste as text.
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5a125d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@5a125d1
npx https://pkg.pr.new/@moonshot-ai/kimi-code@5a125d1

commit: 5a125d1

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3148a51067

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/telemetry/src/crash.ts Outdated
const soleListener = process.listenerCount('unhandledRejection') === 1;
if (!isAbortError(reason)) {
trackCrash(rejectionErrorType(reason), 'unhandledRejection');
if (reason instanceof Error) recordedRejections.add(reason);

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 Badge Deduplicate non-Error rejection reasons

In print/server modes where this handler is the sole unhandledRejection listener, a rejection like Promise.reject({ code: 'E' }) is tracked here and then rethrown. Because only Error instances are added to recordedRejections, the subsequent uncaughtExceptionMonitor pass is not deduped and records a second crash event for the same failure; primitive/null reasons can also hit the monitor's less-safe error-type path after being rethrown. This corrupts crash counts for non-Error rejection reasons, so the dedupe needs to cover every rethrown reason or the monitor needs to use the same safe classification.

Useful? React with 👍 / 👎.

Non-Error rejection reasons (plain objects, strings, null) were recorded
by the rejection handler but not added to the dedupe set, so the
uncaughtExceptionMonitor pass after the rethrow reported a second crash
for the same failure; null/undefined reasons also crashed the monitor's
own error-type extraction. Use a Set so primitives dedupe by value, add
every rethrown reason, and classify monitor crashes null-safely.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a125d1a4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +63 to +64
if (soleListener) {
throw reason;

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 Badge Honor configured unhandled-rejection modes

When the CLI is run with NODE_OPTIONS=--unhandled-rejections=warn or none, Node's documented behavior from node --help is that warn logs a warning and none silences warnings instead of terminating. Because this handler now always throws whenever it is the only unhandledRejection listener, installing crash telemetry turns those explicitly configured non-fatal rejections into an immediate exit(1), so it no longer preserves the process' unhandled-rejection policy outside the default throw mode.

Useful? React with 👍 / 👎.

@RealKai42
RealKai42 merged commit 1d7c205 into main Jul 15, 2026
14 checks passed
@RealKai42
RealKai42 deleted the fix/silent-crash-vectors2 branch July 15, 2026 18:14
@github-actions github-actions Bot mentioned this pull request Jul 15, 2026
wonggm added a commit to wonggm/kimi-code that referenced this pull request Jul 22, 2026
…rminal hosts

The 'falls back to the text paste path when the image paste handler
rejects' test was added in upstream PR MoonshotAI#1758 with a key selector that
only handled process.platform === 'win32'. Our fork extends the paste
key to also bind Alt+V when isWindowsTerminalHost() is true (WT_SESSION
/ WSLENV / WSL_DISTRO_NAME set), so the test must send '\u001Bv' on
those hosts — otherwise the pasteKey check misses, onPasteImage is never
invoked, and onTextPaste never fires.

Triggers here in WSL2 (WSL_DISTRO_NAME=Ubuntu-22.04, WT_SESSION set):
without this fix the test fails with 'expected onTextPaste to be called
once, but got 0 times'.
wonggm added a commit to wonggm/kimi-code that referenced this pull request Jul 24, 2026
…rminal hosts

The 'falls back to the text paste path when the image paste handler
rejects' test was added in upstream PR MoonshotAI#1758 with a key selector that
only handled process.platform === 'win32'. Our fork extends the paste
key to also bind Alt+V when isWindowsTerminalHost() is true (WT_SESSION
/ WSLENV / WSL_DISTRO_NAME set), so the test must send '\u001Bv' on
those hosts — otherwise the pasteKey check misses, onPasteImage is never
invoked, and onTextPaste never fires.

Triggers here in WSL2 (WSL_DISTRO_NAME=Ubuntu-22.04, WT_SESSION set):
without this fix the test fails with 'expected onTextPaste to be called
once, but got 0 times'.
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