Skip to content

fix: compact/goal lock-wait unit bug + backend stdin error handling#4

Merged
william0wang merged 2 commits into
mainfrom
fix/compact-lock-units
Jul 5, 2026
Merged

fix: compact/goal lock-wait unit bug + backend stdin error handling#4
william0wang merged 2 commits into
mainfrom
fix/compact-lock-units

Conversation

@william0wang

Copy link
Copy Markdown
Owner

Summary

Two independent bug fixes ported from the Python reference bridge, surfaced during a /compact investigation where the command returned a false 0.5–3s before the backend actually finished compacting, causing the next user prompt to fail with a prompt is already running.

1. compact/goal lock wait used seconds where milliseconds were required

waitForTurnIdle compares elapsed time against Date.now() (milliseconds), but the timeout was passed as 300 / 60 — copied verbatim from Python's timeout=300 / timeout=60 (which are seconds). The probe loop therefore expired after the first 2s sleep, long before the internal AI turn's prompt lock was released.

  • extensions.ts: compact 300 → 300_000, goal 60 → 60_000
  • extensions.ts: compact() now returns __lockTimeout: !released so the slash path can warn
  • slash.ts: /compact emits ⚠ compact timed out (300s)… instead of a false ✓ compacted when the lock never releases (mirrors Python's ⚠ 压缩超时 branch)
  • extensions.ts: enriched the [probe] diagnostic logs with probe index + elapsed seconds (this was the evidence that revealed the unit bug: timeout=0s)

2. Backend stdin write errors + preempt lock rejection

  • client.ts: Node stream write errors (EPIPE on a closed stdin) are emitted as async 'error' events, not thrown synchronously — the existing try/catch around sendReply/sendError never caught them, and an unhandled 'error' event would crash the bridge. Install a stdin 'error' listener in the constructor (marks reader dead) and drop the redundant try/catch.
  • session.ts: withPreemptLock's next.finally(...) returns a new promise that rejects if the section body rejected — without a .catch, Node raised UnhandledPromiseRejection and crashed. Added a swallowing .catch.

Verification

  • pnpm build — clean
  • pnpm test — 86/86 passing
  • prettier applied to the 4 changed files only

Audit note

A full sweep of every setTimeout / Date.now() / request(timeoutMs) site in src/ was cross-checked against the Python reference. No other unit-mismatch bugs found — all other request() timeouts were already correct (15000timeout=15, 5000timeout=5, etc.).

…preempt lock rejection

Node stream write errors (EPIPE on a closed stdin) are emitted as async
'error' events, not thrown synchronously, so the try/catch around
sendReply/sendError never caught them — an unhandled 'error' event would
crash the bridge. Install a stdin 'error' listener in the constructor
that marks the reader dead, and drop the now-redundant try/catch.
Also guard withPreemptLock's finally chain with a .catch so a rejecting
section body doesn't raise an UnhandledPromiseRejection.
…ired

waitForTurnIdle compares against Date.now() (ms) but the timeout was
passed as 300 (from Python's seconds), so the probe loop exited after
the first 2s sleep — long before the internal turn's prompt lock was
released, and the next prompt hit 'a prompt is already running'.
Compact now waits 300_000ms (5min) and goal 60_000ms; /compact also
surfaces a 'timed out' warning instead of a false checkmark.
@william0wang william0wang merged commit 44d685e into main Jul 5, 2026
1 check passed
@william0wang william0wang deleted the fix/compact-lock-units branch July 5, 2026 12:22

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request improves robustness and error handling across several components. Key changes include adding an async error listener to the backend's stdin stream, correcting timeout values from seconds to milliseconds in waitForTurnIdle, handling lock timeouts for the compact command, and catching unhandled promise rejections in withPreemptLock. The feedback highlights that stdin.write can still throw synchronous exceptions (e.g., ERR_STREAM_DESTROYED) if the stream is closed, and recommends retaining the try/catch blocks in sendReply and sendError to prevent potential process crashes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/backend/client.ts
Comment on lines +227 to +230
// Write errors (EPIPE) are delivered via the stdin 'error' listener
// installed in the constructor (synchronous try/catch cannot catch them);
// no try/catch needed here.
stdin.write(JSON.stringify({ id, result }) + "\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

While it is true that asynchronous write errors (like EPIPE) are emitted as 'error' events on the stream and handled by the new listener in the constructor, stdin.write() can still throw synchronous exceptions (such as ERR_STREAM_DESTROYED or ERR_STREAM_WRITE_AFTER_END) if the stream has already been closed, ended, or destroyed but stdin.destroyed is not yet updated. Without a try/catch block, these synchronous exceptions will go uncaught and crash the entire Node.js process. Keeping the defensive try/catch block around stdin.write() is highly recommended for robustness.

Suggested change
// Write errors (EPIPE) are delivered via the stdin 'error' listener
// installed in the constructor (synchronous try/catch cannot catch them);
// no try/catch needed here.
stdin.write(JSON.stringify({ id, result }) + "\n");
try {
stdin.write(JSON.stringify({ id, result }) + "\n");
} catch (e) {
this.readerDead = true;
log(
"backend: sendReply write failed (marked dead): " + (e instanceof Error ? e.message : String(e))
);
}

Comment thread src/backend/client.ts
`backend: sendError write failed (marked dead): ${e instanceof Error ? e.message : String(e)}`,
);
}
stdin.write(JSON.stringify({ id, error: { code, message } }) + "\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similar to sendReply, stdin.write() can throw synchronous exceptions (such as ERR_STREAM_DESTROYED or ERR_STREAM_WRITE_AFTER_END) if the stream has already been closed, ended, or destroyed but stdin.destroyed is not yet updated. Without a try/catch block, these synchronous exceptions will go uncaught and crash the entire Node.js process. Keeping the defensive try/catch block around stdin.write() is highly recommended for robustness.

Suggested change
stdin.write(JSON.stringify({ id, error: { code, message } }) + "\n");
try {
stdin.write(JSON.stringify({ id, error: { code, message } }) + "\n");
} catch (e) {
this.readerDead = true;
log(
"backend: sendError write failed (marked dead): " + (e instanceof Error ? e.message : String(e))
);
}

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