Skip to content

Interactive PTY: compound commands silently produce no output (WASI EFAULT), and shell diagnostics never reach the terminal #1893

Description

@jog1t

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:

  1. Compound commands (;, &&, |, ( ), backticks) print nothing. Redirecting stderr shows the real failure is WASI errno 21 (EFAULT) on a write.
  2. 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 / ; themselvesexec("ls /opt/agentos/bin | head -5") returns correct output.
  • The kernel PTY, fd wiring, job controlsh -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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions