fix(cli): route diagnostic logs to stderr when stdout is piped#51641
fix(cli): route diagnostic logs to stderr when stdout is piped#51641shivama205 wants to merge 3 commits intoopenclaw:mainfrom
Conversation
When stdout is not a TTY (e.g. captured via $(...) or piped), plugin init logs ([plugins] lines) contaminate stdout, breaking scripts that parse command output. Route all diagnostic logs to stderr in non-TTY mode using the existing routeLogsToStderr() mechanism. Also fixes pre-existing format issue in daemon-cli/lifecycle.test.ts. Closes openclaw#51496 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Greptile SummaryThis PR fixes plugin init logs (e.g. Key observations:
Confidence Score: 5/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/logging/console-capture.test.ts
Line: 136-147
Comment:
**Test call order inverted from production**
The test calls `routeLogsToStderr()` before `enableConsoleCapture()`, but the production code in `run-main.ts` does the opposite — `enableConsoleCapture()` first, then `routeLogsToStderr()`. This works because `loggingState.forceConsoleToStderr` is read lazily inside the `forward` wrapper at call time (not captured at setup time), so both orderings are functionally equivalent. However, aligning the test with the actual production call order would make it a better regression guard.
```suggestion
it("routes info-level console.log to process.stderr when enabled", () => {
setLoggerOverride({ level: "info", file: tempLogPath() });
enableConsoleCapture();
routeLogsToStderr();
const stderrWrite = vi.spyOn(process.stderr, "write").mockReturnValue(true);
const stdoutWrite = vi.spyOn(process.stdout, "write").mockReturnValue(true);
console.log("[plugins] test message");
expect(stderrWrite).toHaveBeenCalled();
const written = String(stderrWrite.mock.calls[0]?.[0] ?? "");
expect(written).toContain("test message");
expect(stdoutWrite).not.toHaveBeenCalled();
});
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "fix(cli): route diag..." |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 84975f00cc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
When stdout is not a TTY (piped/captured), subsystem diagnostic logs (e.g. [plugins] init lines) contaminate stdout, breaking scripts that parse command output. Fix by routing info/debug/trace-level subsystem logger output to stderr in writeConsoleLine() when !process.stdout.isTTY. This is scoped to the subsystem logger sink only — command data output via runtime.log()/console.log() is unaffected, so --json, --version, and all other CLI data output stays on stdout. Also changes version output in help.ts to use process.stdout.write() directly (it's data, not a diagnostic log). Closes openclaw#51496 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
|
Addressed both review comments in the latest push: @greptile-apps (test call order): Fixed — but went further than just swapping the call order. The original @chatgpt-codex-connector (version stdout concern): Great catch — this was a real bug. Revised approach: Instead of globally routing all |
|
To use Codex here, create a Codex account and connect to github. |
|
Excellent revision! The scoped approach is much cleaner: ✅ subsystem.ts:248-251 — TTY check is perfectly placed. Only routes info/debug/trace subsystem logs to stderr when piped, leaving ✅ help.ts:114-117 — Good catch on the version bug. The new tests properly verify both TTY states. This is a solid, surgical fix that solves the original issue without side effects. Ready to ship. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5664f0d2bf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } else if (!process.stdout.isTTY) { | ||
| // When stdout is piped/captured, route subsystem diagnostic logs to stderr | ||
| // so stdout stays clean for command data output (runtime.log / JSON results). | ||
| (sink.error ?? console.error)(sanitized); |
There was a problem hiding this comment.
Scope non-TTY stderr routing to machine-output commands
Routing every non-warn/error subsystem line through console.error whenever stdout is non-TTY changes the stream for normal operational logs (for example gateway startup info logs) in common daemon/container runs where stdout.isTTY is false. That makes healthy info traffic appear on stderr, which many log pipelines and supervisors classify as error output, creating false alerts and breaking stdout-only log consumers; this routing should be gated by an explicit “stdout reserved for command data” mode instead of all non-TTY executions.
Useful? React with 👍 / 👎.
Summary
[plugins]lines) are written to stdout instead of stderr, contaminating captured output in scripts that use$(openclaw message send ...)or pipe the CLI output.writeConsoleLine()in the subsystem logger routesinfo-level diagnostic messages throughconsole.log(stdout), even when stdout is piped/captured.writeConsoleLine(), route info/debug/trace-level subsystem diagnostic logs to stderr when!process.stdout.isTTY. This is scoped to the subsystem logger sink only — command data output viaruntime.log()/console.log()is unaffected.help.tsnow usesprocess.stdout.write()directly since it's data, not a diagnostic log.Closes #51496
Change type
Scope
How it works
There are two separate output paths in the CLI:
runtime.log()→console.log()→forward()wrapper → stdout (unchanged)createSubsystemLogger()→writeConsoleLine()→rawConsole.log→ stdout (this is the bug)The fix adds a TTY check in
writeConsoleLine()(src/logging/subsystem.ts):This only affects subsystem logger output.
--version,--json, and allruntime.log()command output stays on stdout.Test plan
process.stdout.write()instead ofconsole.logsubsystem.test.tstests pass (13 total)console-capture.test.tstests pass (11 total)run-main.test.tstests pass (15 total)help.test.tstests pass (4 total)pnpm tsgopassesNote
This PR was AI-assisted using Claude (Opus 4.6) via Claude Code. All code has been reviewed and tested locally by a human.
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.6 (1M context) [email protected]