Skip to content

fix: resolve two bugs making interactive mode unusable with plugin ecosystems (#825)#830

Merged
kevincodex1 merged 2 commits intoGitlawb:mainfrom
fernandoxavier02:fix/interactive-hangs-issue-825
May 7, 2026
Merged

fix: resolve two bugs making interactive mode unusable with plugin ecosystems (#825)#830
kevincodex1 merged 2 commits intoGitlawb:mainfrom
fernandoxavier02:fix/interactive-hangs-issue-825

Conversation

@fernandoxavier02
Copy link
Copy Markdown

Summary

This PR fixes the two bugs I documented in #825. Together they make openclaude interactive mode effectively unusable for any user with a non-trivial plugin ecosystem installed — which is the stated target audience of the project. Non-interactive mode (openclaude -p "...") is unaffected by both.

Each bug is addressed in its own commit so they can be reviewed, cherry-picked, or reverted independently:

  1. fix(ink): import logForDebugging in App.tsx to prevent ReferenceError
  2. fix(hooks): always close stdin after initial hook payload

Bug 1 — ReferenceError: logForDebugging is not defined

src/ink/components/App.tsx calls logForDebugging() in four places (XTVERSION async handlers at lines 266/268, handleReadable and handleDataChunk stdin re-attach branches at lines 373/394) but never imports the function.

In the bundled dist/cli.mjs (v0.6.0), esbuild tried to resolve the undefined identifier from the surrounding scope and ended up with a name collision: most of the 2,400+ call sites throughout the bundle were renamed to logForDebugging2, but the four in App.tsx were left pointing at the original name, which is undefined at runtime. Any modern terminal replying to the XTVERSION probe (CSI > 0 q), or any stdin error-recovery path, triggers:

[UNHANDLED] ReferenceError: logForDebugging is not defined
  at file:///.../@gitlawb/openclaude/dist/cli.mjs:138103:17
  at process.processTicksAndRejections (node:internal/process/task_queues:105:5)

Fix: add the missing import from ../../utils/debug.js. After this change, the bundler emits a single consistent name (logForDebugging) for every call site — the 2 suffix disappears entirely from the bundle, which is empirical evidence that it only existed to work around the unresolved reference.

Diff: 1 line added.

Bug 2 — UserPromptSubmit hooks hang for up to 60 s on every prompt

In src/utils/hooks.ts:1352-1354, the initial hook-payload write closes stdin only when requestPrompt is falsy:

if (!requestPrompt) {
  child.stdin.end()
}

requestPrompt is a useCallback bound to the REPL (src/screens/REPL.tsx:2556), so in interactive mode it is always truthy. Stdin therefore never closes. Every plugin hook written against the documented Anthropic hook input contract reads stdin until EOF — hooks block on their per-hook timeout (default 60 s) while the prompt pipeline waits. The provider is never called during that window; ~/openclaude-debug.log shows zero FETCH events until the timeouts fire.

With a typical plugin setup (pipeline-orchestrator, superpowers / superpowers-dev, skill-advisor, episodic-memory, reflexion, remember, and friends — ~10 UserPromptSubmit hooks), this serializes into minutes of wait per user message.

Fix: always close stdin after writing the initial JSON payload, restoring the documented EOF-based contract. I've added a comment in the source explaining the rationale and linking back to #825.

Diff: 4 lines removed, 9 lines added (the bulk is the explanatory comment).

⚠️ Trade-off you should be aware of

The code at hooks.ts:1237 writes a second JSON payload into the hook's stdin as a response to a prompt request — a duplex-stdin pattern (hook emits prompt requests on stdout, host writes responses back to stdin on the same stream). This PR breaks that path. An EOF-terminated single-shot stream and a duplex RPC channel are incompatible by design on the same pipe; you can have one or the other.

The right long-term solution is a separate IPC channel for prompt-response — a named pipe, child.send (Node IPC), or an extra stdio stream. That refactor is larger than this fix and belongs in its own discussion.

Given that:

  • every user with a plugin ecosystem sees unusable interactive mode today,
  • the prompt-response duplex path serves a narrow, optional feature (only hooks that explicitly emit prompt requests), and
  • the hook contract documented upstream specifies EOF close,

I think the trade-off is worth making now while a proper IPC channel is designed. If you'd prefer to preserve the duplex path at the cost of keeping every Anthropic-spec plugin broken, I'm happy to rework this PR — please flag and I'll iterate. An alternative I considered but didn't implement here: an opt-in config flag on the hook (e.g. interactivePrompt: true) that keeps stdin open for hooks that declare they need it, while defaulting to the EOF contract for everyone else.

Verification

  • bun install && bun run build — clean build.
  • bun run smoke — passes (0.6.0 (Open Claude)).
  • Bundle inspection confirms both fixes compile as expected:
    • logForDebugging2 is absent from the regenerated dist/cli.mjs (all call sites unified to logForDebugging).
    • Both child.stdin.end() call sites in hooks.tsdist/cli.mjs (lines 502030 and 502168 in the regenerated bundle) are now unconditional.
  • I patched the same two changes locally against my installed dist/cli.mjs before opening this PR. Typical oi turnaround dropped from ~60 s (hook timeouts) to ~1 s (actual API latency). All plugin hooks that return JSON on the initial stdin payload now complete correctly and the model responds promptly.
  • Reproduction steps and the full debug-log evidence are in Two critical bugs in v0.6.0 make interactive mode unusable with plugin ecosystems #825.

Scope / not-in-this-PR

  • No new tests. I looked for an interactive-mode harness to cover execCommandHook's stdin behaviour but didn't find one in-tree (the existing *.hooks.test.ts covers conversationRecovery, which is a different domain). Happy to add one in a follow-up PR if you point me at the pattern you'd like.
  • No changes to dist/ — that's generated output.
  • The duplex-stdin IPC discussion belongs in its own issue.

Environment where this was reproduced and verified

  • openclaude v0.6.0 (@gitlawb/openclaude, installed via npm -g)
  • Node: v24.6.0 on Windows 11 Pro 10.0.26200 (original reproduction)
  • Node: v20+ / Bun 1.3.11 on macOS 15 arm64 (fix development and source build)
  • Provider: Codex OAuth, also reproduces with Anthropic-default provider
  • Shells: PowerShell 7, bash (git-for-windows), zsh (macOS)

Closes #825

App.tsx used `logForDebugging()` in four call sites (XTVERSION async
handlers, handleReadable/handleDataChunk stdin error-recovery branches)
without importing it. When esbuild bundled this, the unresolved symbol
collided with another identifier in scope; the bundler renamed most
references to `logForDebugging2` but left the four in App.tsx pointing
at the original name, which became undefined in the bundle. At runtime
any modern terminal replying to the XTVERSION probe triggers an
`unhandledRejection: logForDebugging is not defined`.

Adding the missing import resolves the symbol before bundling, so
the bundler emits a single consistent name for every call site.

Refs Gitlawb#825
The conditional `if (!requestPrompt) child.stdin.end()` kept stdin
open in interactive mode (where requestPrompt is always truthy while
the REPL is mounted). Every plugin hook written against the Anthropic
hook input contract reads stdin until EOF, so hooks blocked on the
per-hook timeout (default 60s) on every user message — no HTTP request
to the provider was made until every UserPromptSubmit hook had timed
out. With ~10 plugins hooked to UserPromptSubmit (pipeline-orchestrator,
superpowers, skill-advisor, episodic-memory, reflexion, etc.), a single
prompt accumulated minutes of wait before any model call.

Always closing stdin after the initial JSON payload restores the
documented EOF-based contract. Verified locally: typical `oi`
turnaround drops from ~60s to ~1s.

Trade-off: the existing duplex-stdin path (hooks emitting prompt
requests on stdout and receiving responses written back to stdin at
hooks.ts:1237) is incompatible with an EOF contract by design and
stops working with this change. Restoring that feature requires a
separate IPC channel (named pipe, node IPC, or a second stream)
rather than reusing the initial stdin; that refactor is out of scope
for this fix. Given the blast radius of the current behaviour (every
user with a plugin ecosystem sees unusable interactive mode), trading
the rarely-used duplex path for the documented single-shot contract
is the right short-term move.

Closes Gitlawb#825
Copilot AI review requested due to automatic review settings April 22, 2026 15:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes two regressions in interactive mode that impact users running openclaude with plugin ecosystems: a runtime ReferenceError in Ink UI code and hook execution hangs caused by never-closed stdin.

Changes:

  • Import logForDebugging in src/ink/components/App.tsx to prevent a runtime ReferenceError.
  • Always close hook child-process stdin after writing the initial JSON payload to restore the EOF-based hook input contract.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/utils/hooks.ts Unconditionally closes hook stdin after writing initial payload to prevent interactive-mode hangs with EOF-reading hooks.
src/ink/components/App.tsx Adds missing logForDebugging import to avoid runtime failures in XTVERSION / stdin recovery paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/utils/hooks.ts
Comment on lines +1356 to +1360
// future prompt-response on the same channel caused every UserPromptSubmit
// hook to block for the full per-hook timeout (default 60s) on every user
// message in interactive mode, since requestPrompt is truthy whenever the
// REPL is mounted. See issue #825 for the full analysis.
child.stdin.end()
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

Now that stdin is always closed via child.stdin.end(), the existing prompt-request implementation (which later writes prompt responses back to child.stdin when requestPrompt is set) will hit write-after-end/EPIPE behavior and won’t be able to deliver responses.

If dropping duplex prompt-response is intentional, consider explicitly disabling that path (stop parsing prompt requests and emit a clear log/warning) and update the stdin error-handler comment/logic that still says stdin stays open for prompt responses. If it should remain supported, it needs a separate IPC channel/stdio stream.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

@gnanam1990 gnanam1990 left a comment

Choose a reason for hiding this comment

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

Thanks for the precise diagnosis — both root causes (esbuild name collision + requestPrompt-truthy stdin leak) are well-explained, and the trade-off on the duplex pipe is documented honestly. Surgical fix for #825. LGTM 🚀

@Meetpatel006
Copy link
Copy Markdown
Contributor

LGTM

@kevincodex1 kevincodex1 merged commit e438c89 into Gitlawb:main May 7, 2026
4 of 6 checks passed
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.

Two critical bugs in v0.6.0 make interactive mode unusable with plugin ecosystems

5 participants