Skip to content

Bug Fix - IntelliJIdea IDE freezes caused by a stdin/stdout pipe deadlock betwee… - #92

Open
Lycogrypho wants to merge 4 commits into
serialexperimentslainnnn:developfrom
Lycogrypho:develop
Open

Lycogrypho wants to merge 4 commits into
serialexperimentslainnnn:developfrom
Lycogrypho:develop

Conversation

@Lycogrypho

Copy link
Copy Markdown

PROBLEM
During long sessions IntelliJ IDEA froze, sometimes for over an hour, and only recovered when the claude process was restarted. idea.log records 41 UI freezes. All the long ones are blocked in ClaudeProcess.writeLine / FileOutputStream.writeBytes.

ROOT CAUSE
The stdout reader thread wrote auto-approved permission replies to claude's stdin synchronously, while processing claude's output. When the stdin pipe was full, that thread blocked and stopped draining stdout. claude then blocked on its own stdout and stopped reading stdin, so neither side could proceed. The UI thread, sending a periodic usage query, then blocked on the same write lock and froze the IDE. Teardown also closed stdin under that lock, so it could stall behind the stuck write.

CHANGES

  • New controller/process/StdinWriter.kt: writes newline-terminated UTF-8 lines to stdin on a single dedicated thread, preserving order. Write failures are logged and do not stop later lines.
  • controller/process/ClaudeProcess.kt: writeLine queues the line on a bounded single-thread application pool executor and returns immediately; it still returns false when the process is dead. The write lock is removed. Teardown destroys the process before closing stdin, so a write stuck on a full pipe fails instead of stalling the shutdown.
  • New test controller/process/StdinWriterTest.kt: a pipe that stops draining does not block the caller; lines arrive whole and in order; a broken pipe does not stop later lines.
  • No security code was changed. The fix sits below PermissionBroker, which is untouched.
  • Documentation: CLAUDE.md gains the standard header. DIRECTIVES.md gains a "How the plugin is put together" architecture section. AGENTS.md points to DIRECTIVES.md for architecture and adds commands for a single test class, integration tests, a single frontend test file, and running builds and tests through the IDE's run server. CONTRIBUTING.md points to CLAUDE.md and DIRECTIVES.md and drops the link to the missing PROJECTMAP.md.

BEHAVIOUR CHANGE
writeLine now reports that a line was queued, not that it was written. A later pipe failure is logged and handled by the existing process-termination path.

VERIFICATION
Built with JBR 21.0.11 on Windows. StdinWriterTest (3), EdtProcessTeardownContractTest (4), and the NoComments, PackageDependency and FileSize contract tests pass. spotlessCheck and detekt pass. Full JVM suite: 4881 tests, 21 failed, 8 skipped. The same 21 fail on the original code: they depend on a POSIX environment (the bin/fake-claude script, Unix socket permissions and path length, symlinks, path separators). buildPlugin produced build/distributions/claude-code-native-6.0.1.zip, which contains StdinWriter and the updated ClaudeProcess.

OPEN POINTS
Not yet verified end to end in a running IDE. The integration tests that drive a real process cannot run on Windows. Manual check: install the zip and run a long task that previously froze.

…n the plugin and the claude process. Restructure the agent documentation so its cross-references resolve.

PROBLEM
During long sessions IntelliJ IDEA froze, sometimes for over an hour, and only recovered when the claude process was restarted. idea.log records 41 UI freezes. All the long ones are blocked in ClaudeProcess.writeLine / FileOutputStream.writeBytes.

ROOT CAUSE
The stdout reader thread wrote auto-approved permission replies to claude's stdin synchronously, while processing claude's output. When the stdin pipe was full, that thread blocked and stopped draining stdout. claude then blocked on its own stdout and stopped reading stdin, so neither side could proceed. The UI thread, sending a periodic usage query, then blocked on the same write lock and froze the IDE. Teardown also closed stdin under that lock, so it could stall behind the stuck write.

CHANGES
- New controller/process/StdinWriter.kt: writes newline-terminated UTF-8 lines to stdin on a single dedicated thread, preserving order. Write failures are logged and do not stop later lines.
- controller/process/ClaudeProcess.kt: writeLine queues the line on a bounded single-thread application pool executor and returns immediately; it still returns false when the process is dead. The write lock is removed. Teardown destroys the process before closing stdin, so a write stuck on a full pipe fails instead of stalling the shutdown.
- New test controller/process/StdinWriterTest.kt: a pipe that stops draining does not block the caller; lines arrive whole and in order; a broken pipe does not stop later lines.
- No security code was changed. The fix sits below PermissionBroker, which is untouched.
- Documentation: CLAUDE.md gains the standard header. DIRECTIVES.md gains a "How the plugin is put together" architecture section. AGENTS.md points to DIRECTIVES.md for architecture and adds commands for a single test class, integration tests, a single frontend test file, and running builds and tests through the IDE's run server. CONTRIBUTING.md points to CLAUDE.md and DIRECTIVES.md and drops the link to the missing PROJECTMAP.md.

