From 6cfd0aba6ccf8eba411308be9b233f45d9d6f5f6 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sun, 5 Apr 2026 14:08:05 -0400 Subject: [PATCH] fix: filter chat messages in place --- lib/compress/search.ts | 4 +-- lib/hooks.ts | 39 +++++++++++++------------ lib/messages/inject/subagent-results.ts | 4 +-- lib/messages/shape.ts | 19 +++++++++++- tests/hooks-permission.test.ts | 4 +-- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/lib/compress/search.ts b/lib/compress/search.ts index 718bf19e..2cc16cf6 100644 --- a/lib/compress/search.ts +++ b/lib/compress/search.ts @@ -1,7 +1,7 @@ import type { SessionState, WithParts } from "../state" import { formatBlockRef, parseBoundaryId } from "../message-ids" import { isIgnoredUserMessage } from "../messages/query" -import { filterProcessableMessages } from "../messages/shape" +import { filterMessages } from "../messages/shape" import { countAllMessageTokens } from "../token-utils" import type { BoundaryReference, SearchContext, SelectionResolution } from "./types" @@ -10,7 +10,7 @@ export async function fetchSessionMessages(client: any, sessionId: string): Prom path: { id: sessionId }, }) - return filterProcessableMessages(response?.data || response) + return filterMessages(response?.data || response) } export function buildSearchContext(state: SessionState, rawMessages: WithParts[]): SearchContext { diff --git a/lib/hooks.ts b/lib/hooks.ts index 1a38ac2d..17eaa337 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -22,7 +22,7 @@ import { consumeCompressionStart, resolveCompressionDuration, } from "./compress/timing" -import { filterProcessableMessages } from "./messages/shape" +import { filterMessages, filterMessagesInPlace } from "./messages/shape" import { applyPendingManualTrigger, handleContextCommand, @@ -104,49 +104,50 @@ export function createChatMessageTransformHandler( hostPermissions: HostPermissionSnapshot, ) { return async (input: {}, output: { messages: WithParts[] }) => { - const messages = filterProcessableMessages(output.messages) - if (messages.length !== output.messages.length) { + const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0 + const messages = filterMessagesInPlace(output.messages) + if (messages.length !== receivedMessages) { logger.warn("Skipping messages with unexpected shape during chat transform", { - received: output.messages.length, + received: receivedMessages, usable: messages.length, }) } - await checkSession(client, state, logger, messages, config.manualMode.enabled) + await checkSession(client, state, logger, output.messages, config.manualMode.enabled) - syncCompressPermissionState(state, config, hostPermissions, messages) + syncCompressPermissionState(state, config, hostPermissions, output.messages) if (state.isSubAgent && !config.experimental.allowSubAgents) { return } stripHallucinations(output.messages) - cacheSystemPromptTokens(state, messages) - assignMessageRefs(state, messages) - syncCompressionBlocks(state, logger, messages) - syncToolCache(state, config, logger, messages) - buildToolIdList(state, messages) - prune(state, logger, config, messages) + cacheSystemPromptTokens(state, output.messages) + assignMessageRefs(state, output.messages) + syncCompressionBlocks(state, logger, output.messages) + syncToolCache(state, config, logger, output.messages) + buildToolIdList(state, output.messages) + prune(state, logger, config, output.messages) await injectExtendedSubAgentResults( client, state, logger, - messages, + output.messages, config.experimental.allowSubAgents, ) - const compressionPriorities = buildPriorityMap(config, state, messages) + const compressionPriorities = buildPriorityMap(config, state, output.messages) prompts.reload() injectCompressNudges( state, config, logger, - messages, + output.messages, prompts.getRuntimePrompts(), compressionPriorities, ) - injectMessageIds(state, config, messages, compressionPriorities) - applyPendingManualTrigger(state, messages, logger) - stripStaleMetadata(messages) + injectMessageIds(state, config, output.messages, compressionPriorities) + applyPendingManualTrigger(state, output.messages, logger) + stripStaleMetadata(output.messages) if (state.sessionId) { await logger.saveContext(state.sessionId, output.messages) @@ -174,7 +175,7 @@ export function createCommandExecuteHandler( const messagesResponse = await client.session.messages({ path: { id: input.sessionID }, }) - const messages = filterProcessableMessages(messagesResponse.data || messagesResponse) + const messages = filterMessages(messagesResponse.data || messagesResponse) await ensureSessionInitialized( client, diff --git a/lib/messages/inject/subagent-results.ts b/lib/messages/inject/subagent-results.ts index f3c87d7a..8ca3d1d5 100644 --- a/lib/messages/inject/subagent-results.ts +++ b/lib/messages/inject/subagent-results.ts @@ -1,6 +1,6 @@ import type { Logger } from "../../logger" import type { SessionState, WithParts } from "../../state" -import { filterProcessableMessages } from "../shape" +import { filterMessages } from "../shape" import { buildSubagentResultText, getSubAgentId, @@ -13,7 +13,7 @@ async function fetchSubAgentMessages(client: any, sessionId: string): Promise { +test("chat message transform drops messages without info instead of crashing", async () => { const state = createSessionState() const logger = new Logger(false) const config = buildConfig("deny") @@ -148,7 +148,7 @@ test("chat message transform ignores messages without info instead of crashing", await handler({}, output as any) assert.equal(state.sessionId, null) - assert.equal(output.messages.length, 1) + assert.equal(output.messages.length, 0) }) test("command execute exits after effective permission resolves to deny", async () => {