Skip to content

fix: skip stdin read when positional message provided#935

Open
sahrizvi wants to merge 7 commits into
mainfrom
fix/run-stdin-wedge-934
Open

fix: skip stdin read when positional message provided#935
sahrizvi wants to merge 7 commits into
mainfrom
fix/run-stdin-wedge-934

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

PINEAPPLE

Closes #934

Summary

altimate-code run "<task>" hangs at 0% CPU forever when invoked as a subprocess from a context that inherits stdin without closing it (Claude Code's Bash tool, Python subprocess.run, CI, plugin hosts that don't pin stdin to /dev/null). The root cause is a blind Bun.stdin.text() on a non-TTY stdin that waits forever for an EOF the parent never sends.

This PR replaces the unbounded read with a deliberate two-gate strategy in a new util helper (packages/opencode/src/util/stdin.ts):

  1. fstat gate — only FIFOs (pipes), regular files (redirects), and sockets carry real input. TTYs and character devices (< /dev/null) skip immediately. The isSocket() branch is load-bearing: when spawned with stdio: ["pipe","pipe","pipe"] on Bun on macOS, the inherited stdin actually reports as a socket, not a FIFO (verified empirically during review).
  2. First-byte timeout — instead of bounding the whole-stream drain, wait up to 500ms for the first readable byte. If no byte arrives in that window, treat stdin as inherited-idle and skip. If a byte arrives, drain to EOF without further deadline — so a slow producer that takes longer than 500ms total but flushes its first chunk within the window is not truncated.

Default reader uses process.stdin events (not Bun.stdin.text()) with explicit listener cleanup + unref() on the no-data path so an idle inherited fd doesn't pin the event loop.

Behavior at the run call site

run.ts:434-436 now reads:

message = assembleStdinMessage(message, await readStdinIfAvailable())

assembleStdinMessage is an exported pure function — positional + "\n" + stdin when both are non-empty; either alone when only one is present. Conventional CLI semantics: positional and stdin are concatenated, not mutually exclusive. So echo "context" | altimate-code run "summarize:" delivers "summarize:\ncontext", matching the git diff | tool "review this" pattern that the original PR #935 reviewer (anandgupta42) asked us to preserve.

Tunables

  • ALTIMATE_STDIN_TIMEOUT_MS=N — env override for the first-byte timeout (ms). Set higher for environments with slow-flush producers (DB queries that need to plan, decompression, network calls with DNS+TLS handshake).
  • Stderr note — if the fstat gate passed (fd 0 looked like real input) but the first-byte window timed out, the helper writes one line to stderr so a user whose pipe was silently dropped finds out instead of seeing nothing. The note also points at the env var.

Downstream context

Downstream consumers have been working around the wedge by spawning altimate-code with stdio: ["ignore", "pipe", "pipe"] — see altimate-opencode-plugin plugins/altimate-code/index.ts:28. That workaround is fine for the plugin but doesn't protect any other caller (Claude Code's Bash tool, ad-hoc CI scripts, future plugins that forget). This PR is the proper fix at the source.

Test plan

  • Unit (injection): 20 cases in test/util/stdin.test.ts — TTY skip, < /dev/null skip, EBADF, FIFO + data, file redirect, socket accepted, idle timeout, slow-but-pre-timeout, empty FIFO, whitespace-only, undefined process.stdin, warn fires when input was expected but empty, warn does NOT fire for TTY / char-device / data-delivered, env override + invalid-env fallback. Plus 5 cases for assembleStdinMessage covering the PR fix: skip stdin read when positional message provided #935 concatenation regression.
  • E2E (spawned subprocess): 4 cases in test/util/stdin-e2e.test.ts — inherited-idle pipe exits in <1500ms; echo-style data is delivered; slow producer (~300ms first byte) is preserved; post-timeout writes (1200ms) are discarded. These exercise the actual process.stdin event path against real fd 0.
  • Broader: 840 tests across test/util/ + test/cli/ pass; bun run typecheck clean.

Notes for reviewers

Summary by CodeRabbit

Bug Fixes

  • Fixed a hang in the run command when stdin was inherited but idle.
  • Improved safe handling of redirected/piped stdin so the command only consumes input when appropriate.

New Features

  • Added robust stdin ingestion with a configurable first-byte timeout.
  • Introduced ALTIMATE_STDIN_TIMEOUT_MS to tune how long to wait for stdin data.
  • Better support for different stdin source types (including pipes, files, and sockets).