BEHAVIOUR CHANGE
writeLine now reports that a line was queued, not that it was written. A later pipe failure is logged and handled by the existing process-termination path.

VERIFICATION
Built with JBR 21.0.11 on Windows. StdinWriterTest (3), EdtProcessTeardownContractTest (4), and the NoComments, PackageDependency and FileSize contract tests pass. spotlessCheck and detekt pass. Full JVM suite: 4881 tests, 21 failed, 8 skipped. The same 21 fail on the original code: they depend on a POSIX environment (the bin/fake-claude script, Unix socket permissions and path length, symlinks, path separators). buildPlugin produced build/distributions/claude-code-native-6.0.1.zip, which contains StdinWriter and the updated ClaudeProcess.

OPEN POINTS
Not yet verified end to end in a running IDE. The integration tests that drive a real process cannot run on Windows. Manual check: install the zip and run a long task that previously froze.
…ry question card offers an Other answer with a free-text field.

The model label could show a tagline from the catalog description instead of the model's name, because it took whatever came before " · ". It now uses only a segment that names the model with a version. Question cards had no way to give an answer outside the offered options; each question now ends with an Other choice that opens a text field, and the typed text is what gets sent.

src/main/kotlin/dev/lain/claudejb/view/payload/composer/JcefModelLabels.kt

 - modelDisplayLabel picks the first description segment that starts with the model family and carries a version. Failing that, it uses the display name, then the name derived from the model id, but only if they carry a version. Failing that, it uses the bare name. A description segment without the family and a version is never used, because the catalog sometimes puts only a tagline there. The version pattern needs a word boundary after the digits, so a context size such as "1M" is not read as a version.

src/main/ts/jcef/views/permissions/cards/question.ts

 - Every question ends with one fixed Other option. An Other supplied by the model (matched case-insensitively) is dropped rather than shown twice. Choosing Other reveals the text field and focuses it, and deselecting it hides the field. On submit, Other is replaced by the trimmed text typed into the field, so the model gets the answer and not the word Other. If the field is empty, Other is left out. On a multi-select the typed text is joined with the other chosen options. The click logic and option construction were moved into toggle, optionButton, otherInput and answerFor, keeping the builder readable and inside the file-size ceiling.

src/main/resources/jcef/css/views/permissions/cards.css

 - Layout and hidden state of .q-other-input: full width, sitting under the options.

src/main/resources/jcef/css/views/permissions/elicit.css

 - .q-other-input joins the existing input, focus and placeholder selectors, reusing the existing field style instead of defining a second one.

src/test/kotlin/dev/lain/claudejb/view/payload/composer/JcefModelLabelTest.kt

src/test/frontend/permissions/question-card.test.js

Verification: npm test 46 files / 686 tests passed; eslint and prettier --check clean; gradlew test on JcefModelLabelTest and all *ContractTest passed; spotlessCheck passed. Not verified: detekt fails before analysing anything (Gradle appears to run on JDK 25 instead of JBR 21); the IDE's own analysis of the files had not finished; the keyboard pass on the new field has not been done.
… transcript, the up and down arrows recall earlier prompts, and the input area folds away.

Three chat-page improvements: User rows get an accent tint and a left bar but stay rows, not cards. Up and down recall the prompts already sent in the tab, read from the transcript so no prompt text is stored anywhere new. The input area folds into a one-line strip that shows the turn's phase, and permission and question cards stay answerable while folded.

src/main/resources/jcef/css/views/chat/rows.css

 - .msg.user gets a 2px accent left border, its body a color-mix of --accent and --text, and its "You" label --accent, all from theme variables so every theme follows. A background or shadow was rejected: it would bring back the card the existing transcript test forbids.

src/main/ts/jcef/models/composer/history.ts

 - Pure step over the sent prompts, skipping repeats sent back to back. Going up from no recall saves the box's current text as the draft; going down past the newest restores it.

src/main/ts/jcef/controllers/composer/history.ts

 - Reads the prompts from the conversation's .msg.user .body rows (raw text, so the typed markdown) rather than recording them on send, which would miss resumed sessions. Acts only with the caret on the first line (up) or the last line (down) and without modifiers, so a multi-line prompt stays editable. Restarts from the newest once the box no longer holds the text it last recalled, which covers both edits and sends. Kept out of send.ts, which is near the 250-line ceiling.

