Skip to content

Commit dd79375

Browse files
feat: add compression time to /dcp stats
1 parent 2c28051 commit dd79375

8 files changed

Lines changed: 177 additions & 3 deletions

File tree

index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createChatMessageHandler,
1414
createChatMessageTransformHandler,
1515
createCommandExecuteHandler,
16+
createEventHandler,
1617
createSystemPromptHandler,
1718
createTextCompleteHandler,
1819
} from "./lib/hooks"
@@ -68,6 +69,7 @@ const plugin: Plugin = (async (ctx) => {
6869
) as any,
6970
"chat.message": createChatMessageHandler(state, logger, config, hostPermissions),
7071
"experimental.text.complete": createTextCompleteHandler(),
72+
event: createEventHandler(state, logger),
7173
"command.execute.before": createCommandExecuteHandler(
7274
ctx.client,
7375
state,

lib/commands/stats.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function formatStatsMessage(
2222
sessionTokens: number,
2323
sessionTools: number,
2424
sessionMessages: number,
25+
sessionCompressionTimeMs: number,
2526
allTime: AggregatedStats,
2627
): string {
2728
const lines: string[] = []
@@ -35,6 +36,7 @@ function formatStatsMessage(
3536
lines.push(` Tokens pruned: ~${formatTokenCount(sessionTokens)}`)
3637
lines.push(` Tools pruned: ${sessionTools}`)
3738
lines.push(` Messages pruned: ${sessionMessages}`)
39+
lines.push(` LLM summary time: ${formatCompressionTime(sessionCompressionTimeMs)}`)
3840
lines.push("")
3941
lines.push("All-time:")
4042
lines.push("─".repeat(60))
@@ -46,11 +48,21 @@ function formatStatsMessage(
4648
return lines.join("\n")
4749
}
4850

51+
function formatCompressionTime(ms: number): string {
52+
if (ms < 1000) {
53+
return `${ms} ms`
54+
}
55+
56+
const seconds = ms / 1000
57+
return `${seconds.toFixed(seconds < 10 ? 2 : 1)} s`
58+
}
59+
4960
export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void> {
5061
const { client, state, logger, sessionId, messages } = ctx
5162

5263
// Session stats from in-memory state
5364
const sessionTokens = state.stats.totalPruneTokens
65+
const sessionCompressionTimeMs = state.stats.compressionTimeMs
5466

5567
const prunedToolIds = new Set<string>(state.prune.tools.keys())
5668
for (const block of state.prune.messages.blocksById.values()) {
@@ -72,7 +84,13 @@ export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void
7284
// All-time stats from storage files
7385
const allTime = await loadAllSessionStats(logger)
7486

75-
const message = formatStatsMessage(sessionTokens, sessionTools, sessionMessages, allTime)
87+
const message = formatStatsMessage(
88+
sessionTokens,
89+
sessionTools,
90+
sessionMessages,
91+
sessionCompressionTimeMs,
92+
allTime,
93+
)
7694

7795
const params = getCurrentParams(state, messages, logger)
7896
await sendIgnoredMessage(client, sessionId, message, params, logger)
@@ -81,6 +99,7 @@ export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void
8199
sessionTokens,
82100
sessionTools,
83101
sessionMessages,
102+
sessionCompressionTimeMs,
84103
allTimeTokens: allTime.totalTokens,
85104
allTimeTools: allTime.totalTools,
86105
allTimeMessages: allTime.totalMessages,

lib/compress/message.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export function createCompressMessageTool(ctx: ToolContext): ReturnType<typeof t
4848
async execute(args, toolCtx) {
4949
const input = args as CompressMessageToolArgs
5050
validateArgs(input)
51-
5251
const { rawMessages, searchContext } = await prepareSession(
5352
ctx,
5453
toolCtx,

lib/compress/range.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export function createCompressRangeTool(ctx: ToolContext): ReturnType<typeof too
5959
async execute(args, toolCtx) {
6060
const input = args as CompressRangeToolArgs
6161
validateArgs(input)
62-
6362
const { rawMessages, searchContext } = await prepareSession(
6463
ctx,
6564
toolCtx,

lib/hooks.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,56 @@ export function createTextCompleteHandler() {
266266
}
267267
}
268268

269+
export function createEventHandler(state: SessionState, logger: Logger) {
270+
return async (input: { event: any }) => {
271+
if (!state.sessionId) {
272+
return
273+
}
274+
275+
if (input.event.type !== "message.part.updated") {
276+
return
277+
}
278+
279+
const { sessionID, part, time } = input.event.properties
280+
if (sessionID !== state.sessionId || part.type !== "tool" || part.tool !== "compress") {
281+
return
282+
}
283+
284+
if (part.state.status === "pending") {
285+
if (state.compressionStarts.has(part.callID)) {
286+
return
287+
}
288+
289+
state.compressionStarts.set(part.callID, time)
290+
logger.debug("Recorded compress input start", {
291+
callID: part.callID,
292+
startedAt: time,
293+
})
294+
return
295+
}
296+
297+
if (part.state.status === "running") {
298+
const startedAt = state.compressionStarts.get(part.callID)
299+
if (startedAt === undefined) {
300+
return
301+
}
302+
303+
state.compressionStarts.delete(part.callID)
304+
const durationMs = Math.max(0, time - startedAt)
305+
state.stats.compressionTimeMs += durationMs
306+
307+
logger.info("Recorded compress input generation time", {
308+
callID: part.callID,
309+
durationMs,
310+
totalDurationMs: state.stats.compressionTimeMs,
311+
})
312+
return
313+
}
314+
315+
state.compressionStarts.delete(part.callID)
316+
}
317+
}
318+
269319
export function createChatMessageHandler(
270320
state: SessionState,
271321
logger: Logger,

lib/state/state.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ export function createSessionState(): SessionState {
8080
stats: {
8181
pruneTokenCounter: 0,
8282
totalPruneTokens: 0,
83+
compressionTimeMs: 0,
8384
},
85+
compressionStarts: new Map<string, number>(),
8486
toolParameters: new Map<string, ToolParameterEntry>(),
8587
subAgentResultCache: new Map<string, string>(),
8688
toolIdList: [],
@@ -115,7 +117,9 @@ export function resetSessionState(state: SessionState): void {
115117
state.stats = {
116118
pruneTokenCounter: 0,
117119
totalPruneTokens: 0,
120+
compressionTimeMs: 0,
118121
}
122+
state.compressionStarts.clear()
119123
state.toolParameters.clear()
120124
state.subAgentResultCache.clear()
121125
state.toolIdList = []
@@ -176,5 +180,6 @@ export async function ensureSessionInitialized(
176180
state.stats = {
177181
pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
178182
totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
183+
compressionTimeMs: persisted.stats?.compressionTimeMs || 0,
179184
}
180185
}

lib/state/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface ToolParameterEntry {
1919
export interface SessionStats {
2020
pruneTokenCounter: number
2121
totalPruneTokens: number
22+
compressionTimeMs: number
2223
}
2324

2425
export interface PrunedMessageEntry {
@@ -96,6 +97,7 @@ export interface SessionState {
9697
prune: Prune
9798
nudges: Nudges
9899
stats: SessionStats
100+
compressionStarts: Map<string, number>
99101
toolParameters: Map<string, ToolParameterEntry>
100102
subAgentResultCache: Map<string, string>
101103
toolIdList: string[]

tests/hooks-permission.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
createChatMessageHandler,
66
createChatMessageTransformHandler,
77
createCommandExecuteHandler,
8+
createEventHandler,
89
createTextCompleteHandler,
910
} from "../lib/hooks"
1011
import { Logger } from "../lib/logger"
@@ -152,3 +153,100 @@ test("text complete strips hallucinated metadata tags", async () => {
152153

153154
assert.equal(output.text, "alpha omega")
154155
})
156+
157+
test("event hook records compress input generation duration", async () => {
158+
const state = createSessionState()
159+
state.sessionId = "session-1"
160+
const handler = createEventHandler(state, new Logger(false))
161+
162+
await handler({
163+
event: {
164+
type: "message.part.updated",
165+
properties: {
166+
sessionID: "session-1",
167+
time: 100,
168+
part: {
169+
type: "tool",
170+
tool: "compress",
171+
callID: "call-1",
172+
state: {
173+
status: "pending",
174+
input: {},
175+
raw: "",
176+
},
177+
},
178+
},
179+
},
180+
})
181+
182+
await handler({
183+
event: {
184+
type: "message.part.updated",
185+
properties: {
186+
sessionID: "session-1",
187+
time: 325,
188+
part: {
189+
type: "tool",
190+
tool: "compress",
191+
callID: "call-1",
192+
state: {
193+
status: "running",
194+
input: { topic: "x" },
195+
time: { start: 325 },
196+
},
197+
},
198+
},
199+
},
200+
})
201+
202+
assert.equal(state.stats.compressionTimeMs, 225)
203+
assert.equal(state.compressionStarts.has("call-1"), false)
204+
})
205+
206+
test("event hook ignores non-compress tool parts", async () => {
207+
const state = createSessionState()
208+
state.sessionId = "session-1"
209+
const handler = createEventHandler(state, new Logger(false))
210+
211+
await handler({
212+
event: {
213+
type: "message.part.updated",
214+
properties: {
215+
sessionID: "session-1",
216+
time: 120,
217+
part: {
218+
type: "tool",
219+
tool: "bash",
220+
callID: "call-2",
221+
state: {
222+
status: "pending",
223+
input: {},
224+
raw: "",
225+
},
226+
},
227+
},
228+
},
229+
})
230+
231+
await handler({
232+
event: {
233+
type: "message.part.updated",
234+
properties: {
235+
sessionID: "session-1",
236+
time: 220,
237+
part: {
238+
type: "tool",
239+
tool: "bash",
240+
callID: "call-2",
241+
state: {
242+
status: "running",
243+
input: {},
244+
time: { start: 220 },
245+
},
246+
},
247+
},
248+
},
249+
})
250+
251+
assert.equal(state.stats.compressionTimeMs, 0)
252+
})

0 commit comments

Comments
 (0)