π File detail
services/compact/grouping.ts
π§© .tsπ 64 linesπΎ 2,794 bytesπ text
β Back to All Filesπ― Use case
This file lives under βservices/β, which covers long-lived services (LSP, MCP, OAuth, tool execution, memory, compaction, voice, settings sync, β¦). On the API surface it exposes groupMessagesByApiRound β mainly functions, hooks, or classes. It composes internal code from types (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import type { Message } from '../../types/message.js' /** * Groups messages at API-round boundaries: one group per API round-trip. * A boundary fires when a NEW assistant response begins (different
π€ Exports (heuristic)
groupMessagesByApiRound
π₯οΈ Source preview
import type { Message } from '../../types/message.js'
/**
* Groups messages at API-round boundaries: one group per API round-trip.
* A boundary fires when a NEW assistant response begins (different
* message.id from the prior assistant). For well-formed conversations
* this is an API-safe split point β the API contract requires every
* tool_use to be resolved before the next assistant turn, so pairing
* validity falls out of the assistant-id boundary. For malformed inputs
* (dangling tool_use after resume/truncation) the fork's
* ensureToolResultPairing repairs the split at API time.
*
* Replaces the prior human-turn grouping (boundaries only at real user
* prompts) with finer-grained API-round grouping, allowing reactive
* compact to operate on single-prompt agentic sessions (SDK/CCR/eval
* callers) where the entire workload is one human turn.
*
* Extracted to its own file to break the compact.ts β compactMessages.ts
* cycle (CC-1180) β the cycle shifted module-init order enough to surface
* a latent ws CJS/ESM resolution race in CI shard-2.
*/
export function groupMessagesByApiRound(messages: Message[]): Message[][] {
const groups: Message[][] = []
let current: Message[] = []
// message.id of the most recently seen assistant. This is the sole
// boundary gate: streaming chunks from the same API response share an
// id, so boundaries only fire at the start of a genuinely new round.
// normalizeMessages yields one AssistantMessage per content block, and
// StreamingToolExecutor interleaves tool_results between chunks live
// (yield order, not concat order β see query.ts:613). The id check
// correctly keeps `[tu_A(id=X), result_A, tu_B(id=X)]` in one group.
let lastAssistantId: string | undefined
// In a well-formed conversation the API contract guarantees every
// tool_use is resolved before the next assistant turn, so lastAssistantId
// alone is a sufficient boundary gate. Tracking unresolved tool_use IDs
// would only do work when the conversation is malformed (dangling tool_use
// after resume-from-partial-batch or max_tokens truncation) β and in that
// case it pins the gate shut forever, merging all subsequent rounds into
// one group. We let those boundaries fire; the summarizer fork's own
// ensureToolResultPairing at claude.ts:1136 repairs the dangling tu at
// API time.
for (const msg of messages) {
if (
msg.type === 'assistant' &&
msg.message.id !== lastAssistantId &&
current.length > 0
) {
groups.push(current)
current = [msg]
} else {
current.push(msg)
}
if (msg.type === 'assistant') {
lastAssistantId = msg.message.id
}
}
if (current.length > 0) {
groups.push(current)
}
return groups
}