src/main/ts/jcef/controllers/composer/fold.ts

 - A strip first in #dock with a phase label and a toggle carrying aria-expanded and aria-controls. Folding sets #dock.folded. The fold is offered only while the session runs and no sign-in is wanted. Ctrl+Shift+Down folds and Ctrl+Shift+Up unfolds (the owner's choice over Alt+Shift, which the editor uses to move lines). While folded, a printable key or Up outside an editable unfolds and focuses the input. Focus moves to the toggle when the input it was in is folded away. Stick-to-bottom is sampled before the toggle and re-applied after. Keys are ignored once the strip is detached from the page.

src/main/resources/jcef/css/views/composer/fold.css

 - Hides #composer, #controls and #palette when folded, never #permissions: a pending card blocks the turn, and answering it needs no input.

src/main/ts/jcef/controllers/composer/composer.ts

 - Wires the history on the input and renders the fold on every state push.

src/main/ts/jcef/types/composer.d.ts

 - Types for the history step and the two new composer entry points.

src/main/kotlin/dev/lain/claudejb/view/jcef/PageAssembly.kt

 - Registers the two history modules and the fold module before send.js and composer.js, which call them while the page loads, and fold.css in CSS_PARTS.

.gitignore

 - Keeps the local working documents ToDo.md, Done.md and DesignNotes.md out of the repository; the root *.md allowlist would otherwise track them.

src/test/frontend/chat/user-row-colour.test.js

src/test/frontend/composer/prompt-history.test.js

src/test/frontend/composer/composer-fold.test.js
…iority 1 entry of the 2026-11-12 audit is closed.

Eighteen defects fixed tests-first across the session lifecycle, the chat page, the plugin's MCP tools and the settings form, plus two security entries changed on the owner's explicit order (F_0038, F_0039). Nothing under model/permission and no guard test is touched.

src/main/kotlin/dev/lain/claudejb/model/diff/DiffPresenter.kt

 - F_0038, on order. isWithinRoot resolves the nearest existing ancestor with toRealPath and re-appends the missing tail, because File.canonicalFile does not resolve links or junctions on Windows and the check also runs on files not created yet. A dangling symlink is refused.

src/main/kotlin/dev/lain/claudejb/controller/bridge/BridgeSettings.kt

 - F_0039, on order. JcefSettingsMenu.apply is called once, inside settings.update, with the verdict taken from it. It ran three times per toggle (twice on the live state, once on the store copy); update's own two runs remain. No duplicated effect was observed: every effect reached is idempotent.

src/main/kotlin/dev/lain/claudejb/controller/session/SessionLifecycle.kt
src/main/kotlin/dev/lain/claudejb/controller/session/auth/AuthGate.kt

 - F_0006. An UNKNOWN credential answer is resolved on a pooled thread and then launches or asks for login directly; going back through start() would get UNKNOWN again.
 - F_0007. The ready block checks the launch generation.

src/main/kotlin/dev/lain/claudejb/controller/session/ProcessSlot.kt
src/main/kotlin/dev/lain/claudejb/controller/session/SessionProcess.kt
src/main/kotlin/dev/lain/claudejb/controller/session/StaleResume.kt

 - F_0007. Generation and process live in one slot claimed under one lock; events of a superseded launch are dropped.
 - F_0008. A failed resume starts a new conversation only when the transcript file is gone. Matching the error text was not possible: stderr is logged, not kept.

src/main/kotlin/dev/lain/claudejb/controller/session/ClaudeSession.kt

 - F_0010. send queues the prompt whatever start() answers, since the attachment tray is already empty by then. A /btw with no running session asks to be repeated instead of becoming a normal prompt.

src/main/kotlin/dev/lain/claudejb/controller/session/ChatSessionManager.kt
src/main/kotlin/dev/lain/claudejb/controller/session/events/EarlyLines.kt
src/main/kotlin/dev/lain/claudejb/controller/session/events/ToolEvents.kt

 - F_0009. Sessions are disposed through Disposer, so the message-bus connection goes with them. Early tool output is bounded to 16 tool ids; filtering by session was rejected because the dispatcher does not know which chat a client belongs to.

src/main/kotlin/dev/lain/claudejb/controller/session/SessionLiveSettings.kt

 - F_0011. adopt assigns session.launch once, then restarts once or sends set_model and set_permission_mode. It no longer writes the permission mode back to the settings it just read.

src/main/kotlin/dev/lain/claudejb/controller/process/ClaudeProcess.kt
src/main/kotlin/dev/lain/claudejb/controller/process/StdoutLines.kt
src/main/kotlin/dev/lain/claudejb/model/protocol/parse/ControlParsers.kt

 - F_0012. A control request without a request object, or inside a line over the size cap, is answered with an error. The rest of an oversized line is discarded up to its newline, and the last line of a dying process is parsed.

src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/run/TestRunChoice.kt
src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/run/TestTools.kt

 - F_0013. run_tests prefers a framework's own runner, then the Gradle configuration whose tasks include test, so a test file no longer runs in checkDrift.

