From fe2e881b37fd0d8df5d19959d82b52bf5b057454 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 19 May 2026 23:47:41 +1000 Subject: [PATCH] fix(agent-runner): drop envelope so claude-agent-sdk calls API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When 2+ pending messages were bundled into ... at container/agent-runner/src/formatter.ts:162-167, the Claude Agent SDK responded with a synthetic stub (model="", stop_reason= "stop_sequence", content="No response requested.") instead of calling the real API. The poll loop never yielded a `result` event, so the inbound message was never marked completed; the container exited; the next sweep tick respawned it with the same batch; same synthetic; the transcript file ballooned with each retry until tries=5 → failed. Single-message turns (which skipped the wrapper) worked normally — the SDK's heuristic appears to treat the wrapped envelope as a context dump rather than a real user turn. Each `...` block is already self-contained, so dropping the outer wrapper lets the N>1 case work the same way the N=1 case always has. Fix: function formatChatMessages(messages: MessageInRow[]): string { return messages.map(formatSingleChat).join('\n'); } Updates one existing test that asserted on the envelope, and adds two regression tests: one negative (no `` wrapper), one positive (each inbound row produces a `` block in order). Confirmed working in a real install: two stuck lanes recovered after reducing their pending queue to 1 message, and both produced normal replies from claude after the wipe + this fix were both applied (the wipe alone wasn't enough — a fresh session given the same batch shape hit the same synthetic loop). Refs nanocoai/nanoclaw#2555 for full repro + transcript evidence. Co-Authored-By: Claude Opus 4.7 (1M context) --- container/agent-runner/src/formatter.test.ts | 35 ++++++++++++++++++-- container/agent-runner/src/formatter.ts | 19 +++++------ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/container/agent-runner/src/formatter.test.ts b/container/agent-runner/src/formatter.test.ts index e34156cd1..9121366f8 100644 --- a/container/agent-runner/src/formatter.test.ts +++ b/container/agent-runner/src/formatter.test.ts @@ -51,14 +51,43 @@ describe('context timezone header', () => { expect(result).toContain(` block', () => { + it('header comes before the first block when multiple are present', () => { insertMessage('m1', 'chat', { sender: 'Alice', text: 'one' }); insertMessage('m2', 'chat', { sender: 'Bob', text: 'two' }); const result = formatMessages(getPendingMessages()); const ctxIdx = result.indexOf(''); + const firstMsgIdx = result.indexOf(' { + // Regression guard for #2555: an outer `` envelope around + // multiple chat messages caused the Claude Agent SDK to emit a synthetic + // `No response requested.` stub instead of calling the API. Each + // `` block is self-contained; concatenating them is enough. + it('does NOT wrap multiple chat messages in an outer envelope', () => { + insertMessage('m1', 'chat', { sender: 'Alice', text: 'one' }); + insertMessage('m2', 'chat', { sender: 'Bob', text: 'two' }); + const result = formatMessages(getPendingMessages()); + expect(result).not.toContain(''); + expect(result).not.toContain(''); + }); + + it('emits one block per inbound row, in order', () => { + insertMessage('m1', 'chat', { sender: 'Alice', text: 'first' }); + insertMessage('m2', 'chat', { sender: 'Bob', text: 'second' }); + insertMessage('m3', 'chat', { sender: 'Carol', text: 'third' }); + const result = formatMessages(getPendingMessages()); + const matches = result.match(/]*>/g) ?? []; + expect(matches.length).toBe(3); + const firstIdx = result.indexOf('first'); + const secondIdx = result.indexOf('second'); + const thirdIdx = result.indexOf('third'); + expect(firstIdx).toBeGreaterThan(0); + expect(secondIdx).toBeGreaterThan(firstIdx); + expect(thirdIdx).toBeGreaterThan(secondIdx); }); }); diff --git a/container/agent-runner/src/formatter.ts b/container/agent-runner/src/formatter.ts index 236dbfb80..590875c9e 100644 --- a/container/agent-runner/src/formatter.ts +++ b/container/agent-runner/src/formatter.ts @@ -155,16 +155,15 @@ export function formatMessages(messages: MessageInRow[]): string { } function formatChatMessages(messages: MessageInRow[]): string { - if (messages.length === 1) { - return formatSingleChat(messages[0]); - } - - const lines = ['']; - for (const msg of messages) { - lines.push(formatSingleChat(msg)); - } - lines.push(''); - return lines.join('\n'); + // Each `...` block is self-contained; + // concatenating them reads to the agent as a sequence of distinct messages. + // Earlier revisions wrapped multi-message batches in an outer `` + // envelope, but the Claude Agent SDK responded to that shape with a + // synthetic stub (`model: ""`, `content: "No response + // requested."`) instead of calling the API — see #2555 for the full trace. + // The fix is simply to drop the wrapper; the single-message path (which + // already worked) is now just the N=1 case of the same code. + return messages.map(formatSingleChat).join('\n'); } function formatSingleChat(msg: MessageInRow): string {