Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/compress/search.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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 {
Expand Down
39 changes: 20 additions & 19 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
consumeCompressionStart,
resolveCompressionDuration,
} from "./compress/timing"
import { filterProcessableMessages } from "./messages/shape"
import { filterMessages, filterMessagesInPlace } from "./messages/shape"
import {
applyPendingManualTrigger,
handleContextCommand,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions lib/messages/inject/subagent-results.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,7 +13,7 @@ async function fetchSubAgentMessages(client: any, sessionId: string): Promise<Wi
path: { id: sessionId },
})

return filterProcessableMessages(response?.data || response)
return filterMessages(response?.data || response)
}

export const injectExtendedSubAgentResults = async (
Expand Down
19 changes: 18 additions & 1 deletion lib/messages/shape.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,27 @@ export function isMessageWithInfo(message: unknown): message is WithParts {
)
}

export function filterProcessableMessages(messages: unknown): WithParts[] {
export function filterMessages(messages: unknown): WithParts[] {
if (!Array.isArray(messages)) {
return []
}

return messages.filter(isMessageWithInfo)
}

export function filterMessagesInPlace(messages: unknown): WithParts[] {
if (!Array.isArray(messages)) {
return []
}

let writeIndex = 0

for (const message of messages) {
if (isMessageWithInfo(message)) {
messages[writeIndex++] = message
}
}

messages.length = writeIndex
return messages as WithParts[]
}
4 changes: 2 additions & 2 deletions tests/hooks-permission.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ test("chat message transform strips hallucinated tags even when compress is deni
assert.equal((output.messages[0]?.parts[0] as any).text, "alpha omega")
})

test("chat message transform ignores messages without info instead of crashing", async () => {
test("chat message transform drops messages without info instead of crashing", async () => {
const state = createSessionState()
const logger = new Logger(false)
const config = buildConfig("deny")
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading