Skip to content

fix(cli): force-exit headless runs so a lingering handle can't wedge kimi -p - #1233

Merged
RealKai42 merged 4 commits into
mainfrom
kaiyi/bangalore
Jun 30, 2026
Merged

fix(cli): force-exit headless runs so a lingering handle can't wedge kimi -p#1233
RealKai42 merged 4 commits into
mainfrom
kaiyi/bangalore

Conversation

@RealKai42

Copy link
Copy Markdown
Collaborator

Problem

kimi -p (headless/print mode) can hang after the run completes: the agent finishes, prints its answer and the resume hint, then the process never exits and is eventually killed by an external timeout (e.g. a benchmark/sandbox wall-clock limit).

Root cause

Print mode never calls process.exit() — it relies on the Node event loop draining once the run is done. If a stray ref'd handle survives shutdown — a lingering socket (e.g. a connection blackholed by a restrictive firewall, or an HTTP/2 session kept alive by PING), an un-cleared timer, or a child whose pipes stay open — the loop never empties and the completed run hangs.

The full headless path (turn → run_in_background task → telemetry → harness.close()) was reproduced end to end; on a healthy network it exits in ~1–2s, so the lifecycle/background-cleanup code itself is fine — the hang needs a lingering handle, and -p has no fallback to force exit.

Fix

  • Arm an unref'd force-exit fallback after a headless run completes (scheduleHeadlessForceExit). A healthy run drains and exits before it fires, so behaviour is unchanged; it only force-exits a run whose loop is already wedged. The exit code is read lazily, so goal turns that set process.exitCode are preserved.
  • Bound prompt cleanup with a timeout so a wedged shutdown step (a SessionEnd hook, MCP shutdown, or a blackholed connection) can't keep a completed run alive forever.

Tests

  • New unit tests for the force-exit guard: fires with the resolved code after the grace; the timer is unref'd so a healthy run isn't delayed; cancellable via clearTimeout.
  • New runPrompt test: completes even when harness.close() never resolves.
  • Full suite green except 6 pre-existing failures in test/cli/update/preflight.test.ts (unrelated to this change — they fail identically on main).

…kimi -p

Print mode (`kimi -p`) never calls process.exit(); it relies on the Node
event loop draining once the run completes. A stray ref'd handle left over
from the run — a lingering socket, an un-cleared timer, or a child whose
pipes stay open — keeps the loop alive, so a finished run hangs until an
external timeout kills it instead of exiting.

- Arm an unref'd force-exit fallback after a headless run completes. A
  healthy run drains and exits before it fires (behaviour unchanged); it
  only force-exits a run whose loop is already wedged.
- Bound prompt cleanup with a timeout so a wedged shutdown step (a
  SessionEnd hook, MCP shutdown, or a connection blackholed by a
  restrictive firewall) can't keep a completed run alive forever.

Add unit tests for the force-exit guard and for cleanup staying bounded
when harness.close() hangs.
@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 269b9d5

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 Jun 30, 2026

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

commit: 269b9d5

@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: 2e41108a8f

ℹ️ 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 apps/kimi-code/src/main.ts Outdated
…eMainCommand pure

handleMainCommand is a reusable, exported, unit-tested handler — scheduling a
deferred process.exit inside it could terminate a test runner or an embedding
host once the spied exit was restored. Make it pure: it now returns a
MainCommandOutcome, and the process entrypoint (the program action, which
already owns the error-path process.exit) arms the unref'd force-exit fallback
only for a completed headless run.

@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: 8672e4fc6c

ℹ️ 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 apps/kimi-code/src/main.ts Outdated
…ing output

The unref'd force-exit fallback fired after a fixed grace even when the event
loop was alive only because buffered stdout/stderr had not finished flushing to
a slow or piped consumer (the prompt writers ignore write() backpressure). That
could truncate the assistant output or resume hint of an otherwise-successful
`kimi -p` run — a regression versus the previous natural-drain exit.

finalizeHeadlessRun now flushes stdio first (bounded by HEADLESS_STDIO_DRAIN_TIMEOUT_MS)
and only then arms the force-exit. Draining first also means a leaked handle is
the only thing that can still hold the loop, so the backstop stays precise and
bounded.

@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: 28ce5a82f1

ℹ️ 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 apps/kimi-code/src/cli/run-prompt.ts Outdated
The cleanup timeout guard caught every rejection, so a cleanup step that failed
fast (e.g. restoring a resumed session's permission, or harness.close hitting a
persistence error) was silently swallowed: runPrompt resolved as success and the
session could be left in `auto` with no error surfaced.

A timeout should bound how long we wait, not change the outcome. raceWithTimeout
now propagates a rejection that settles before the timeout, and only swallows the
abandoned promise's late rejection once the timeout has won the race.
@RealKai42
RealKai42 merged commit 020992c into main Jun 30, 2026
9 checks passed
@RealKai42
RealKai42 deleted the kaiyi/bangalore branch June 30, 2026 14:13
@github-actions github-actions Bot mentioned this pull request Jun 30, 2026
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