Skip to content

Commit 7116280

Browse files
committed
group compression runs by tool call
1 parent 7e850dc commit 7116280

18 files changed

Lines changed: 795 additions & 124 deletions
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { CompressionBlock, PruneMessagesState } from "../state"
2+
3+
export interface CompressionTarget {
4+
displayId: number
5+
runId: number
6+
topic: string
7+
compressedTokens: number
8+
grouped: boolean
9+
blocks: CompressionBlock[]
10+
}
11+
12+
function byBlockId(a: CompressionBlock, b: CompressionBlock): number {
13+
return a.blockId - b.blockId
14+
}
15+
16+
function buildTarget(blocks: CompressionBlock[]): CompressionTarget {
17+
const ordered = [...blocks].sort(byBlockId)
18+
const first = ordered[0]
19+
if (!first) {
20+
throw new Error("Cannot build compression target from empty block list.")
21+
}
22+
23+
const grouped = first.mode === "message"
24+
return {
25+
displayId: first.blockId,
26+
runId: first.runId,
27+
topic: grouped ? first.batchTopic || first.topic : first.topic,
28+
compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
29+
grouped,
30+
blocks: ordered,
31+
}
32+
}
33+
34+
function groupMessageBlocks(blocks: CompressionBlock[]): CompressionTarget[] {
35+
const grouped = new Map<number, CompressionBlock[]>()
36+
37+
for (const block of blocks) {
38+
const existing = grouped.get(block.runId)
39+
if (existing) {
40+
existing.push(block)
41+
continue
42+
}
43+
grouped.set(block.runId, [block])
44+
}
45+
46+
return Array.from(grouped.values()).map(buildTarget)
47+
}
48+
49+
function splitTargets(blocks: CompressionBlock[]): CompressionTarget[] {
50+
const messageBlocks: CompressionBlock[] = []
51+
const singleBlocks: CompressionBlock[] = []
52+
53+
for (const block of blocks) {
54+
if (block.mode === "message") {
55+
messageBlocks.push(block)
56+
} else {
57+
singleBlocks.push(block)
58+
}
59+
}
60+
61+
const targets = [
62+
...singleBlocks.map((block) => buildTarget([block])),
63+
...groupMessageBlocks(messageBlocks),
64+
]
65+
return targets.sort((a, b) => a.displayId - b.displayId)
66+
}
67+
68+
export function getActiveCompressionTargets(
69+
messagesState: PruneMessagesState,
70+
): CompressionTarget[] {
71+
const activeBlocks = Array.from(messagesState.activeBlockIds)
72+
.map((blockId) => messagesState.blocksById.get(blockId))
73+
.filter((block): block is CompressionBlock => !!block && block.active)
74+
75+
return splitTargets(activeBlocks)
76+
}
77+
78+
export function getRecompressibleCompressionTargets(
79+
messagesState: PruneMessagesState,
80+
availableMessageIds: Set<string>,
81+
): CompressionTarget[] {
82+
const allBlocks = Array.from(messagesState.blocksById.values()).filter((block) => {
83+
return availableMessageIds.has(block.compressMessageId)
84+
})
85+
86+
const messageGroups = new Map<number, CompressionBlock[]>()
87+
const singleTargets: CompressionTarget[] = []
88+
89+
for (const block of allBlocks) {
90+
if (block.mode === "message") {
91+
const existing = messageGroups.get(block.runId)
92+
if (existing) {
93+
existing.push(block)
94+
} else {
95+
messageGroups.set(block.runId, [block])
96+
}
97+
continue
98+
}
99+
100+
if (block.deactivatedByUser && !block.active) {
101+
singleTargets.push(buildTarget([block]))
102+
}
103+
}
104+
105+
for (const blocks of messageGroups.values()) {
106+
if (blocks.some((block) => block.deactivatedByUser && !block.active)) {
107+
singleTargets.push(buildTarget(blocks))
108+
}
109+
}
110+
111+
return singleTargets.sort((a, b) => a.displayId - b.displayId)
112+
}
113+
114+
export function resolveCompressionTarget(
115+
messagesState: PruneMessagesState,
116+
blockId: number,
117+
): CompressionTarget | null {
118+
const block = messagesState.blocksById.get(blockId)
119+
if (!block) {
120+
return null
121+
}
122+
123+
if (block.mode !== "message") {
124+
return buildTarget([block])
125+
}
126+
127+
const blocks = Array.from(messagesState.blocksById.values()).filter(
128+
(candidate) => candidate.mode === "message" && candidate.runId === block.runId,
129+
)
130+
if (blocks.length === 0) {
131+
return null
132+
}
133+
134+
return buildTarget(blocks)
135+
}

