From 1d45d78784006849ce7c33d0b2df87c93c1ea0fa Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 1 Sep 2026 14:17:59 +0200 Subject: [PATCH 1/4] fix(coding-agent): suppress rlm_child_update emits with an unchanged snapshot Child streaming re-emits the parent's child snapshot on every assistant delta; once the answer preview caps and activity is steady, the repeats are byte-identical and only add wire and render noise for every attached client. Emit only when the serialized snapshot actually changed. --- .../coding-agent/src/core/agent-session.ts | 10 ++++- .../test/agent-session-recursion.test.ts | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 922136b768..e7a5115259 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -916,6 +916,8 @@ interface RlmChildRun { completeDeletion?: () => Promise; reportDeletionCleanupFailure?: (error: unknown) => Promise; emitUpdate?: () => void; + /** Serialized last-emitted snapshot; suppresses no-change per-delta updates. */ + lastEmittedUpdate?: string; unsubscribe?: () => void; } @@ -10467,7 +10469,13 @@ export class AgentSession { this._activeRlmChildRuns.set(run.id, run); this._unsettledRlmChildRuns.add(run); const emitChildUpdate = () => { - this._emit({ type: "rlm_child_update", child: this._rlmChildSnapshotForRun(run) }); + // Child streaming re-emits per delta; once the snapshot stops changing + // (preview capped, activity steady) the repeats are pure wire noise. + const child = this._rlmChildSnapshotForRun(run); + const serialized = JSON.stringify(child); + if (serialized === run.lastEmittedUpdate) return; + run.lastEmittedUpdate = serialized; + this._emit({ type: "rlm_child_update", child }); }; run.emitUpdate = emitChildUpdate; emitChildUpdate(); diff --git a/packages/coding-agent/test/agent-session-recursion.test.ts b/packages/coding-agent/test/agent-session-recursion.test.ts index d492e1bbc4..298158c4a2 100644 --- a/packages/coding-agent/test/agent-session-recursion.test.ts +++ b/packages/coding-agent/test/agent-session-recursion.test.ts @@ -102,6 +102,8 @@ interface InspectableRlmRun { settled: boolean; error?: string; abandonedForQuiescence?: boolean; + activity?: { kind: string }; + emitUpdate?: () => void; publication?: { promise: Promise; resolve(): void; reject(error: Error): void }; settlement?: { promise: Promise; resolve(): void; reject(error: Error): void }; detachedDeletion?: Awaited>["subagents"][number]; @@ -2374,6 +2376,45 @@ describe("AgentSession rlm recursion", () => { await waitFor(() => rootRun.status === "done"); }); + it("suppresses repeated child updates whose snapshot did not change", async () => { + let releaseChild: () => void = () => {}; + const release = new Promise((resolve) => { + releaseChild = resolve; + }); + let childStarted = false; + const root = createSession({ + streamFn: (_model, context) => { + const text = userText(context); + const stream = createAssistantMessageEventStream(); + childStarted = true; + void release.then(() => { + stream.push({ type: "done", reason: "stop", message: assistantMessage(`child answer: ${text}`) }); + }); + return stream; + }, + }); + let updates = 0; + root.subscribe((event) => { + if (event.type === "rlm_child_update") updates += 1; + }); + + await root.runRlmChild("slow shard"); + await waitFor(() => childStarted); + const run = [...(root as unknown as InspectableRlmSession)._activeRlmChildRuns.values()][0]; + if (!run?.emitUpdate || !run.session) throw new Error("Missing child run emit"); + // The gated stream leaves the run in a steady state; re-emits carry no change. + await waitFor(() => run.activity?.kind === "waiting"); + const before = updates; + run.emitUpdate(); + run.emitUpdate(); + expect(updates).toBe(before); + run.session.setCurrentRecap("changed recap"); + await waitFor(() => updates === before + 1); + + releaseChild(); + await waitFor(() => run.status === "done"); + }); + it("runs a child agent without requiring ripgrep", async () => { const streamFn = vi.fn((_model, context: Context) => streamAnswer(`child answer: ${userText(context)}`)); const root = createSession({ streamFn }); From 9a5e9a17d8d1ad5bad0bd5ac3dde66c6b56945e4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 1 Sep 2026 14:18:04 +0200 Subject: [PATCH 2/4] fix(coding-agent): bound the kernel stderr diagnostic buffer to a tail kernelStderr accumulated for the kernel's lifetime while every reader takes at most the last 1 KiB; keep only an 8 KiB tail. --- .../coding-agent/.changes/relay-hygiene.md | 1 + .../src/core/kernel/repl-manager.ts | 11 +++++++-- .../test/repl-kernel-startup.test.ts | 24 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 packages/coding-agent/.changes/relay-hygiene.md diff --git a/packages/coding-agent/.changes/relay-hygiene.md b/packages/coding-agent/.changes/relay-hygiene.md new file mode 100644 index 0000000000..33cec95f0c --- /dev/null +++ b/packages/coding-agent/.changes/relay-hygiene.md @@ -0,0 +1 @@ +- Reduced idle streaming overhead: repeated `rlm_child_update` events with an unchanged child snapshot are no longer emitted, and the kernel stderr diagnostic buffer keeps only a bounded tail instead of growing for the kernel's lifetime. diff --git a/packages/coding-agent/src/core/kernel/repl-manager.ts b/packages/coding-agent/src/core/kernel/repl-manager.ts index a78ae3282a..4f9209965f 100644 --- a/packages/coding-agent/src/core/kernel/repl-manager.ts +++ b/packages/coding-agent/src/core/kernel/repl-manager.ts @@ -56,6 +56,9 @@ const MAX_HANDLED_HOST_REQUEST_IDS = 1024; // Cap for unattributed background output buffered between and during cells. const MAX_BACKGROUND_OUTPUT_CHARS = 64 * 1024; +// Diagnostics keep only a stderr tail; readers slice at most the last 1 KiB. +const MAX_KERNEL_STDERR_CHARS = 8 * 1024; + /** ExecuteResult plus the raw fields of the request's `done` event (state ops). */ interface InternalExecuteResult extends ExecuteResult { doneFields?: Record; @@ -201,7 +204,11 @@ export class ReplKernelManager { } private appendKernelDiagnostic(message: string): void { - this.kernelStderr += `[kernel] ${message.endsWith("\n") ? message : `${message}\n`}`; + this.appendKernelStderr(`[kernel] ${message.endsWith("\n") ? message : `${message}\n`}`); + } + + private appendKernelStderr(chunk: string): void { + this.kernelStderr = (this.kernelStderr + chunk).slice(-MAX_KERNEL_STDERR_CHARS); } async start(options: KernelStartOptions = {}): Promise { @@ -329,7 +336,7 @@ export class ReplKernelManager { }); child.stderr?.on("data", (buf: Buffer) => { - this.kernelStderr += buf.toString(); + this.appendKernelStderr(buf.toString()); }); child.on("error", (err) => { diff --git a/packages/coding-agent/test/repl-kernel-startup.test.ts b/packages/coding-agent/test/repl-kernel-startup.test.ts index 219453f729..5f6cea9d76 100644 --- a/packages/coding-agent/test/repl-kernel-startup.test.ts +++ b/packages/coding-agent/test/repl-kernel-startup.test.ts @@ -39,6 +39,30 @@ describe("ReplKernelManager startup", () => { } }); + it("keeps only a bounded stderr tail from a chatty kernel", async () => { + const python = join(tempDir, "python"); + // ~200 KiB of stderr noise before dying; only a tail may stay resident. + writeExecutable( + python, + ["#!/bin/sh", 'yes "noise line" | head -c 204800 >&2', 'echo "final stderr line" >&2', "exit 42", ""].join( + "\n", + ), + ); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + const manager = new ReplKernelManager({ python, cwd: tempDir }); + + try { + await expect(manager.execute("print(1)")).rejects.toThrow( + /Kernel exited before ready[\s\S]*final stderr line/, + ); + const stderr = (manager as unknown as { kernelStderr: string }).kernelStderr; + expect(stderr.length).toBeLessThanOrEqual(8 * 1024); + } finally { + errorSpy.mockRestore(); + await manager.shutdown({ snapshot: true, drainHostRequests: true }); + } + }); + it("fails a runtime announcing an unexpected protocol version", async () => { const python = join(tempDir, "python"); writeExecutable( From d9b47e6d9b3c014edc8419ce10b8bc9eaf9bdb24 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 1 Sep 2026 14:26:22 +0200 Subject: [PATCH 3/4] chore(coding-agent): drop narrating comments from the relay-hygiene diff --- packages/coding-agent/src/core/agent-session.ts | 3 --- packages/coding-agent/src/core/kernel/repl-manager.ts | 2 -- packages/coding-agent/test/agent-session-recursion.test.ts | 1 - packages/coding-agent/test/repl-kernel-startup.test.ts | 1 - 4 files changed, 7 deletions(-) diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index e7a5115259..f2a7068e0a 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -916,7 +916,6 @@ interface RlmChildRun { completeDeletion?: () => Promise; reportDeletionCleanupFailure?: (error: unknown) => Promise; emitUpdate?: () => void; - /** Serialized last-emitted snapshot; suppresses no-change per-delta updates. */ lastEmittedUpdate?: string; unsubscribe?: () => void; } @@ -10469,8 +10468,6 @@ export class AgentSession { this._activeRlmChildRuns.set(run.id, run); this._unsettledRlmChildRuns.add(run); const emitChildUpdate = () => { - // Child streaming re-emits per delta; once the snapshot stops changing - // (preview capped, activity steady) the repeats are pure wire noise. const child = this._rlmChildSnapshotForRun(run); const serialized = JSON.stringify(child); if (serialized === run.lastEmittedUpdate) return; diff --git a/packages/coding-agent/src/core/kernel/repl-manager.ts b/packages/coding-agent/src/core/kernel/repl-manager.ts index 4f9209965f..d09f255177 100644 --- a/packages/coding-agent/src/core/kernel/repl-manager.ts +++ b/packages/coding-agent/src/core/kernel/repl-manager.ts @@ -53,10 +53,8 @@ const REPAIR_STEP_TIMEOUT_MS = 30_000; // Runtime-minted host-request ids never repeat; the bound only guards a // misbehaving runtime from growing the dedup set forever. const MAX_HANDLED_HOST_REQUEST_IDS = 1024; -// Cap for unattributed background output buffered between and during cells. const MAX_BACKGROUND_OUTPUT_CHARS = 64 * 1024; -// Diagnostics keep only a stderr tail; readers slice at most the last 1 KiB. const MAX_KERNEL_STDERR_CHARS = 8 * 1024; /** ExecuteResult plus the raw fields of the request's `done` event (state ops). */ diff --git a/packages/coding-agent/test/agent-session-recursion.test.ts b/packages/coding-agent/test/agent-session-recursion.test.ts index 298158c4a2..579ad3fc5a 100644 --- a/packages/coding-agent/test/agent-session-recursion.test.ts +++ b/packages/coding-agent/test/agent-session-recursion.test.ts @@ -2402,7 +2402,6 @@ describe("AgentSession rlm recursion", () => { await waitFor(() => childStarted); const run = [...(root as unknown as InspectableRlmSession)._activeRlmChildRuns.values()][0]; if (!run?.emitUpdate || !run.session) throw new Error("Missing child run emit"); - // The gated stream leaves the run in a steady state; re-emits carry no change. await waitFor(() => run.activity?.kind === "waiting"); const before = updates; run.emitUpdate(); diff --git a/packages/coding-agent/test/repl-kernel-startup.test.ts b/packages/coding-agent/test/repl-kernel-startup.test.ts index 5f6cea9d76..9f9263a067 100644 --- a/packages/coding-agent/test/repl-kernel-startup.test.ts +++ b/packages/coding-agent/test/repl-kernel-startup.test.ts @@ -41,7 +41,6 @@ describe("ReplKernelManager startup", () => { it("keeps only a bounded stderr tail from a chatty kernel", async () => { const python = join(tempDir, "python"); - // ~200 KiB of stderr noise before dying; only a tail may stay resident. writeExecutable( python, ["#!/bin/sh", 'yes "noise line" | head -c 204800 >&2', 'echo "final stderr line" >&2', "exit 42", ""].join( From 6ae09f86ef5470d28badeb605a6568144926f2f7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 1 Sep 2026 14:51:56 +0200 Subject: [PATCH 4/4] revert(coding-agent): move the kernel stderr bound out to its own PR --- .../coding-agent/.changes/relay-hygiene.md | 2 +- .../src/core/kernel/repl-manager.ts | 11 +++------ .../test/repl-kernel-startup.test.ts | 23 ------------------- 3 files changed, 4 insertions(+), 32 deletions(-) diff --git a/packages/coding-agent/.changes/relay-hygiene.md b/packages/coding-agent/.changes/relay-hygiene.md index 33cec95f0c..c3324a85dc 100644 --- a/packages/coding-agent/.changes/relay-hygiene.md +++ b/packages/coding-agent/.changes/relay-hygiene.md @@ -1 +1 @@ -- Reduced idle streaming overhead: repeated `rlm_child_update` events with an unchanged child snapshot are no longer emitted, and the kernel stderr diagnostic buffer keeps only a bounded tail instead of growing for the kernel's lifetime. +- Stopped re-emitting `rlm_child_update` events whose child snapshot did not change; identical per-token progress updates no longer reach attached clients. diff --git a/packages/coding-agent/src/core/kernel/repl-manager.ts b/packages/coding-agent/src/core/kernel/repl-manager.ts index d09f255177..a78ae3282a 100644 --- a/packages/coding-agent/src/core/kernel/repl-manager.ts +++ b/packages/coding-agent/src/core/kernel/repl-manager.ts @@ -53,10 +53,9 @@ const REPAIR_STEP_TIMEOUT_MS = 30_000; // Runtime-minted host-request ids never repeat; the bound only guards a // misbehaving runtime from growing the dedup set forever. const MAX_HANDLED_HOST_REQUEST_IDS = 1024; +// Cap for unattributed background output buffered between and during cells. const MAX_BACKGROUND_OUTPUT_CHARS = 64 * 1024; -const MAX_KERNEL_STDERR_CHARS = 8 * 1024; - /** ExecuteResult plus the raw fields of the request's `done` event (state ops). */ interface InternalExecuteResult extends ExecuteResult { doneFields?: Record; @@ -202,11 +201,7 @@ export class ReplKernelManager { } private appendKernelDiagnostic(message: string): void { - this.appendKernelStderr(`[kernel] ${message.endsWith("\n") ? message : `${message}\n`}`); - } - - private appendKernelStderr(chunk: string): void { - this.kernelStderr = (this.kernelStderr + chunk).slice(-MAX_KERNEL_STDERR_CHARS); + this.kernelStderr += `[kernel] ${message.endsWith("\n") ? message : `${message}\n`}`; } async start(options: KernelStartOptions = {}): Promise { @@ -334,7 +329,7 @@ export class ReplKernelManager { }); child.stderr?.on("data", (buf: Buffer) => { - this.appendKernelStderr(buf.toString()); + this.kernelStderr += buf.toString(); }); child.on("error", (err) => { diff --git a/packages/coding-agent/test/repl-kernel-startup.test.ts b/packages/coding-agent/test/repl-kernel-startup.test.ts index 9f9263a067..219453f729 100644 --- a/packages/coding-agent/test/repl-kernel-startup.test.ts +++ b/packages/coding-agent/test/repl-kernel-startup.test.ts @@ -39,29 +39,6 @@ describe("ReplKernelManager startup", () => { } }); - it("keeps only a bounded stderr tail from a chatty kernel", async () => { - const python = join(tempDir, "python"); - writeExecutable( - python, - ["#!/bin/sh", 'yes "noise line" | head -c 204800 >&2', 'echo "final stderr line" >&2', "exit 42", ""].join( - "\n", - ), - ); - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - const manager = new ReplKernelManager({ python, cwd: tempDir }); - - try { - await expect(manager.execute("print(1)")).rejects.toThrow( - /Kernel exited before ready[\s\S]*final stderr line/, - ); - const stderr = (manager as unknown as { kernelStderr: string }).kernelStderr; - expect(stderr.length).toBeLessThanOrEqual(8 * 1024); - } finally { - errorSpy.mockRestore(); - await manager.shutdown({ snapshot: true, drainHostRequests: true }); - } - }); - it("fails a runtime announcing an unexpected protocol version", async () => { const python = join(tempDir, "python"); writeExecutable(