Tests

  • Added unit and end-to-end coverage for stdin timing, empty-input behavior, and warning output.

`!process.stdin.isTTY` alone is too broad — it matches not just
"someone piped input in" but also "the parent process inherited
stdin from somewhere upstream and never closed its end." In the
second case, `Bun.stdin.text()` waits forever for an EOF that
never arrives. The process sits at 0% CPU with no session
created, no log activity, no error — a silent hang.

This surfaces in every subprocess invocation pattern that passes
a positional message and inherits stdin: Claude Code's Bash tool,
Python `subprocess.run(..., stdin=None)`, CI runners, plugin
hosts that don't pin stdin to `/dev/null`. Downstream callers
have been working around it with `stdio: "ignore"` on spawn
(see e.g. altimate-opencode-plugin
`plugins/altimate-code/index.ts:28`), but anything that forgets
to do that still hits the wedge.

Fix: only read stdin when no positional `message` was provided.
Matches conventional CLI semantics (positional overrides stdin)
and unblocks every subprocess caller. Pipe-only invocations
without a positional arg continue to work unchanged.

Closes #934
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A new stdin utility module replaces unconditional Bun.stdin.text() reads in the run command with a configurable first-byte-timeout approach. The utilities detect TTY, filter file descriptor types via fstat, and return empty if no data arrives within a timeout window; otherwise drain and return full stdin. The run command now delegates to these utilities, eliminating hangs on inherited but unclosed stdin. Comprehensive unit and end-to-end tests verify timeout behavior and regression-proof the fix.

Changes

Safe stdin read with first-byte timeout

Layer / File(s) Summary
Stdin utility contracts and configuration
packages/opencode/src/util/stdin.ts
Introduces ReadStdinDeps dependency-injection interface with optional isTTY, fstat, readStdin, timeoutMs, and warn callback fields. Defines timeout resolution from ALTIMATE_STDIN_TIMEOUT_MS environment variable with 500ms fallback. Documents the two-gate strategy (fstat source filtering and first-byte timeout) and warning behavior when stdin appears available but no data arrives.
Stdin reading with first-byte timeout
packages/opencode/src/util/stdin.ts
Implements readStdinIfAvailable(), which returns empty for TTY, uses fstat to filter stdin file descriptor type (allowing only FIFO/file/socket), then calls readStdin with configurable timeout. Implements defaultReadStdin(timeoutMs) using process.stdin event listeners with first-byte timer: timeout resolves empty; first byte arrival triggers drain to EOF and returns full UTF-8 content. Cleanup removes listeners, clears timers, and unrefs stream.
Message assembly utility
packages/opencode/src/util/stdin.ts
Implements assembleStdinMessage(positional, stdinInput) to merge arguments: returns positional if stdin is empty/whitespace, returns stdin if positional is empty, concatenates both with newline separator if both are non-empty.
Unit test coverage for stdin utilities
packages/opencode/test/util/stdin.test.ts
Comprehensive test suite for readStdinIfAvailable covering TTY short-circuit, file descriptor type filtering (rejecting character devices, accepting FIFO/file/socket), fstat error handling, first-byte timeout behavior (empty on timeout, full content on arrival, non-truncation for slow producers), whitespace preservation, undefined process.stdin handling, and warning invocation. Separate suite for assembleStdinMessage validates joining, precedence rules, and edge cases.
End-to-end test fixture and regression suite
packages/opencode/test/util/stdin-fixture.ts, packages/opencode/test/util/stdin-e2e.test.ts
Subprocess fixture that calls real readStdinIfAvailable() on fd 0, measures elapsed time, and emits JSON output. End-to-end regression tests verify: inherited-but-idle stdin exits promptly without hang, piped input is delivered correctly, slow producers near timeout boundary are fully captured, and producers past cutoff receive empty result. Includes runFixture helper for subprocess spawning and JSON parsing.
Run command stdin integration
packages/opencode/src/cli/cmd/run.ts
Delegates stdin handling to new utilities via assembleStdinMessage(message, await readStdinIfAvailable()), replacing previous unconditional Bun.stdin.text() read on non-TTY. Inline comments document inherited-but-idle hang scenario, first-byte-race fix, and null-safe behavior.

Sequence Diagram

sequenceDiagram
  participant Client as Client/parent process
  participant RunCmd as run command
  participant ReadStdin as readStdinIfAvailable
  participant DefaultRead as defaultReadStdin
  participant ProcessStdin as process.stdin
  
  Client->>RunCmd: invoke with inherited stdin
  RunCmd->>ReadStdin: await readStdinIfAvailable()
  ReadStdin->>ProcessStdin: check isTTY
  alt stdin is TTY
    ReadStdin-->>RunCmd: return ''
  else stdin is non-TTY
    ReadStdin->>ProcessStdin: fstat fd 0
    alt fd not FIFO/file/socket
      ReadStdin-->>RunCmd: return ''
    else fd is FIFO/file/socket
      ReadStdin->>DefaultRead: call with timeoutMs
      DefaultRead->>ProcessStdin: attach data/end/error listeners
      DefaultRead->>ProcessStdin: start first-byte timer
      alt first byte arrives within timeout
        ProcessStdin->>DefaultRead: data event
        DefaultRead->>DefaultRead: clear timer, collect chunks
        ProcessStdin->>DefaultRead: end event
        DefaultRead->>ProcessStdin: cleanup listeners/pause/unref
        DefaultRead-->>ReadStdin: return drained UTF-8
      else timeout fires before first byte
        DefaultRead->>DefaultRead: clear timer
        DefaultRead->>ProcessStdin: cleanup listeners
        DefaultRead-->>ReadStdin: return ''
      end
      ReadStdin-->>RunCmd: return content or ''
    end
  end
  RunCmd->>RunCmd: assembleStdinMessage(message, stdin)
  RunCmd->>RunCmd: execute command
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • AltimateAI/altimate-code#937: Also addresses stdin hang prevention in the run command, though this PR introduces a comprehensive first-byte-timeout utility with dependency injection and full test coverage.

Suggested reviewers

  • suryaiyer95

Poem

🐰 The rabbit built walls around stdin's dark door,
A timeout to catch what comes knocking before.
No more eternal waits in the event loop's embrace—
The inherited FD meets its first-byte grace! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: replacing unconditional stdin reads with a conditional approach that skips stdin when a positional message is provided.
Description check ✅ Passed The PR description is comprehensive and includes all required sections: Summary, Test Plan, and Checklist with all items marked complete. It addresses the core issue, explains the two-gate solution, documents behavior, tunables, and test coverage.
Linked Issues check ✅ Passed The PR fully addresses issue #934 by implementing a two-gate stdin strategy (fstat + first-byte timeout) that prevents hangs on inherited stdin across all subprocess contexts while preserving pipe-only invocations and concatenating positional + piped input per conventional CLI semantics.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the #934 stdin hang: new stdin utility module with fstat/timeout gates, refactored run.ts to use the helper, and comprehensive unit + E2E tests validating the fix across idle/slow-producer/timeout scenarios. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/run-stdin-wedge-934

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

Re-trigger cubic

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

Copy link
Copy Markdown
Contributor Author

Centralized-test bot failures — likely shared infra, not PR-caused

The dev-punia-altimate bot reported 15 TypeScript failures (connection_refused / timeout / parse_error / network_error / auth_failure / rate_limit / internal_error / empty_error / oom / permission_denied / ...). The identical 15-failure set appears on PRs #933 and #937, which touch unrelated code (packages/dbt-tools and packages/opencode/src/tool/question.ts respectively). This PR only modifies packages/opencode/src/cli/cmd/run.ts.

Strong signal these are shared fault-injection-harness failures, not regressions introduced by this PR. Flagging here so the pattern is on record across the three PRs; happy to dig into the harness config if the bot owner wants.

suryaiyer95
suryaiyer95 previously approved these changes Jun 13, 2026
@anandgupta42

Copy link
Copy Markdown
Contributor

Code Review — /code-review (Claude)

Verdict: Request changes (one behavior question to confirm). The diagnosis is correct and the fix removes the hang for the reported caller class (subprocess/CI/agent passing a positional message with an inherited-but-open stdin). Two things to resolve before merge.

MAJOR — positional message now silently drops piped stdin (Behavior change) — run.ts:433

Previously !isTTY meant positional message and piped stdin were concatenated:

echo "context data" | altimate-code run "summarize:"   # old: "summarize:\n\ncontext data"

