Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/pm/history/pm-beads-6a9l.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"ts":"2026-07-24T20:24:33.459Z","author":"claude-opus-5","op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-beads-6a9l"},{"op":"add","path":"/metadata/title","value":"pm item reads are capped at Node's 1 MiB spawnSync default, so a mature tracker fails with no diagnosis"},{"op":"add","path":"/metadata/description","value":"readPmItems (export/import idempotency index) spawns the pm CLI to read items but passes no maxBuffer, so Node's 1 MiB spawnSync default applies. Measured against a real 443-item pm workspace, this package's exact argv returns 1054050 bytes (ENOBUFS today). Past the cap the child is killed with status null, signal SIGTERM, error.code ENOBUFS and EMPTY stderr, so the failure surfaces with nothing to diagnose; at larger sizes stdout is genuinely truncated mid-document (verified: a 100 KiB cap yields 'Unterminated string in JSON at position 130829'). Sibling packages pm-changelog, pm-context, pm-brief, pm-graph and pm-linear already cap explicitly; this package was a straggler. Fix: cap at 64 MiB and name an ENOBUFS overrun explicitly instead of reporting an unexplained failure (or, for the never-throw demo/search paths, an empty result set)."},{"op":"add","path":"/metadata/type","value":"Issue"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":1},{"op":"add","path":"/metadata/tags","value":["agent-ux","maxbuffer","production-readiness","real-data"]},{"op":"add","path":"/metadata/created_at","value":"2026-07-24T20:24:33.459Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-24T20:24:33.459Z"},{"op":"add","path":"/metadata/author","value":"claude-opus-5"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"f5771fbd8365af7e50e34b41379eff444b4d7c6b9e739dc8fa3d4ff6d192e310","message":"File the 1 MiB pm-read ceiling found by a fleet-wide audit against a real 443-item workspace"}
{"ts":"2026-07-24T20:24:34.068Z","author":"claude-opus-5","op":"close","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-07-24T20:24:34.068Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-07-24T20:24:34.067Z"},{"op":"add","path":"/metadata/resolution","value":"Added PM_JSON_MAX_BUFFER (64 MiB) and passed it to the pm read; ENOBUFS is now reported as a buffer overrun naming the limit and the remedy."},{"op":"add","path":"/metadata/expected_result","value":"Reads of a mature tracker succeed, and any future overrun reports its real cause instead of an unexplained failure or a silent empty result."},{"op":"add","path":"/metadata/actual_result","value":"Verified with the package's exact argv against the real 443-item workspace: default cap -> status null / ENOBUFS; 64 MiB cap -> status 0 with the complete payload. Typecheck and the full test suite pass."},{"op":"add","path":"/metadata/close_reason","value":"Capped the pm read at 64 MiB and made an ENOBUFS overrun self-describing"}],"before_hash":"f5771fbd8365af7e50e34b41379eff444b4d7c6b9e739dc8fa3d4ff6d192e310","after_hash":"5f2294af7d26d3ea91e3d4d13f08da369a121802a4e20328b4a2ed969ab1b93b","message":"Close after verifying the capped read against the real workspace"}
16 changes: 16 additions & 0 deletions .agents/pm/issues/pm-beads-6a9l.toon
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
id: pm-beads-6a9l
title: "pm item reads are capped at Node's 1 MiB spawnSync default, so a mature tracker fails with no diagnosis"
description: "readPmItems (export/import idempotency index) spawns the pm CLI to read items but passes no maxBuffer, so Node's 1 MiB spawnSync default applies. Measured against a real 443-item pm workspace, this package's exact argv returns 1054050 bytes (ENOBUFS today). Past the cap the child is killed with status null, signal SIGTERM, error.code ENOBUFS and EMPTY stderr, so the failure surfaces with nothing to diagnose; at larger sizes stdout is genuinely truncated mid-document (verified: a 100 KiB cap yields 'Unterminated string in JSON at position 130829'). Sibling packages pm-changelog, pm-context, pm-brief, pm-graph and pm-linear already cap explicitly; this package was a straggler. Fix: cap at 64 MiB and name an ENOBUFS overrun explicitly instead of reporting an unexplained failure (or, for the never-throw demo/search paths, an empty result set)."
type: Issue
status: closed
priority: 1
tags[4]: agent-ux,maxbuffer,production-readiness,real-data
created_at: "2026-07-24T20:24:33.459Z"
updated_at: "2026-07-24T20:24:34.068Z"
closed_at: "2026-07-24T20:24:34.067Z"
author: claude-opus-5
resolution: Added PM_JSON_MAX_BUFFER (64 MiB) and passed it to the pm read; ENOBUFS is now reported as a buffer overrun naming the limit and the remedy.
expected_result: "Reads of a mature tracker succeed, and any future overrun reports its real cause instead of an unexplained failure or a silent empty result."
actual_result: "Verified with the package's exact argv against the real 443-item workspace: default cap -> status null / ENOBUFS; 64 MiB cap -> status 0 with the complete payload. Typecheck and the full test suite pass."
close_reason: Capped the pm read at 64 MiB and made an ENOBUFS overrun self-describing
body: ""
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixed

