Summary
Under an interactive PTY (terminal.open() → brush), compound commands produce no output and no error. The same constructs work through exec() and through sh -c inside the same PTY, so this is specific to brush's interactive path, not to pipelines.
Two symptoms, likely one root cause — both are writes from the interactive shell that never reach the terminal:
- Compound commands (
;, &&, |, ( ), backticks) print nothing. Redirecting stderr shows the real failure is WASI errno 21 (EFAULT) on a write.
- brush's own diagnostics never reach the PTY at all, so an unknown command silently returns to the prompt instead of printing
command not found.
Symptom 2 is what makes symptom 1 invisible: the shell is reporting an error, it just goes nowhere.
Reproduction
import { AgentOs } from "@rivet-dev/agentos-core";
import common from "@agentos-software/common";
const vm = await AgentOs.create({ software: [common] });
const shell = await vm.terminal.open({ cols: 80, rows: 24 });
const id = shell.shellId;
let buf = "";
vm.onShellData(id, (e) => { buf += Buffer.from(e?.data ?? e).toString("utf8"); });
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await sleep(1500);
for (const c of [
"echo one", // OK
"echo b; echo c", // FAIL - no output
"echo d && echo e", // FAIL
"echo a | cat", // FAIL
"(echo sub)", // FAIL
"echo `echo backtick`", // FAIL
"echo 'semi;colon'", // OK - quoted, so it is one simple command
"sh -c 'echo b; echo c'", // OK - same PTY, non-interactive brush
"nosuchcmd", // FAIL - no "command not found"
]) {
buf = "";
await vm.terminal.write(id, c + "\n");
await sleep(3000);
console.log(JSON.stringify(c), "=>", JSON.stringify(buf.replace(/\r/g, "")));
}
Observed:
| command |
interactive PTY |
exec() |
echo one |
one |
ok |
uname -a |
correct output |
ok |
echo 'semi;colon' |
semi;colon |
ok |
sh -c 'echo a | cat' |
a |
ok |
sh -c 'echo b; echo c' |
b c |
ok |
echo b; echo c |
nothing |
ok |
echo d && echo e |
nothing |
ok |
echo a | cat |
nothing |
ok |
(echo sub) |
nothing |
ok |
nosuchcmd |
nothing (no error) |
prints error |
Note echo here is a brush builtin, so the failing case involves no external command at all.
The hidden error
Redirecting stderr to a file makes both failures visible:
$ nosuchcmd 2>/tmp/e1
$ cat /tmp/e1
error: command not found: nosuchcmd
$ echo b; echo c 2>/tmp/e2
$ cat /tmp/e2
error: echo: i/o error: Bad address (os error 21)
Under wasm32-wasip1, errno 21 is EFAULT (WASI numbering, not Linux's EISDIR), so "Bad address" and the code agree. brush parsed and executed the list correctly — the write to stdout failed.
Ruled out
- Pipelines /
; themselves — exec("ls /opt/agentos/bin | head -5") returns correct output.
- The kernel PTY, fd wiring, job control —
sh -c 'echo a | cat' works inside the failing interactive shell.
- stderr routing generally — a child's stderr does reach the terminal (
sh -c 'echo to-stderr 1>&2' renders). Only the interactive shell's own stderr is lost.
TERM — identical failure with TERM=xterm-256color and TERM=dumb.
- The terminal client — reproduced entirely server-side via
vm.terminal.write(), no browser involved.
Where to look
_fdWrite in crates/execution/assets/runners/wasi-module.js:1404 returns EFAULT only via _writeUint32 (:413) — _mapFsError (:811) defaults to EIO, not EFAULT. So the failing path is the nwritten pointer write, or a synthetic-pipe branch, rather than a generic fs error.
crates/CLAUDE.md already documents this failure mode in the WASM host-process bridge:
child_process.poll returning ECHILD after an exit event's trailing-drain pass is terminal, not a new fault … post-exit drain loops must stop on ECHILD instead of converting a successful pipeline into WASI_ERRNO_FAULT.
Debug logging: AGENTOS_WASM_WASI_DEBUG=1 enables [agentos-wasi] traces from that runner.
Per software/CLAUDE.md ("fix portability one layer down, in the sysroot … patch the real upstream tool only as a fallback"), the fix likely belongs in the WASI/fd layer rather than in brush.
Related
Summary
Under an interactive PTY (
terminal.open()→ brush), compound commands produce no output and no error. The same constructs work throughexec()and throughsh -cinside the same PTY, so this is specific to brush's interactive path, not to pipelines.Two symptoms, likely one root cause — both are writes from the interactive shell that never reach the terminal:
;,&&,|,( ), backticks) print nothing. Redirecting stderr shows the real failure is WASI errno 21 (EFAULT) on a write.command not found.Symptom 2 is what makes symptom 1 invisible: the shell is reporting an error, it just goes nowhere.
Reproduction
Observed:
exec()echo oneoneuname -aecho 'semi;colon'semi;colonsh -c 'echo a | cat'ash -c 'echo b; echo c'bcecho b; echo cecho d && echo eecho a | cat(echo sub)nosuchcmdNote
echohere is a brush builtin, so the failing case involves no external command at all.The hidden error
Redirecting stderr to a file makes both failures visible:
Under
wasm32-wasip1, errno 21 isEFAULT(WASI numbering, not Linux'sEISDIR), so "Bad address" and the code agree. brush parsed and executed the list correctly — the write to stdout failed.Ruled out
;themselves —exec("ls /opt/agentos/bin | head -5")returns correct output.sh -c 'echo a | cat'works inside the failing interactive shell.sh -c 'echo to-stderr 1>&2'renders). Only the interactive shell's own stderr is lost.TERM— identical failure withTERM=xterm-256colorandTERM=dumb.vm.terminal.write(), no browser involved.Where to look
_fdWriteincrates/execution/assets/runners/wasi-module.js:1404returnsEFAULTonly via_writeUint32(:413) —_mapFsError(:811) defaults toEIO, notEFAULT. So the failing path is thenwrittenpointer write, or a synthetic-pipe branch, rather than a generic fs error.crates/CLAUDE.mdalready documents this failure mode in the WASM host-process bridge:Debug logging:
AGENTOS_WASM_WASI_DEBUG=1enables[agentos-wasi]traces from that runner.Per
software/CLAUDE.md("fix portability one layer down, in the sysroot … patch the real upstream tool only as a fallback"), the fix likely belongs in the WASI/fd layer rather than in brush.Related
exec, whereecho hi | head -n 1still works. Shares the "silent failure / misleading exit code" theme.>>append truncates and< filestdin redirection fails #1657 — other brush redirection divergences (>>,< file).