src/main/kotlin/dev/lain/claudejb/model/mcp/ToolResult.kt
src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/ReadWindow.kt
src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/ReadTools.kt

 - F_0014. read_file stops adding lines at a 15 000-character budget shared by the files of one call and reports to and truncated. fit cuts at the room when the newline cut would discard most of the text.

src/main/kotlin/dev/lain/claudejb/model/mcp/MetaTools.kt
src/main/kotlin/dev/lain/claudejb/model/mcp/Batch.kt

 - F_0015. An undeclared argument is refused after the guard has spoken, naming the valid ones. A call naming two lists keeps one card: the host has no tool specs, and such a call is now refused whole.

src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/FileGlob.kt
src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/SearchTools.kt

 - F_0016. A glob containing / matches the project-relative path.
 - F_0015. search_text, find_files and list_directory collect max + 1 and report truncated only when something was left out.

src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/HierarchyTools.kt
src/main/kotlin/dev/lain/claudejb/controller/mcp/tools/code/UastTools.kt

 - F_0017. A caller is the nearest UMethod, UField or UClass where the Java plugin is enabled, else the nearest named PSI owner. The UAST code stays in UastTools.kt, the only file allowed to name org.jetbrains.uast.

src/main/kotlin/dev/lain/claudejb/model/settings/McpConfigShape.kt
src/main/kotlin/dev/lain/claudejb/model/settings/LaunchDefaults.kt
src/main/kotlin/dev/lain/claudejb/view/settings/sections/SettingsMcpSection.kt

 - F_0002. Claude's mcpServers wrapper is removed when the form stores the text, never at launch: the execution-trust check reads the stored shape, so a launch-only unwrap would have run a pasted stdio server without the trust prompt. Other shapes are refused naming the entry. isValidMcpConfig is deleted; the hint shows stdio and http examples.

src/main/ts/jcef/views/permissions/base.ts
src/main/ts/jcef/views/permissions/cards/tool.ts
src/main/ts/jcef/views/permissions/cards/elicit.ts
src/main/ts/jcef/views/permissions/cards/question.ts
src/main/ts/jcef/types/permissions.d.ts

 - F_0020. PM.answer builds the buttons that answer a card and disables them all on the first click. Submit is enabled only when every question has a non-empty answer.

src/main/ts/jcef/controllers/composer/fold.ts
src/main/ts/jcef/controllers/permissions/permissions.ts
src/main/resources/jcef/css/views/composer/fold.css
src/main/ts/jcef/types/core.d.ts

 - F_0018. While folded, a key unfolds only from body or the transcript, and plain ArrowUp no longer does. The phase label follows the cards, focus is rescued from anywhere in the dock, and the dead palette selector is removed.

src/main/ts/jcef/controllers/composer/history.ts
src/main/ts/jcef/models/composer/history.ts
src/main/ts/jcef/controllers/git/chat.ts

 - F_0019. The history index is trusted only while its entry is still the text in the box. Prompts come from the transcript that is shown, a recall starts only at offset 0 or at the end, and the ghost is repainted.

src/test/kotlin/dev/lain/claudejb/controller/bridge/BridgeSettingsContractTest.kt
src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/FileGlobTest.kt
src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/code/ReadWindowTest.kt
src/test/kotlin/dev/lain/claudejb/controller/mcp/tools/run/TestRunChoiceTest.kt
src/test/kotlin/dev/lain/claudejb/controller/process/StdoutLinesTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/AdoptSettingsContractTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/ProcessSlotTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/SessionDisposalContractTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/SessionSendContractTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/StaleResumeTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/auth/EdtAuthContractTest.kt
src/test/kotlin/dev/lain/claudejb/controller/session/events/EarlyLinesTest.kt
src/test/kotlin/dev/lain/claudejb/headless/HierarchyCallerHeadlessTest.kt
src/test/kotlin/dev/lain/claudejb/headless/SessionLiveSettingsHeadlessTest.kt
src/test/kotlin/dev/lain/claudejb/headless/SettingsMcpSectionHeadlessTest.kt
src/test/kotlin/dev/lain/claudejb/model/diff/DiffPresenterIsWithinRootTest.kt
src/test/kotlin/dev/lain/claudejb/model/mcp/BatchTest.kt
src/test/kotlin/dev/lain/claudejb/model/mcp/ToolModelTest.kt
src/test/kotlin/dev/lain/claudejb/model/protocol/parse/ControlParsersTest.kt
src/test/kotlin/dev/lain/claudejb/model/settings/McpConfigShapeTest.kt
src/test/frontend/composer/composer-fold.test.js
src/test/frontend/composer/prompt-history.test.js
src/test/frontend/permissions/answer-once.test.js
src/test/frontend/permissions/permissions.test.js
src/test/frontend/permissions/question-card.test.js

This branch has not been deployed

No deployments
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