Summary
fetchIssues() in LIFEOS/PULSE/modules/work.ts spawns gh issue list with stderr: "pipe" but never reads that stream. On a non-zero exit it logs only the exit code and discards gh's actual error message:
const proc = Bun.spawn(
["gh", "issue", "list", "--repo", repo, "--state", "all", "--limit", "500", "--json", /* … */],
{ stdout: "pipe", stderr: "pipe", timeout: 12000 },
);
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.error(`[${MODULE}] gh issue list failed (exit ${exitCode})`); // gh's stderr is lost
return null;
}
This one root cause (stderr piped but never drained) has two consequences:
1. No diagnosability. The work module polls every 60s, so a transient gh failure (auth blip, GitHub secondary rate limit, network) fills the log with cause-less [work] gh issue list failed (exit 1) lines. There's no way to tell an auth problem from a rate-limit from a repo-access problem. Observed live: hundreds of such lines accumulated in pulse-stderr.log, while the identical gh issue list command run by hand against the same repo returns exit 0.
2. Latent deadlock → spurious timeout failure. Because stderr is piped but never consumed, if gh ever writes more than the OS pipe buffer (~64KB) to stderr, gh blocks on the stderr write, await proc.exited never resolves, and the process is killed at the 12s timeout — surfacing as a failure even though gh would have succeeded. This is currently masked when the work repo has zero issues (gh writes nothing to stderr), but any gh deprecation/warning banner or a larger error payload can trigger it.
This is the same "undrained/dangling Bun stream" class already fixed for hook stdin in #1021 (hooks/lib/hook-io.ts); the fix here is symmetric.
Affected
LIFEOS/PULSE/modules/work.ts → fetchIssues() — identical on main and in the v7.1.1 release.
- The sibling
gh helpers in TOOLS/WorkSweep.ts (ghIssueSearchSlug, ghLabelsExisting, ghListOpenIssues, ghCreateIssue, ghAddLabel) share the same pipe-stderr-but-never-read pattern.
Suggested fix
Drain both pipes and include stderr in the failure log:
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.error(`[${MODULE}] gh issue list failed (exit ${exitCode}): ${stderr.trim().slice(0, 500)}`);
return null;
}
Draining both streams removes the deadlock and makes failures self-explaining.
Environment
- LifeOS 7.1.1
- Ubuntu, Pulse running as a
systemd --user service
gh version 2.96.0
Summary
fetchIssues()inLIFEOS/PULSE/modules/work.tsspawnsgh issue listwithstderr: "pipe"but never reads that stream. On a non-zero exit it logs only the exit code and discards gh's actual error message:This one root cause (stderr piped but never drained) has two consequences:
1. No diagnosability. The work module polls every 60s, so a transient
ghfailure (auth blip, GitHub secondary rate limit, network) fills the log with cause-less[work] gh issue list failed (exit 1)lines. There's no way to tell an auth problem from a rate-limit from a repo-access problem. Observed live: hundreds of such lines accumulated inpulse-stderr.log, while the identicalgh issue listcommand run by hand against the same repo returns exit 0.2. Latent deadlock → spurious timeout failure. Because stderr is piped but never consumed, if
ghever writes more than the OS pipe buffer (~64KB) to stderr,ghblocks on the stderr write,await proc.exitednever resolves, and the process is killed at the 12stimeout— surfacing as a failure even thoughghwould have succeeded. This is currently masked when the work repo has zero issues (gh writes nothing to stderr), but anyghdeprecation/warning banner or a larger error payload can trigger it.This is the same "undrained/dangling Bun stream" class already fixed for hook stdin in #1021 (
hooks/lib/hook-io.ts); the fix here is symmetric.Affected
LIFEOS/PULSE/modules/work.ts→fetchIssues()— identical onmainand in thev7.1.1release.ghhelpers inTOOLS/WorkSweep.ts(ghIssueSearchSlug,ghLabelsExisting,ghListOpenIssues,ghCreateIssue,ghAddLabel) share the same pipe-stderr-but-never-read pattern.Suggested fix
Drain both pipes and include stderr in the failure log:
Draining both streams removes the deadlock and makes failures self-explaining.
Environment
systemd --userserviceghversion 2.96.0