In a guest shell pipeline, a WebAssembly command reading from a pipe is not woken when the writer exits. The reader blocks until the runtime's limits.resources.maxBlockingReadMs cap (10s by default) expires, and then either completes or fails with Would block / Resource temporarily unavailable (EAGAIN). In my tests a three-stage pipeline pays the cap once per stage.
Single commands are unaffected (~100ms). The stall happens only when the writer finishes before the reader has consumed everything.
Environment
@rivet-dev/agentos-core 0.2.19 (@rivet-dev/agentos-sidecar-linux-x64-gnu 0.2.19)
- Node.js v24.20.0
- Linux x86_64, glibc (Debian bookworm,
node:24.20.0-bookworm-slim image)
Reproduction
mkdir agentos-pipe && cd agentos-pipe && npm init -y >/dev/null
npm i @rivet-dev/agentos-core@0.2.19
// repro.mjs
import { AgentOs } from "@rivet-dev/agentos-core";
const permissions = { fs: "allow", childProcess: "allow", process: "allow", env: "allow", binding: "allow", network: "allow" };
for (const maxBlockingReadMs of [undefined, 2000, 500]) {
const vm = await AgentOs.create({
permissions,
...(maxBlockingReadMs ? { limits: { resources: { maxBlockingReadMs } } } : {}),
});
const run = async (label, cmd) => {
const t = Date.now();
const r = await vm.process.exec(cmd, { cwd: "/workspace", output: { capture: "all" }, timeoutMs: 60_000 });
console.log(`limit=${maxBlockingReadMs ?? "default"}`.padEnd(14), label.padEnd(22), `${Date.now() - t}ms`, `rc=${r.exitCode}`, JSON.stringify(r.stderr.slice(0, 50)));
};
await run("single command", "echo hi");
await run("ls | sed", "ls -la / | sed -n '1,3p'");
await run("env | sort | sed", "env | sort | sed -n '1,2p'");
await run("node | cat", "node -e 'setTimeout(() => console.log(\"n\"), 1500)' | cat");
await run("(sleep 3; echo) | cat", "(sleep 3; echo late) | cat");
await vm.dispose();
}
process.exit(0);
Measured
maxBlockingReadMs |
command |
time |
exit |
stderr |
| default (10000) |
echo hi |
94ms |
0 |
|
| default |
ls -la / | sed -n '1,3p' |
650ms |
0 |
|
| default |
env | sort | sed -n '1,2p' |
30268ms |
1 |
sed: Would block |
| default |
node -e '...' | cat |
10168ms |
0 |
|
| default |
(sleep 3; echo late) | cat |
3224ms |
0 |
|
| 2000 |
env | sort | sed -n '1,2p' |
332ms |
0 |
|
| 2000 |
node -e '...' | cat |
2185ms |
1 |
cat: -: Resource temporarily unavailable (output lost) |
| 500 |
env | sort | sed -n '1,2p' |
838ms |
1 |
sed: Would block |
| 500 |
node -e '...' | cat |
1659ms |
1 |
cat: -: Resource temporarily unavailable (output lost) |
A realistic agent-authored diagnostic script with five pipelines
(ls -la | sed -n '1,100p', bash --version | head -n 1,
env | sort | sed -E ... | sed -n '1,250p', ...) took 50.8s end to end;
the same script without pipes completes in well under a second.
Expected
The reader receives end-of-file as soon as the last writer closes its end, and a pipeline of fast commands completes in milliseconds regardless of maxBlockingReadMs. EAGAIN should never reach a program reading a blocking pipe.
Actual
The reader is only woken by new data, not by the writer closing, so it sits out the full maxBlockingReadMs at end of stream - once per stage - and then observes EAGAIN. Lowering the limit shortens the stall but turns it into data loss for any producer that is quiet longer than the limit (node | cat above), so there is no safe workaround from the caller's side.
Notes
In a guest shell pipeline, a WebAssembly command reading from a pipe is not woken when the writer exits. The reader blocks until the runtime's
limits.resources.maxBlockingReadMscap (10s by default) expires, and then either completes or fails withWould block/Resource temporarily unavailable(EAGAIN). In my tests a three-stage pipeline pays the cap once per stage.Single commands are unaffected (~100ms). The stall happens only when the writer finishes before the reader has consumed everything.
Environment
@rivet-dev/agentos-core0.2.19 (@rivet-dev/agentos-sidecar-linux-x64-gnu0.2.19)node:24.20.0-bookworm-slimimage)Reproduction
Measured
maxBlockingReadMsecho hils -la / | sed -n '1,3p'env | sort | sed -n '1,2p'sed: Would blocknode -e '...' | cat(sleep 3; echo late) | catenv | sort | sed -n '1,2p'node -e '...' | catcat: -: Resource temporarily unavailable(output lost)env | sort | sed -n '1,2p'sed: Would blocknode -e '...' | catcat: -: Resource temporarily unavailable(output lost)A realistic agent-authored diagnostic script with five pipelines
(
ls -la | sed -n '1,100p',bash --version | head -n 1,env | sort | sed -E ... | sed -n '1,250p', ...) took 50.8s end to end;the same script without pipes completes in well under a second.
Expected
The reader receives end-of-file as soon as the last writer closes its end, and a pipeline of fast commands completes in milliseconds regardless of
maxBlockingReadMs.EAGAINshould never reach a program reading a blocking pipe.Actual
The reader is only woken by new data, not by the writer closing, so it sits out the full
maxBlockingReadMsat end of stream - once per stage - and then observesEAGAIN. Lowering the limit shortens the stall but turns it into data loss for any producer that is quiet longer than the limit (node | catabove), so there is no safe workaround from the caller's side.Notes
env,sort,node -e) stall;(sleep 3; echo) | cat, where the reader is already blocked inreadwhen the writer closes, does not. That suggests the hangup/EOF wake-up is delivered only to a reader already parked in a blocking read, not to one that arrives afterwards.EFAULT).