diff --git a/container/agent-runner/src/poll-loop.test.ts b/container/agent-runner/src/poll-loop.test.ts
index 7b85faaaa9..9fba21ab34 100644
--- a/container/agent-runner/src/poll-loop.test.ts
+++ b/container/agent-runner/src/poll-loop.test.ts
@@ -4,7 +4,7 @@ import { initTestSessionDb, closeSessionDb, getInboundDb, getOutboundDb } from '
import { getPendingMessages, markCompleted } from './db/messages-in.js';
import { getUndeliveredMessages } from './db/messages-out.js';
import { formatMessages, extractRouting } from './formatter.js';
-import { isCorruptionError } from './poll-loop.js';
+import { findClosingMessageTag, isCorruptionError } from './poll-loop.js';
import { MockProvider } from './providers/mock.js';
beforeEach(() => {
@@ -379,6 +379,61 @@ describe('end-to-end with mock provider', () => {
});
});
+describe('findClosingMessageTag', () => {
+ // Helper: simulate the dispatch parser by finding open,
+ // then looking up the matching close starting from the body.
+ function bodyOf(text: string): string {
+ const openRe = //;
+ const m = openRe.exec(text);
+ if (!m) throw new Error('no open tag');
+ const bodyStart = m.index + m[0].length;
+ const close = findClosingMessageTag(text, bodyStart);
+ if (close === -1) return '';
+ return text.slice(bodyStart, close);
+ }
+
+ it('matches the close of a plain message', () => {
+ expect(bodyOf('hello')).toBe('hello');
+ });
+
+ it('ignores inside inline backticks', () => {
+ // Agent writing about NanoClaw destinations in prose
+ const text = 'routing example: `…` covered';
+ expect(bodyOf(text)).toBe('routing example: `…` covered');
+ });
+
+ it('ignores inside fenced code blocks', () => {
+ const text = [
+ 'here is the format:',
+ '```',
+ 'body',
+ '```',
+ 'end',
+ ].join('\n');
+ expect(bodyOf(text)).toBe(['here is the format:', '```', 'body', '```', 'end'].join('\n'));
+ });
+
+ it('returns -1 when the close tag is missing', () => {
+ expect(findClosingMessageTag('no close here', 19)).toBe(-1);
+ });
+
+ it('treats inline-backtick close as unclosed when no other close follows', () => {
+ // Single `` inside inline backticks with nothing after — outer is unclosed
+ const text = 'talking about `` only';
+ expect(findClosingMessageTag(text, 19)).toBe(-1);
+ });
+
+ it('finds the outer close even when an inner close is inside a fenced block', () => {
+ // The inner in the fence must NOT short-circuit the outer match.
+ const text = 'prefix\n```\n\n```\nsuffix';
+ const start = ''.length;
+ const close = findClosingMessageTag(text, start);
+ // It should land on the outer , not the fenced one
+ expect(close).toBeGreaterThan(text.indexOf('```\nsuffix'));
+ expect(text.slice(close)).toBe('');
+ });
+});
+
describe('isCorruptionError', () => {
it('matches the Docker Desktop macOS torn-read symptom', () => {
expect(isCorruptionError('database disk image is malformed')).toBe(true);
diff --git a/container/agent-runner/src/poll-loop.ts b/container/agent-runner/src/poll-loop.ts
index 1b7d181a3c..338932e61d 100644
--- a/container/agent-runner/src/poll-loop.ts
+++ b/container/agent-runner/src/poll-loop.ts
@@ -492,20 +492,31 @@ function handleEvent(event: ProviderEvent, _routing: RoutingContext): void {
* blocks, even with a single destination. Bare text is scratchpad only.
*/
function dispatchResultText(text: string, routing: RoutingContext): { sent: number; hasUnwrapped: boolean } {
- const MESSAGE_RE = /([\s\S]*?)<\/message>/g;
+ const OPEN_RE = //g;
let match: RegExpExecArray | null;
let sent = 0;
let lastIndex = 0;
const scratchpadParts: string[] = [];
- while ((match = MESSAGE_RE.exec(text)) !== null) {
+ while ((match = OPEN_RE.exec(text)) !== null) {
if (match.index > lastIndex) {
scratchpadParts.push(text.slice(lastIndex, match.index));
}
const toName = match[1];
- const body = match[2].trim();
- lastIndex = MESSAGE_RE.lastIndex;
+ const bodyStart = OPEN_RE.lastIndex;
+ // Find the matching , ignoring any inside ``` fences or `inline`
+ // code spans so example tags in proposals/code don't truncate the body.
+ const closeIdx = findClosingMessageTag(text, bodyStart);
+ if (closeIdx === -1) {
+ // Unclosed — treat remainder as scratchpad and stop scanning
+ scratchpadParts.push(text.slice(match.index));
+ lastIndex = text.length;
+ break;
+ }
+ const body = text.slice(bodyStart, closeIdx).trim();
+ lastIndex = closeIdx + ''.length;
+ OPEN_RE.lastIndex = lastIndex;
const dest = findByName(toName);
if (!dest) {
@@ -533,6 +544,44 @@ function dispatchResultText(text: string, routing: RoutingContext): { sent: numb
return { sent, hasUnwrapped };
}
+/**
+ * Find the next `` close tag starting from `start`, ignoring any
+ * that appear inside ``` fenced blocks or single-line `inline` code spans.
+ * Returns the index of the `<` of ``, or -1 if none found.
+ *
+ * Jumps via `indexOf` between candidate close-tags and backticks so the cost
+ * is dominated by the (sparse) backtick spans rather than walking each char.
+ *
+ * Exported for unit testing.
+ */
+export function findClosingMessageTag(text: string, start: number): number {
+ const CLOSE = '';
+ let i = start;
+ while (i < text.length) {
+ const close = text.indexOf(CLOSE, i);
+ if (close === -1) return -1;
+ const tick = text.indexOf('`', i);
+ if (tick === -1 || close < tick) return close;
+ // Tick comes first — figure out if it's a triple-backtick fence or an
+ // inline single-tick span, then skip past the matching close marker.
+ if (text.startsWith('```', tick)) {
+ const end = text.indexOf('```', tick + 3);
+ if (end === -1) return -1; // unterminated fence — give up
+ i = end + 3;
+ continue;
+ }
+ const nl = text.indexOf('\n', tick + 1);
+ const inlineEnd = text.indexOf('`', tick + 1);
+ if (inlineEnd !== -1 && (nl === -1 || inlineEnd < nl)) {
+ i = inlineEnd + 1;
+ } else {
+ // Lone backtick with no closer on the same line — treat as plain text
+ i = tick + 1;
+ }
+ }
+ return -1;
+}
+
function sendToDestination(dest: DestinationEntry, body: string, routing: RoutingContext): void {
const platformId = dest.type === 'channel' ? dest.platformId! : dest.agentGroupId!;
const channelType = dest.type === 'channel' ? dest.channelType! : 'agent';