- pm item reads are capped at Node's 1 MiB spawnSync default, so a mature tracker fails with no diagnosis ([pm-beads-6a9l](https://github.com/unbraind/pm-beads/blob/main/.agents/pm/issues/pm-beads-6a9l.toon))

## 2026.7.23 - 2026-07-23

### Fixed
Expand Down
36 changes: 35 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1361,14 +1361,48 @@ export function extractCreatedId(stdout: string): string | undefined {
// Export core — serialize pm items back to Beads JSONL
// ---------------------------------------------------------------------------

// Node's spawnSync defaults to a 1 MiB stdout cap, which a mature tracker's JSON
// dump passes at a few hundred items. Past that the child is killed with ENOBUFS,
// status null and EMPTY stderr, so the failure surfaces with nothing to diagnose
// (and at larger sizes stdout is genuinely truncated mid-document).
// 64 MiB matches the cap the sibling pm packages settled on.
/** Read-buffer cap for `pm` output, in bytes. 64 MiB by default; override with the
* `PM_JSON_MAX_BUFFER` env var. Resolved per call so the override takes effect
* without an import-order dependency. Invalid or non-positive values fall back to
* the default rather than silently disabling the guard. */
function pmJsonMaxBuffer(): number {
// Number(), not parseInt(): parseInt("64MiB") silently yields 64, which would
// impose a 64-BYTE cap and break every ordinary read while appearing to honor
// the documented invalid-value fallback. Number() rejects the whole string.
const raw = Number(process.env.PM_JSON_MAX_BUFFER);
return Number.isSafeInteger(raw) && raw > 0 ? raw : 64 * 1024 * 1024;
}

/** Name the real cause of a failed `pm` read. A stdout overrun kills the child
* with `status: null` and EMPTY stderr, so without this the failure surfaces as
* an unexplained error (or, worse, as an empty result set). */
function describePmReadFailure(error: Error, limitBytes: number): string {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOBUFS") {
return `pm output exceeded the ${limitBytes} byte read buffer. `
+ "The workspace is larger than this integration's read limit; narrow the "
+ "operation or raise PM_JSON_MAX_BUFFER.";
}
return `pm read failed: ${error.message}`;
}

function readPmItems(pmRoot: string): PmItem[] {
const maxBuffer = pmJsonMaxBuffer();
// `--full --include-body` so descriptions, tags and dependency edges survive
// the export instead of the brief projection (which omits them).
const result = spawnSync(
"pm",
["--path", pmRoot, "--json", "list-all", "--full", "--include-body", "--limit", "10000"],
{ encoding: "utf-8" },
{ encoding: "utf-8", maxBuffer },
);
if (result.error) {
throw new CommandError(describePmReadFailure(result.error, maxBuffer));
}
if (result.status !== 0) {
throw new CommandError(result.stderr || "pm list failed");
}
Expand Down