From a760da7fef07adaae35202396210264db93b8dcf Mon Sep 17 00:00:00 2001 From: gavrielc Date: Mon, 11 May 2026 12:38:49 +0300 Subject: [PATCH] revert: remove compaction destination reminder (PR #2327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compacted event handler injected a system-tagged reminder into the live query after SDK auto-compaction, which caused the agent to send an unintended message. Reverts the four changes from #2327: - Remove `compacted` variant from ProviderEvent union - Restore `result` yield for compact_boundary in ClaudeProvider - Remove compacted event handler and getAllDestinations import in poll-loop - Remove compaction integration tests and CompactingProvider helper Closes #2325 differently — the reminder approach is not viable. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../agent-runner/src/integration.test.ts | 107 ------------------ container/agent-runner/src/poll-loop.ts | 22 +--- .../agent-runner/src/providers/claude.ts | 2 +- container/agent-runner/src/providers/types.ts | 10 +- 4 files changed, 3 insertions(+), 138 deletions(-) diff --git a/container/agent-runner/src/integration.test.ts b/container/agent-runner/src/integration.test.ts index 7396cfec4..063d0647f 100644 --- a/container/agent-runner/src/integration.test.ts +++ b/container/agent-runner/src/integration.test.ts @@ -295,115 +295,8 @@ describe('poll loop integration', () => { await loopPromise.catch(() => {}); }); - it('should inject destination reminder after a compacted event', async () => { - // Two destinations — required for the reminder to fire (single-destination - // groups have a fallback path that works without wrapping). - getInboundDb() - .prepare( - `INSERT INTO destinations (name, display_name, type, channel_type, platform_id, agent_group_id) - VALUES ('discord-second', 'Discord Second', 'channel', 'discord', 'chan-2', NULL)`, - ) - .run(); - - insertMessage('m1', { sender: 'Alice', text: 'First message' }, { platformId: 'chan-1', channelType: 'discord' }); - - const provider = new CompactingProvider(); - const controller = new AbortController(); - const loopPromise = runPollLoopWithTimeout(provider as unknown as MockProvider, controller.signal, 2500); - - await waitFor(() => getUndeliveredMessages().length > 0, 2500); - controller.abort(); - - expect(provider.pushes.length).toBeGreaterThanOrEqual(1); - const reminder = provider.pushes.find((p) => p.includes('Context was just compacted')); - expect(reminder).toBeDefined(); - expect(reminder).toContain('2 destinations'); - expect(reminder).toContain('discord-test'); - expect(reminder).toContain('discord-second'); - expect(reminder).toContain(''); - - await loopPromise.catch(() => {}); - }); - - it('should NOT inject destination reminder with a single destination', async () => { - insertMessage('m1', { sender: 'Alice', text: 'First message' }, { platformId: 'chan-1', channelType: 'discord' }); - - const provider = new CompactingProvider(); - const controller = new AbortController(); - const loopPromise = runPollLoopWithTimeout(provider as unknown as MockProvider, controller.signal, 2500); - - await waitFor(() => getUndeliveredMessages().length > 0, 2500); - controller.abort(); - - // Only the original prompt push (if any) — no reminder, since beforeEach - // seeds exactly one destination. - const reminders = provider.pushes.filter((p) => p.includes('Context was just compacted')); - expect(reminders).toHaveLength(0); - - await loopPromise.catch(() => {}); - }); }); -/** - * Provider that emits a single compacted event mid-stream, then returns a - * result. Captures every push() call so tests can assert on the injected - * reminder content. - */ -class CompactingProvider { - readonly supportsNativeSlashCommands = false; - readonly pushes: string[] = []; - - isSessionInvalid(): boolean { - return false; - } - - query(_input: { prompt: string; cwd: string }) { - const pushes = this.pushes; - let ended = false; - let aborted = false; - let resolveWaiter: (() => void) | null = null; - - async function* events() { - yield { type: 'activity' as const }; - yield { type: 'init' as const, continuation: 'compaction-test-session' }; - yield { type: 'activity' as const }; - yield { type: 'compacted' as const, text: 'Context compacted (50,000 tokens compacted).' }; - - // Wait for poll-loop to push the reminder (or end / abort) - await new Promise((resolve) => { - resolveWaiter = resolve; - // Belt-and-braces: don't hang forever if the reminder never arrives - setTimeout(resolve, 200); - }); - - yield { type: 'activity' as const }; - yield { type: 'result' as const, text: 'ack' }; - while (!ended && !aborted) { - await new Promise((resolve) => { - resolveWaiter = resolve; - setTimeout(resolve, 50); - }); - } - } - - return { - push(message: string) { - pushes.push(message); - resolveWaiter?.(); - }, - end() { - ended = true; - resolveWaiter?.(); - }, - abort() { - aborted = true; - resolveWaiter?.(); - }, - events: events(), - }; - } -} - // Helper: run poll loop until aborted or timeout async function runPollLoopWithTimeout(provider: MockProvider, signal: AbortSignal, timeoutMs: number): Promise { return Promise.race([ diff --git a/container/agent-runner/src/poll-loop.ts b/container/agent-runner/src/poll-loop.ts index 0023c17b0..4761b950d 100644 --- a/container/agent-runner/src/poll-loop.ts +++ b/container/agent-runner/src/poll-loop.ts @@ -1,4 +1,4 @@ -import { findByName, getAllDestinations, type DestinationEntry } from './destinations.js'; +import { findByName, type DestinationEntry } from './destinations.js'; import { getPendingMessages, markProcessing, markCompleted, type MessageInRow } from './db/messages-in.js'; import { writeMessageOut } from './db/messages-out.js'; import { getInboundDb, touchHeartbeat, clearStaleProcessingAcks } from './db/connection.js'; @@ -378,23 +378,6 @@ async function processQuery( if (event.text) { dispatchResultText(event.text, routing); } - } else if (event.type === 'compacted') { - // The SDK auto-compacted the conversation. After compaction the - // model often drops the learned `` wrapping - // discipline (the destinations are still in the system prompt, - // but the behavioral pattern is summarized away). Inject a - // reminder back into the live query so the next turn re-anchors - // on the destination model. Only do this when there's >1 - // destination — single-destination groups have a fallback that - // works without wrapping. See nanocoai/nanoclaw#2325. - const destinations = getAllDestinations(); - if (destinations.length > 1) { - const names = destinations.map((d) => d.name).join(', '); - query.push( - `[system] Context was just compacted. Reminder: you have ${destinations.length} destinations (${names}). ` + - `Use blocks to address them. Bare text goes to the scratchpad fallback only.`, - ); - } } } } finally { @@ -421,9 +404,6 @@ function handleEvent(event: ProviderEvent, _routing: RoutingContext): void { case 'progress': log(`Progress: ${event.message}`); break; - case 'compacted': - log(`Compacted: ${event.text}`); - break; } } diff --git a/container/agent-runner/src/providers/claude.ts b/container/agent-runner/src/providers/claude.ts index d8e78ddc5..31be51ae4 100644 --- a/container/agent-runner/src/providers/claude.ts +++ b/container/agent-runner/src/providers/claude.ts @@ -336,7 +336,7 @@ export class ClaudeProvider implements AgentProvider { } else if (message.type === 'system' && (message as { subtype?: string }).subtype === 'compact_boundary') { const meta = (message as { compact_metadata?: { pre_tokens?: number } }).compact_metadata; const detail = meta?.pre_tokens ? ` (${meta.pre_tokens.toLocaleString()} tokens compacted)` : ''; - yield { type: 'compacted', text: `Context compacted${detail}.` }; + yield { type: 'result', text: `Context compacted${detail}.` }; } else if (message.type === 'system' && (message as { subtype?: string }).subtype === 'task_notification') { const tn = message as { summary?: string }; yield { type: 'progress', message: tn.summary || 'Task notification' }; diff --git a/container/agent-runner/src/providers/types.ts b/container/agent-runner/src/providers/types.ts index 5798beac5..a6722a128 100644 --- a/container/agent-runner/src/providers/types.ts +++ b/container/agent-runner/src/providers/types.ts @@ -89,12 +89,4 @@ export type ProviderEvent = * event (tool call, thinking, partial message, anything) so the * poll-loop's idle timer stays honest during long tool runs. */ - | { type: 'activity' } - /** - * The provider's underlying SDK auto-compacted the conversation context. - * The poll-loop reacts by injecting a destination reminder back into - * the live query so the agent doesn't drop `` wrapping - * after compaction. Distinct from `result` so it doesn't mark the turn - * completed or get dispatched as a chat message. See nanocoai/nanoclaw#2325. - */ - | { type: 'compacted'; text: string }; + | { type: 'activity' };