lib/commands/decompress.ts

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { getCurrentParams } from "../strategies/utils"
66
import { saveSessionState } from "../state/persistence"
77
import { sendIgnoredMessage } from "../ui/notification"
88
import { formatTokenCount } from "../ui/utils"
9+
import {
10+
getActiveCompressionTargets,
11+
resolveCompressionTarget,
12+
type CompressionTarget,
13+
} from "./compression-targets"
914

1015
export interface DecompressCommandContext {
1116
client: any
@@ -31,13 +36,6 @@ function parseBlockIdArg(arg: string): number | null {
3136
return Number.isInteger(parsed) && parsed > 0 ? parsed : null
3237
}
3338

34-
function getAvailableBlocks(messagesState: PruneMessagesState): CompressionBlock[] {
35-
return Array.from(messagesState.activeBlockIds)
36-
.map((blockId) => messagesState.blocksById.get(blockId))
37-
.filter((block): block is CompressionBlock => !!block && block.active)
38-
.sort((a, b) => a.blockId - b.blockId)
39-
}
40-
4139
function findActiveParentBlockId(
4240
messagesState: PruneMessagesState,
4341
block: CompressionBlock,
@@ -71,6 +69,20 @@ function findActiveParentBlockId(
7169
return null
7270
}
7371

72+
function findActiveAncestorBlockId(
73+
messagesState: PruneMessagesState,
74+
target: CompressionTarget,
75+
): number | null {
76+
for (const block of target.blocks) {
77+
const activeAncestorBlockId = findActiveParentBlockId(messagesState, block)
78+
if (activeAncestorBlockId !== null) {
79+
return activeAncestorBlockId
80+
}
81+
}
82+
83+
return null
84+
}
85+
7486
function snapshotActiveMessages(messagesState: PruneMessagesState): Map<string, number> {
7587
const activeMessages = new Map<string, number>()
7688
for (const [messageId, entry] of messagesState.byMessageId) {
@@ -82,14 +94,17 @@ function snapshotActiveMessages(messagesState: PruneMessagesState): Map<string,
8294
}
8395

8496
function formatDecompressMessage(
85-
targetBlockId: number,
97+
target: CompressionTarget,
8698
restoredMessageCount: number,
8799
restoredTokens: number,
88100
reactivatedBlockIds: number[],
89101
): string {
90102
const lines: string[] = []
91103

92-
lines.push(`Restored compression ${targetBlockId}.`)
104+
lines.push(`Restored compression ${target.displayId}.`)
105+
if (target.runId !== target.displayId || target.grouped) {
106+
lines.push(`Tool call label: Compression #${target.runId}.`)
107+
}
93108
if (reactivatedBlockIds.length > 0) {
94109
const refs = reactivatedBlockIds.map((id) => String(id)).join(", ")
95110
lines.push(`Also restored nested compression(s): ${refs}.`)
@@ -106,22 +121,25 @@ function formatDecompressMessage(
106121
return lines.join("\n")
107122
}
108123

109-
function formatAvailableBlocksMessage(availableBlocks: CompressionBlock[]): string {
124+
function formatAvailableBlocksMessage(availableTargets: CompressionTarget[]): string {
110125
const lines: string[] = []
111126

112127
lines.push("Usage: /dcp decompress <n>")
113128
lines.push("")
114129

115-
if (availableBlocks.length === 0) {
130+
if (availableTargets.length === 0) {
116131
lines.push("No compressions are available to restore.")
117132
return lines.join("\n")
118133
}
119134

120135
lines.push("Available compressions:")
121-
const entries = availableBlocks.map((block) => {
122-
const topic = block.topic.replace(/\s+/g, " ").trim() || "(no topic)"
123-
const label = `${block.blockId} (${formatTokenCount(block.compressedTokens)})`
124-
return { label, topic }
136+
const entries = availableTargets.map((target) => {
137+
const topic = target.topic.replace(/\s+/g, " ").trim() || "(no topic)"
138+
const label = `${target.displayId} (${formatTokenCount(target.compressedTokens)})`
139+
const details = target.grouped
140+
? `Compression #${target.runId} - ${target.blocks.length} messages`
141+
: `Compression #${target.runId}`
142+
return { label, topic: `${details} - ${topic}` }
125143
})
126144

127145
const labelWidth = Math.max(...entries.map((entry) => entry.label.length)) + 4
@@ -153,8 +171,8 @@ export async function handleDecompressCommand(ctx: DecompressCommandContext): Pr
153171
const messagesState = state.prune.messages
154172

155173
if (!targetArg) {
156-
const availableBlocks = getAvailableBlocks(messagesState)
157-
const message = formatAvailableBlocksMessage(availableBlocks)
174+
const availableTargets = getActiveCompressionTargets(messagesState)
175+
const message = formatAvailableBlocksMessage(availableTargets)
158176
await sendIgnoredMessage(client, sessionId, message, params, logger)
159177
return
160178
}
@@ -171,8 +189,8 @@ export async function handleDecompressCommand(ctx: DecompressCommandContext): Pr
171189
return
172190
}
173191

174-
const targetBlock = messagesState.blocksById.get(targetBlockId)
175-
if (!targetBlock) {
192+
const target = resolveCompressionTarget(messagesState, targetBlockId)
193+
if (!target) {
176194
await sendIgnoredMessage(
177195
client,
178196
sessionId,
@@ -183,13 +201,14 @@ export async function handleDecompressCommand(ctx: DecompressCommandContext): Pr
183201
return
184202
}
185203

186-
if (!targetBlock.active) {
187-
const activeAncestorBlockId = findActiveParentBlockId(messagesState, targetBlock)
204+
const activeBlocks = target.blocks.filter((block) => block.active)
205+
if (activeBlocks.length === 0) {
206+
const activeAncestorBlockId = findActiveAncestorBlockId(messagesState, target)
188207
if (activeAncestorBlockId !== null) {
189208
await sendIgnoredMessage(
190209
client,
191210
sessionId,
192-
`Compression ${targetBlockId} is inside compression ${activeAncestorBlockId}. Restore compression ${activeAncestorBlockId} first.`,
211+
`Compression ${target.displayId} is inside compression ${activeAncestorBlockId}. Restore compression ${activeAncestorBlockId} first.`,
193212
params,
194213
logger,
195214
)
@@ -199,7 +218,7 @@ export async function handleDecompressCommand(ctx: DecompressCommandContext): Pr
199218
await sendIgnoredMessage(
200219
client,
201220
sessionId,
202-
`Compression ${targetBlockId} is not active.`,
221+
`Compression ${target.displayId} is not active.`,
203222
params,
204223
logger,
205224
)
@@ -208,11 +227,14 @@ export async function handleDecompressCommand(ctx: DecompressCommandContext): Pr
208227

209228
const activeMessagesBefore = snapshotActiveMessages(messagesState)
210229
const activeBlockIdsBefore = new Set(messagesState.activeBlockIds)
230+
const deactivatedAt = Date.now()
211231

212-
targetBlock.active = false
213-
targetBlock.deactivatedByUser = true
214-
targetBlock.deactivatedAt = Date.now()
215-
targetBlock.deactivatedByBlockId = undefined
232+
for (const block of target.blocks) {
233+
block.active = false
234+
block.deactivatedByUser = true
235+
block.deactivatedAt = deactivatedAt
236+
block.deactivatedByBlockId = undefined
237+
}
216238

217239
syncCompressionBlocks(state, logger, messages)
218240

@@ -236,15 +258,16 @@ export async function handleDecompressCommand(ctx: DecompressCommandContext): Pr
236258
await saveSessionState(state, logger)
237259

238260
const message = formatDecompressMessage(
239-
targetBlockId,
261+
target,
240262
restoredMessageCount,
241263
restoredTokens,
242264
reactivatedBlockIds,
243265
)
244266
await sendIgnoredMessage(client, sessionId, message, params, logger)
245267

246268
logger.info("Decompress command completed", {
247-
targetBlockId,
269+
targetBlockId: target.displayId,
270+
targetRunId: target.runId,
248271
restoredMessageCount,
249272
restoredTokens,
250273
reactivatedBlockIds,

0 commit comments

Comments
 (0)