mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
fix(agent-runner): drop <messages> envelope so claude-agent-sdk calls API
When 2+ pending messages were bundled into <messages>...</messages> at
container/agent-runner/src/formatter.ts:162-167, the Claude Agent SDK
responded with a synthetic stub (model="<synthetic>", 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 `<message id=... from=...>...</message>`
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 `<messages>` wrapper), one positive
(each inbound row produces a `<message>` 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) <noreply@anthropic.com>
This commit is contained in:
@@ -51,14 +51,43 @@ describe('context timezone header', () => {
|
|||||||
expect(result).toContain(`<context timezone="${TIMEZONE}"`);
|
expect(result).toContain(`<context timezone="${TIMEZONE}"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('header comes before the <messages> block', () => {
|
it('header comes before the first <message> block when multiple are present', () => {
|
||||||
insertMessage('m1', 'chat', { sender: 'Alice', text: 'one' });
|
insertMessage('m1', 'chat', { sender: 'Alice', text: 'one' });
|
||||||
insertMessage('m2', 'chat', { sender: 'Bob', text: 'two' });
|
insertMessage('m2', 'chat', { sender: 'Bob', text: 'two' });
|
||||||
const result = formatMessages(getPendingMessages());
|
const result = formatMessages(getPendingMessages());
|
||||||
const ctxIdx = result.indexOf('<context');
|
const ctxIdx = result.indexOf('<context');
|
||||||
const msgsIdx = result.indexOf('<messages>');
|
const firstMsgIdx = result.indexOf('<message ');
|
||||||
expect(ctxIdx).toBeGreaterThanOrEqual(0);
|
expect(ctxIdx).toBeGreaterThanOrEqual(0);
|
||||||
expect(msgsIdx).toBeGreaterThan(ctxIdx);
|
expect(firstMsgIdx).toBeGreaterThan(ctxIdx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multi-message chat batches', () => {
|
||||||
|
// Regression guard for #2555: an outer `<messages>` envelope around
|
||||||
|
// multiple chat messages caused the Claude Agent SDK to emit a synthetic
|
||||||
|
// `No response requested.` stub instead of calling the API. Each
|
||||||
|
// `<message>` block is self-contained; concatenating them is enough.
|
||||||
|
it('does NOT wrap multiple chat messages in an outer <messages> envelope', () => {
|
||||||
|
insertMessage('m1', 'chat', { sender: 'Alice', text: 'one' });
|
||||||
|
insertMessage('m2', 'chat', { sender: 'Bob', text: 'two' });
|
||||||
|
const result = formatMessages(getPendingMessages());
|
||||||
|
expect(result).not.toContain('<messages>');
|
||||||
|
expect(result).not.toContain('</messages>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits one <message> 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(/<message [^>]*>/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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -155,16 +155,15 @@ export function formatMessages(messages: MessageInRow[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatChatMessages(messages: MessageInRow[]): string {
|
function formatChatMessages(messages: MessageInRow[]): string {
|
||||||
if (messages.length === 1) {
|
// Each `<message id="..." from="...">...</message>` block is self-contained;
|
||||||
return formatSingleChat(messages[0]);
|
// concatenating them reads to the agent as a sequence of distinct messages.
|
||||||
}
|
// Earlier revisions wrapped multi-message batches in an outer `<messages>`
|
||||||
|
// envelope, but the Claude Agent SDK responded to that shape with a
|
||||||
const lines = ['<messages>'];
|
// synthetic stub (`model: "<synthetic>"`, `content: "No response
|
||||||
for (const msg of messages) {
|
// requested."`) instead of calling the API — see #2555 for the full trace.
|
||||||
lines.push(formatSingleChat(msg));
|
// 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.
|
||||||
lines.push('</messages>');
|
return messages.map(formatSingleChat).join('\n');
|
||||||
return lines.join('\n');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSingleChat(msg: MessageInRow): string {
|
function formatSingleChat(msg: MessageInRow): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user