After this change, any non-empty message (positional arg, or --query/--file extracted parts at :430) suppresses the stdin read, so context data is dropped silently. cmd "prompt" | … and cmd "prompt" < file combining is a common, conventional pattern (e.g. git diff | tool "review this"), so this is a potential silent regression, not just a semantics tweak.

Please confirm that's intended. If combining should still work, the disambiguation shouldn't key on "is there a positional arg" — consider:

  • Explicit opt-in: only read stdin when a - positional / --stdin flag is passed (unambiguous, no hang, no silent drop), or
  • Detect real input: fs.fstatSync(0)isFIFO() || isFile() distinguishes a genuine pipe/redirect from an inherited idle stream in the common cases (caveat: an inherited open pipe still looks like a FIFO, so it's an improvement, not a complete fix).

MINOR — the underlying hang still exists without a positional message — run.ts:433

The root cause is Bun.stdin.text() blocking forever on an inherited-but-never-closed stdin. A non-TTY caller that passes no positional message still hits the original infinite 0% CPU hang. That's outside the PR's stated scope, but the chosen guard papers over the common trigger rather than fixing the wedge. A bounded read (race Bun.stdin.text() against a short timer, or the fstat gate above) would close it for all callers.

Positives

  • Accurate root-cause writeup in the comment — the inherited-open-stdin / missing-EOF failure mode is exactly right and non-obvious.
  • Minimal, low-risk for the targeted callers; message.trim() correctly treats whitespace-only as empty.

Missing tests

This is very testable and currently uncovered. Add a regression test that spawns run "msg" with a non-TTY, open-but-idle stdin and asserts it returns (doesn't hang) within a timeout; plus one asserting a genuine echo data | run … still consumes stdin under whatever final semantics you choose.

Previous fix (a387257) replaced the unbounded `Bun.stdin.text()` call
with a `Promise.race` against a 100ms timer. PR #935 reviewer flagged a
silent stdin-drop regression; a consensus self-review found two more
regressions introduced by the race: the orphaned read kept fd 0 open
(`~1s` exit delay verified on Bun 1.3.11), and the whole-stream timeout
silently truncated slow-but-legitimate piped producers.

This commit replaces the race with a first-byte gate over `process.stdin`
events. The timeout now caps "time until first readable byte," not full
drain — so producers that flush within the window are not truncated, and
the no-data path explicitly removes listeners and `unref`s the stream so
an idle inherited fd no longer pins process exit.

Other changes:
- Accept socket-backed stdin (`isSocket()`) — was a silent narrowing.
- Use `Pick<fs.Stats, ...>` instead of a hand-rolled `Stat` type.
- Move helper from `src/cli/cmd/run-stdin.ts` to `src/util/stdin.ts`.
- Extract `assembleStdinMessage` as a pure function so the
  positional + stdin concatenation case from PR #935 is unit-testable.
- 4 spawn-based E2E tests for the actual fd-0 wedge / pipe paths.
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@sahrizvi

Copy link
Copy Markdown
Contributor Author

Follow-up: replaced timeout race with first-byte gate

Pushed cc222db2a addressing the reviewer feedback and the regressions a multi-model self-review surfaced on the previous fix (a387257789).

Reviewer feedback (anandgupta42)

  • MAJOR — silent stdin drop when positional presentfixed. assembleStdinMessage(positional, stdinInput) now concatenates with \n when both are non-empty; positional no longer suppresses stdin. Unit tests assembleStdinMessage > concatenates positional + stdin with newline and the spawn-based returns piped data when producer writes and closes cover it.
  • MINOR — residual wedge when no positional is passedfixed. Helper now applies to every code path uniformly (no positional/empty/large message all flow through readStdinIfAvailable). Spawn-based exits promptly with empty result when stdin is an inherited-but-idle pipe is the regression test.
  • Missing testsfixed. Added test/util/stdin.test.ts (15 injection-level cases) and test/util/stdin-e2e.test.ts (4 real-fd-0 spawn cases).

Self-review issues (from a multi-model consensus review of the previous patch)

# Issue Status
M1 Orphaned Bun.stdin.text() kept fd 0 open after timeout (~1s exit delay on Bun 1.3.11) Fixed — default reader is now process.stdin event-based with explicit listener cleanup + unref() on the no-data path.
M2 100ms whole-stream timeout silently truncated slow producers Fixed — timeout now caps "time until first readable byte." Past that point we drain to EOF without a deadline, so slow/large producers aren't truncated.
M3 No end-to-end spawned-subprocess test Fixed — added stdin-e2e.test.ts with 4 spawn-based cases against real fd 0.
m1 fstat gate skipped sockets FixedisSocket() accepted; relevant to process supervisors / socket activation / nc -l | run.
m2 Hand-rolled Stat type FixedPick<fs.Stats, "isFIFO" | "isFile" | "isSocket">.
m4 Missing injection-level edge cases Fixed — empty FIFO, whitespace-only, slow-but-pre-timeout, socket-accepted, fstat-throws all covered.
n1 STDIN_READ_TIMEOUT_MS exported but unused Fixed — constant is module-private.
n2 Helper location too narrow Fixed — moved from src/cli/cmd/run-stdin.ts to src/util/stdin.ts.
n3 Comments overstated "buffered by spawn" guarantee Fixed — comments rewritten to match the actual semantics (first-byte gate, not whole-stream).

Out of scope (intentionally not fixed in this PR)

  • tui/thread.ts:58-63 has the same vulnerable pattern. Pre-existing on main; not touched by this PR's diff. Filing as a follow-up since the fix (apply readStdinIfAvailable there too) is a separate, mechanical change.
  • Bun.stdin.text() runtime portability. Pre-existing. Incidentally moot now that the default reader uses process.stdin events instead of Bun.stdin.text().

Verification

  • 19/19 new tests pass (15 unit + 4 E2E spawn)
  • 829/829 in test/util/ + test/cli/ pass (no regressions)
  • bun run typecheck clean

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
packages/opencode/test/util/stdin-e2e.test.ts (1)

21-30: ⚡ Quick win

Start the subprocess kill timer before awaiting writeStdin.

killAfterMs is armed only after await opts.writeStdin(...) (Line 21), so a stuck writer can hang the helper without the intended timeout protection.

Suggested patch
 async function runFixture(opts: {
   writeStdin?: (sink: FileSink) => Promise<void>
   killAfterMs?: number
 }): Promise<{ code: number | null; result?: string; elapsed?: number; stdout: string; stderr: string }> {
   const proc = Bun.spawn(["bun", "run", FIXTURE], {
     stdin: "pipe",
     stdout: "pipe",
     stderr: "pipe",
   })

+  let killTimer: ReturnType<typeof setTimeout> | undefined
+  if (opts.killAfterMs) {
+    killTimer = setTimeout(() => proc.kill(), opts.killAfterMs)
+  }
+
   if (opts.writeStdin) {
     await opts.writeStdin(proc.stdin as unknown as FileSink)
   }
 
-  let killTimer: ReturnType<typeof setTimeout> | undefined
-  if (opts.killAfterMs) {
-    killTimer = setTimeout(() => proc.kill(), opts.killAfterMs)
-  }
   const code = await proc.exited
   if (killTimer) clearTimeout(killTimer)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/test/util/stdin-e2e.test.ts` around lines 21 - 30, The kill
timer is being initialized after awaiting opts.writeStdin(), which means a stuck
writer can hang the helper without timeout protection. Move the killTimer
initialization block (the if statement checking opts.killAfterMs and calling
setTimeout to kill the process) to execute before the await opts.writeStdin()
call. This ensures the timeout protection is active from the start, preventing
the writer from hanging the test indefinitely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/opencode/test/util/stdin-e2e.test.ts`:
- Around line 21-30: The kill timer is being initialized after awaiting
opts.writeStdin(), which means a stuck writer can hang the helper without
timeout protection. Move the killTimer initialization block (the if statement
checking opts.killAfterMs and calling setTimeout to kill the process) to execute
before the await opts.writeStdin() call. This ensures the timeout protection is
active from the start, preventing the writer from hanging the test indefinitely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 36864fcc-6d4e-4d5e-a228-d5a03c1ea911

📥 Commits

Reviewing files that changed from the base of the PR and between a387257 and cc222db.

📒 Files selected for processing (5)
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/util/stdin.ts
  • packages/opencode/test/util/stdin-e2e.test.ts
  • packages/opencode/test/util/stdin-fixture.ts
  • packages/opencode/test/util/stdin.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/opencode/src/cli/cmd/run.ts

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/util/stdin.ts Outdated
@suryaiyer95

Copy link
Copy Markdown
Contributor

Review — concerns (CI green; the shipped helper is a solid, well-tested fix; flagging these before merge):

1. PR description is stale. The body says "Only read stdin when no positional message was provided … positional argument overrides stdin." The shipped assembleStdinMessage does the opposite — it concatenates positional + "\n" + stdin, and readStdinIfAvailable() is called unconditionally. The new behavior is better, but the description + test plan describe a design that wasn't shipped. Please update the body to match (the cubic summary is accurate).

2. The 100ms first-byte timeout silently drops legitimately-slow input. slowcmd | altimate-code run "prompt" where slowcmd takes >100ms to emit its first byte → stdin is treated as idle → input dropped, no warning (locked in by the "returns empty when first byte arrives after the first-byte timeout" test). Correct shape of fix, but 100ms is aggressive for real pipelines (DB query, decompression, network producers). Consider a larger default (500ms–1s) and/or an env override, plus a stderr note when a non-TTY pipe yields nothing — silent data loss is worse than the original hang for someone genuinely piping slow input.

3. readStdinIfAvailable has the exact NPE that #937 fixes. const isTTY = deps.isTTY ?? Boolean(process.stdin.isTTY) is un-guarded and runs before the try/catch — if process.stdin is undefined (embedded/child runtime, per dev-punia's review on #937) this throws TypeError. The helper should use process.stdin?.isTTY and treat undefined as "no stdin → ''".

4. Minor: a producer that emits one byte then never closes re-introduces a teardown wait (onEnd never fires). Acknowledged in the comment; rare, not a blocker.

Cross-PR (important): this and #937 edit the same run.ts line (#937 changes the inline guard; #935 replaces it with assembleStdinMessage(...)), so they'll conflict on merge. #935 is the better base — #937 should rebase onto it, fold its process.stdin null-safety into readStdinIfAvailable (addresses #3 above), and keep only its ALTIMATE_NON_INTERACTIVE block. Merging #937 second without this would clobber the helper.

sahrizvi pushed a commit that referenced this pull request Jun 18, 2026
Previous fix (a387257) replaced the unbounded `Bun.stdin.text()` call
with a `Promise.race` against a 100ms timer. PR #935 reviewer flagged a
silent stdin-drop regression; a consensus self-review found two more
regressions introduced by the race: the orphaned read kept fd 0 open
(`~1s` exit delay verified on Bun 1.3.11), and the whole-stream timeout
silently truncated slow-but-legitimate piped producers.

This commit replaces the race with a first-byte gate over `process.stdin`
events. The timeout now caps "time until first readable byte," not full
drain — so producers that flush within the window are not truncated, and
the no-data path explicitly removes listeners and `unref`s the stream so
an idle inherited fd no longer pins process exit.

Other changes:
- Accept socket-backed stdin (`isSocket()`) — was a silent narrowing.
- Use `Pick<fs.Stats, ...>` instead of a hand-rolled `Stat` type.
- Move helper from `src/cli/cmd/run-stdin.ts` to `src/util/stdin.ts`.
- Extract `assembleStdinMessage` as a pure function so the
  positional + stdin concatenation case from PR #935 is unit-testable.
- 4 spawn-based E2E tests for the actual fd-0 wedge / pipe paths.
@sahrizvi sahrizvi reopened this Jun 18, 2026
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

Resolved conflict on packages/opencode/src/cli/cmd/run.ts by keeping
the assembleStdinMessage call. PR #937's null-safety against undefined
process.stdin is folded into readStdinIfAvailable in a follow-up commit
on top of this merge.
Addresses suryaiyer95's review of `cc222db2a`:

- Default first-byte timeout bumped from 100ms to 500ms. The previous
  value was aggressive against realistic slow-to-first-byte producers
  (DB queries that need to plan, decompression headers, network calls
  with DNS+TLS handshake). 500ms is generous for those without sacri-
  ficing the inherited-idle wedge fix.

- New `ALTIMATE_STDIN_TIMEOUT_MS=N` env override so a user hitting an
  even slower pipeline can bump it without code changes.

- Stderr note when fd 0 looked like real input (FIFO / file / socket)
  but readStdin came back empty. Silent drop was the original UX
  failure that motivated this whole review thread. The note fires on
  the dominant inherited-idle path too; that's intentional since
  stderr is captured but not surfaced for most subprocess callers
  (Claude Code's Bash tool, CI), and a single line of noise per call
  is worth the explicitness for the slow-producer case.

- `process.stdin` null-safety folded into `readStdinIfAvailable`: in
  embedded / child runtimes (dev-punia review on PR #937) accessing
  `process.stdin.isTTY` could throw. Treat undefined stdin as "no
  input to read".

- 6 new injection tests: env override + invalid env fallback, warning
  fires/doesn't fire across each gate, undefined-stdin guard.

- E2E thresholds adjusted for the 500ms timeout (idle-exit assertion
  bumped to 1500ms, post-timeout sleep bumped to 1200ms).
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

3 similar comments
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/opencode/src/util/stdin.ts Outdated
Comment thread packages/opencode/src/util/stdin.ts Outdated
Matches the literal form suryaiyer95 proposed in review #3 on PR #935:
`process.stdin?.isTTY` instead of an early-return + plain access.
Functionally equivalent — the fstat try/catch already returns "" when
fd 0 isn't open, so the optional chaining alone covers the embedded-
runtime case.
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

P2 (NPE guard incomplete): a caller that passes `deps.isTTY` bypasses
the outer `process.stdin?.isTTY` guard, and if `process.stdin` is
undefined the default reader crashes on `stdin.on(...)`. Add a defensive
check inside `defaultReadStdin` that returns "" when stdin is absent.
New regression test exercises the path with `deps.isTTY` set + undefined
`process.stdin` + no `readStdin` injection.

P3 (warning text misleading for non-timeout empty results): an empty
result can come from the timer firing, a clean `end` with zero bytes,
or an error event — the previous "no data received within Nms" wording
implied only the first. Reword to cause-neutral "stdin produced no
data" while keeping the env-var tip for slow-producer cases.
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

1 similar comment
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/opencode/src/util/stdin.ts Outdated
The cause-neutral wording from 8eac6fa still kept the
`set ALTIMATE_STDIN_TIMEOUT_MS=N` tip. cubic flagged that the tip
points users at timeout tuning even when the real cause is a stream
error or the dominant inherited-idle subprocess case. Drop the tip;
the env var stays documented in code/docs for the rare slow-producer
scenario.

Final wording: "altimate-code: stdin produced no data; proceeding
without it."
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/util/stdin.ts">

<violation number="1" location="packages/opencode/src/util/stdin.ts:107">
P2: Removal of the `ALTIMATE_STDIN_TIMEOUT_MS` configuration tip from the stdin warning reduces discoverability of the timeout tunable for slow-producer scenarios. The env var is only documented in code comments and tests, so the warning was the primary user-facing channel for this knob.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// tip; cubic-dev-ai flagged it as misleading for the error and the
// dominant inherited-idle subprocess cases. The env var stays
// documented in code/docs for the rare slow-producer scenario.
warn(`altimate-code: stdin produced no data; proceeding without it.`)

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: Removal of the ALTIMATE_STDIN_TIMEOUT_MS configuration tip from the stdin warning reduces discoverability of the timeout tunable for slow-producer scenarios. The env var is only documented in code comments and tests, so the warning was the primary user-facing channel for this knob.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/util/stdin.ts, line 107:

<comment>Removal of the `ALTIMATE_STDIN_TIMEOUT_MS` configuration tip from the stdin warning reduces discoverability of the timeout tunable for slow-producer scenarios. The env var is only documented in code comments and tests, so the warning was the primary user-facing channel for this knob.</comment>

<file context>
@@ -99,13 +99,12 @@ export async function readStdinIfAvailable(deps: ReadStdinDeps = {}): Promise<st
+    // tip; cubic-dev-ai flagged it as misleading for the error and the
+    // dominant inherited-idle subprocess cases. The env var stays
+    // documented in code/docs for the rare slow-producer scenario.
+    warn(`altimate-code: stdin produced no data; proceeding without it.`)
   }
 
</file context>

@dev-punia-altimate

Copy link
Copy Markdown
Contributor

❌ Tests — Failures Detected

TypeScript — 15 failure(s)

  • connection_refused [1.00ms]
  • timeout
  • permission_denied
  • parse_error
  • network_error [1.00ms]
  • auth_failure
  • rate_limit
  • internal_error
  • empty_error
  • connection_refused
  • timeout
  • permission_denied [1.00ms]
  • parse_error
  • network_error
  • auth_failure

Next Step

Please address the failing cases above and re-run verification.

cc @sahrizvi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

altimate-code run wedges silently on inherited stdin when invoked as subprocess

4 participants