Skip to content

Pipe reader is not woken on writer close: every | stage stalls for maxBlockingReadMs at EOF, then fails with EAGAIN #1959

Description

@pdparchitect

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);
node repro.mjs

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

Activity

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

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