From 7f18db0416b186ca83bf72803e21c621e815e3db Mon Sep 17 00:00:00 2001 From: thisisjun786 Date: Sun, 6 Sep 2026 14:59:49 +0000 Subject: [PATCH] fix(subagents): drain spawn hook stdout before exiting --- README.ko.md | 2 +- README.md | 2 +- README.zh.md | 2 +- .../subagent-config/dist/spawn-attach-hook.js | 3 +- .../subagent-config/src/spawn-attach-hook.ts | 3 +- .../test/spawn-attach-hook.test.ts | 31 +++++++++++++++++++ 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/README.ko.md b/README.ko.md index 67dcaf9e..3dd0fd6f 100644 --- a/README.ko.md +++ b/README.ko.md @@ -13,7 +13,7 @@

CI - 2,579 tests passing + 2,580 tests passing 28 skills 23 hooks Documentation diff --git a/README.md b/README.md index 3ef5130f..7702b2d0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@

CI - 2,579 tests passing + 2,580 tests passing 28 skills 23 hooks Documentation diff --git a/README.zh.md b/README.zh.md index 2f645a3b..91b72199 100644 --- a/README.zh.md +++ b/README.zh.md @@ -13,7 +13,7 @@

CI - 2,579 tests passing + 2,580 tests passing 28 skills 23 hooks Documentation diff --git a/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js b/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js index ca8bb4d0..d04be79a 100644 --- a/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js +++ b/plugins/codexclaw/components/subagent-config/dist/spawn-attach-hook.js @@ -968,7 +968,8 @@ function main() { ? denyEnvelope("codexclaw spawn policy input exceeded 4 MiB; refusing to bypass the recursion and trust boundary") : runSpawnAttachHook(stdin.raw); if (out) process.stdout.write(out); - process.exit(0); + // Piped stdout is asynchronous on POSIX; let pending writes finish before exiting. + process.exitCode = 0; } // Only run as a CLI entrypoint, not when imported by tests. Compare via realpath: diff --git a/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts b/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts index 05350057..95c756d9 100644 --- a/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts +++ b/plugins/codexclaw/components/subagent-config/src/spawn-attach-hook.ts @@ -968,7 +968,8 @@ function main(): void { ? denyEnvelope("codexclaw spawn policy input exceeded 4 MiB; refusing to bypass the recursion and trust boundary") : runSpawnAttachHook(stdin.raw); if (out) process.stdout.write(out); - process.exit(0); + // Piped stdout is asynchronous on POSIX; let pending writes finish before exiting. + process.exitCode = 0; } // Only run as a CLI entrypoint, not when imported by tests. Compare via realpath: diff --git a/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts b/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts index efc73b4d..58a471f0 100644 --- a/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts +++ b/plugins/codexclaw/components/subagent-config/test/spawn-attach-hook.test.ts @@ -1049,3 +1049,34 @@ test("concise entrypoint is delivered once without recursively inlining its refs const detail = readFileSync(join(SKILLS_DIR, "dev", "references", "development-practice.md"), "utf8").trim(); assert.ok(!first.includes(detail)); }); + +test("BUG-R1: CLI source and dist drain large rewritten spawn JSON over a pipe", () => { + const SENTINEL = "CXC-STDOUT-DRAIN-SENTINEL-END"; + const originalMessage = `${"A".repeat(200 * 1024)}${SENTINEL}`; + assert.ok(originalMessage.length < 256 * 1024, "spawn message must stay under the normalization cap"); + const cwd = workspaceWithConfig({ + executor: { mode: "model", model: "gpt-5.6-luna", effort: "high", promptOverride: null }, + }); + const payload = spawnPayloadAt(cwd, { agent_type: "worker", message: originalMessage }); + try { + for (const [label, cli] of [ + ["source", resolve(here, "../src/spawn-attach-hook.ts")], + ["dist", resolve(here, "../dist/spawn-attach-hook.js")], + ] as const) { + const result = spawnSync(process.execPath, [cli, "hook", "pre-tool-use"], { + input: payload, + encoding: "utf8", + maxBuffer: 2 * 1024 * 1024, + }); + assert.equal(result.status, 0, `${label} exit status`); + const parsed = JSON.parse(result.stdout); + const ui = parsed.hookSpecificOutput.updatedInput as Record; + assert.equal(ui.model, "gpt-5.6-luna", `${label} model`); + assert.equal(ui.reasoning_effort, "high", `${label} effort`); + assert.equal(typeof ui.message, "string", `${label} message type`); + assert.ok(String(ui.message).endsWith(originalMessage), `${label} original message retained`); + } + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +});