Skip to content

Commit 577831e

Browse files
feat: add compression time to /dcp stats
refactor refactor: natural time display fix: format check fix: stabilize compression timing correlation refactor: simplify compression timing fix: restore compression timing durability fix: attach compression timing to blocks fix: make compression timing robust
1 parent 2c28051 commit 577831e

15 files changed

Lines changed: 803 additions & 7 deletions

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/compression-targets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface CompressionTarget {
55
runId: number
66
topic: string
77
compressedTokens: number
8+
durationMs: number
89
grouped: boolean
910
blocks: CompressionBlock[]
1011
}
@@ -26,6 +27,7 @@ function buildTarget(blocks: CompressionBlock[]): CompressionTarget {
2627
runId: first.runId,
2728
topic: grouped ? first.batchTopic || first.topic : first.topic,
2829
compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
30+
durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
2931
grouped,
3032
blocks: ordered,
3133
}

lib/commands/stats.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { sendIgnoredMessage } from "../ui/notification"
99
import { formatTokenCount } from "../ui/utils"
1010
import { loadAllSessionStats, type AggregatedStats } from "../state/persistence"
1111
import { getCurrentParams } from "../token-utils"
12+
import { getActiveCompressionTargets } from "./compression-targets"
1213

1314
export interface StatsCommandContext {
1415
client: any
@@ -22,6 +23,7 @@ function formatStatsMessage(
2223
sessionTokens: number,
2324
sessionTools: number,
2425
sessionMessages: number,
26+
sessionDurationMs: number,
2527
allTime: AggregatedStats,
2628
): string {
2729
const lines: string[] = []
@@ -35,6 +37,7 @@ function formatStatsMessage(
3537
lines.push(` Tokens pruned: ~${formatTokenCount(sessionTokens)}`)
3638
lines.push(` Tools pruned: ${sessionTools}`)
3739
lines.push(` Messages pruned: ${sessionMessages}`)
40+
lines.push(` Compression time: ${formatCompressionTime(sessionDurationMs)}`)
3841
lines.push("")
3942
lines.push("All-time:")
4043
lines.push("─".repeat(60))
@@ -46,11 +49,38 @@ function formatStatsMessage(
4649
return lines.join("\n")
4750
}
4851

52+
function formatCompressionTime(ms: number): string {
53+
const safeMs = Math.max(0, Math.round(ms))
54+
if (safeMs < 1000) {
55+
return `${safeMs} ms`
56+
}
57+
58+
const totalSeconds = safeMs / 1000
59+
if (totalSeconds < 60) {
60+
return `${totalSeconds.toFixed(1)} s`
61+
}
62+
63+
const wholeSeconds = Math.floor(totalSeconds)
64+
const hours = Math.floor(wholeSeconds / 3600)
65+
const minutes = Math.floor((wholeSeconds % 3600) / 60)
66+
const seconds = wholeSeconds % 60
67+
68+
if (hours > 0) {
69+
return `${hours}h ${minutes}m ${seconds}s`
70+
}
71+
72+
return `${minutes}m ${seconds}s`
73+
}
74+
4975
export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void> {
5076
const { client, state, logger, sessionId, messages } = ctx
5177

5278
// Session stats from in-memory state
5379
const sessionTokens = state.stats.totalPruneTokens
80+
const sessionDurationMs = getActiveCompressionTargets(state.prune.messages).reduce(
81+
(total, target) => total + target.durationMs,
82+
0,
83+
)
5484

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

75-
const message = formatStatsMessage(sessionTokens, sessionTools, sessionMessages, allTime)
105+
const message = formatStatsMessage(
106+
sessionTokens,
107+
sessionTools,
108+
sessionMessages,
109+
sessionDurationMs,
110+
allTime,
111+
)
76112

77113
const params = getCurrentParams(state, messages, logger)
78114
await sendIgnoredMessage(client, sessionId, message, params, logger)
@@ -81,6 +117,7 @@ export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void
81117
sessionTokens,
82118
sessionTools,
83119
sessionMessages,
120+
sessionDurationMs,
84121
allTimeTokens: allTime.totalTokens,
85122
allTimeTools: allTime.totalTools,
86123
allTimeMessages: allTime.totalMessages,

lib/compress/message.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export function createCompressMessageTool(ctx: ToolContext): ReturnType<typeof t
4848
async execute(args, toolCtx) {
4949
const input = args as CompressMessageToolArgs
5050
validateArgs(input)
51+
const callId =
52+
typeof (toolCtx as unknown as { callID?: unknown }).callID === "string"
53+
? (toolCtx as unknown as { callID: string }).callID
54+
: undefined
5155

5256
const { rawMessages, searchContext } = await prepareSession(
5357
ctx,
@@ -107,7 +111,9 @@ export function createCompressMessageTool(ctx: ToolContext): ReturnType<typeof t
107111
mode: "message",
108112
runId,
109113
compressMessageId: toolCtx.messageID,
114+
compressCallId: callId,
110115
summaryTokens,
116+
durationMs: 0,
111117
},
112118
plan.selection,
113119
plan.anchorMessageId,

lib/compress/range.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export function createCompressRangeTool(ctx: ToolContext): ReturnType<typeof too
5959
async execute(args, toolCtx) {
6060
const input = args as CompressRangeToolArgs
6161
validateArgs(input)
62+
const callId =
63+
typeof (toolCtx as unknown as { callID?: unknown }).callID === "string"
64+
? (toolCtx as unknown as { callID: string }).callID
65+
: undefined
6266

6367
const { rawMessages, searchContext } = await prepareSession(
6468
ctx,
@@ -148,7 +152,9 @@ export function createCompressRangeTool(ctx: ToolContext): ReturnType<typeof too
148152
mode: "range",
149153
runId,
150154
compressMessageId: toolCtx.messageID,
155+
compressCallId: callId,
151156
summaryTokens,
157+
durationMs: 0,
152158
},
153159
preparedPlan.selection,
154160
preparedPlan.anchorMessageId,

lib/compress/state.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,41 @@ export function allocateRunId(state: SessionState): number {
2626
return next
2727
}
2828

29+
export function recordCompressionDuration(
30+
state: SessionState,
31+
callId: string,
32+
durationMs: number,
33+
): void {
34+
state.compressionDurations.set(callId, durationMs)
35+
}
36+
37+
export function attachCompressionDuration(
38+
state: SessionState,
39+
callId: string,
40+
messageId: string,
41+
): number {
42+
const durationMs = state.compressionDurations.get(callId)
43+
if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) {
44+
return 0
45+
}
46+
47+
state.compressionDurations.delete(callId)
48+
49+
let updates = 0
50+
for (const block of state.prune.messages.blocksById.values()) {
51+
const matchesCall = block.compressCallId === callId
52+
const matchesMessage = !block.compressCallId && block.compressMessageId === messageId
53+
if (!matchesCall && !matchesMessage) {
54+
continue
55+
}
56+
57+
block.durationMs = durationMs
58+
updates++
59+
}
60+
61+
return updates
62+
}
63+
2964
export function wrapCompressedSummary(blockId: number, summary: string): string {
3065
const header = COMPRESSED_BLOCK_HEADER
3166
const footer = formatMessageIdTag(formatBlockRef(blockId))
@@ -93,13 +128,15 @@ export function applyCompressionState(
93128
deactivatedByUser: false,
94129
compressedTokens: 0,
95130
summaryTokens: input.summaryTokens,
131+
durationMs: input.durationMs,
96132
mode: input.mode,
97133
topic: input.topic,
98134
batchTopic: input.batchTopic,
99135
startId: input.startId,
100136
endId: input.endId,
101137
anchorMessageId,
102138
compressMessageId: input.compressMessageId,
139+
compressCallId: input.compressCallId,
103140
includedBlockIds: included,
104141
consumedBlockIds: consumed,
105142
parentBlockIds: [],

lib/compress/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,7 @@ export interface CompressionStateInput {
102102
mode: CompressionMode
103103
runId: number
104104
compressMessageId: string
105+
compressCallId?: string
105106
summaryTokens: number
107+
durationMs: number
106108
}

lib/hooks.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from "./messages"
1717
import { renderSystemPrompt, type PromptStore } from "./prompts"
1818
import { buildProtectedToolsExtension } from "./prompts/extensions/system"
19+
import { attachCompressionDuration, recordCompressionDuration } from "./compress/state"
1920
import {
2021
applyPendingManualTrigger,
2122
handleContextCommand,
@@ -29,7 +30,7 @@ import {
2930
} from "./commands"
3031
import { type HostPermissionSnapshot } from "./host-permissions"
3132
import { compressPermission, syncCompressPermissionState } from "./compress-permission"
32-
import { checkSession, ensureSessionInitialized, syncToolCache } from "./state"
33+
import { checkSession, ensureSessionInitialized, saveSessionState, syncToolCache } from "./state"
3334
import { cacheSystemPromptTokens } from "./ui/utils"
3435

3536
const INTERNAL_AGENT_SIGNATURES = [
@@ -266,6 +267,135 @@ export function createTextCompleteHandler() {
266267
}
267268
}
268269

270+
export function createEventHandler(state: SessionState, logger: Logger) {
271+
return async (input: { event: any }) => {
272+
const eventTime =
273+
typeof input.event?.time === "number" && Number.isFinite(input.event.time)
274+
? input.event.time
275+
: typeof input.event?.properties?.time === "number" &&
276+
Number.isFinite(input.event.properties.time)
277+
? input.event.properties.time
278+
: undefined
279+
280+
if (input.event.type !== "message.part.updated") {
281+
return
282+
}
283+
284+
const part = input.event.properties?.part
285+
if (part?.type !== "tool" || part.tool !== "compress") {
286+
return
287+
}
288+
289+
if (part.state.status === "pending") {
290+
if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
291+
return
292+
}
293+
294+
if (state.compressionStarts.has(part.callID)) {
295+
return
296+
}
297+
298+
const startedAt = eventTime ?? Date.now()
299+
state.compressionStarts.set(part.callID, {
300+
messageId: part.messageID,
301+
startedAt,
302+
})
303+
logger.debug("Recorded compression start", {
304+
callID: part.callID,
305+
messageID: part.messageID,
306+
startedAt,
307+
})
308+
return
309+
}
310+
311+
if (part.state.status === "running") {
312+
if (typeof part.callID !== "string") {
313+
return
314+
}
315+
316+
const start = state.compressionStarts.get(part.callID)
317+
if (!start) {
318+
return
319+
}
320+
321+
const runningAt =
322+
typeof part.state.time?.start === "number" && Number.isFinite(part.state.time.start)
323+
? part.state.time.start
324+
: eventTime
325+
if (typeof runningAt !== "number") {
326+
return
327+
}
328+
329+
state.compressionStarts.delete(part.callID)
330+
const durationMs = Math.max(0, runningAt - start.startedAt)
331+
recordCompressionDuration(state, part.callID, durationMs)
332+
333+
logger.info("Recorded compression time", {
334+
callID: part.callID,
335+
messageID: start.messageId,
336+
durationMs,
337+
})
338+
return
339+
}
340+
341+
if (part.state.status === "completed") {
342+
if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
343+
return
344+
}
345+
346+
if (!state.compressionDurations.has(part.callID)) {
347+
const start = state.compressionStarts.get(part.callID)
348+
const runningAt =
349+
typeof part.state.time?.start === "number" &&
350+
Number.isFinite(part.state.time.start)
351+
? part.state.time.start
352+
: eventTime
353+
354+
if (start && typeof runningAt === "number") {
355+
state.compressionStarts.delete(part.callID)
356+
const durationMs = Math.max(0, runningAt - start.startedAt)
357+
recordCompressionDuration(state, part.callID, durationMs)
358+
} else {
359+
const toolStart = part.state.time?.start
360+
const toolEnd = part.state.time?.end
361+
if (
362+
typeof toolStart === "number" &&
363+
Number.isFinite(toolStart) &&
364+
typeof toolEnd === "number" &&
365+
Number.isFinite(toolEnd)
366+
) {
367+
const durationMs = Math.max(0, toolEnd - toolStart)
368+
recordCompressionDuration(state, part.callID, durationMs)
369+
}
370+
}
371+
}
372+
373+
const updates = attachCompressionDuration(state, part.callID, part.messageID)
374+
if (updates === 0) {
375+
return
376+
}
377+
378+
logger.info("Attached compression time to blocks", {
379+
callID: part.callID,
380+
messageID: part.messageID,
381+
blocks: updates,
382+
})
383+
384+
saveSessionState(state, logger).catch((error) => {
385+
logger.warn("Failed to persist compression time update", {
386+
error: error instanceof Error ? error.message : String(error),
387+
})
388+
})
389+
return
390+
}
391+
392+
if (typeof part.callID === "string") {
393+
state.compressionStarts.delete(part.callID)
394+
state.compressionDurations.delete(part.callID)
395+
}
396+
}
397+
}
398+
269399
export function createChatMessageHandler(
270400
state: SessionState,
271401
logger: Logger,

lib/state/state.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SessionState, ToolParameterEntry, WithParts } from "./types"
1+
import type { CompressionStart, SessionState, ToolParameterEntry, WithParts } from "./types"
22
import type { Logger } from "../logger"
33
import { loadSessionState, saveSessionState } from "./persistence"
44
import {
@@ -81,6 +81,8 @@ export function createSessionState(): SessionState {
8181
pruneTokenCounter: 0,
8282
totalPruneTokens: 0,
8383
},
84+
compressionStarts: new Map<string, CompressionStart>(),
85+
compressionDurations: new Map<string, number>(),
8486
toolParameters: new Map<string, ToolParameterEntry>(),
8587
subAgentResultCache: new Map<string, string>(),
8688
toolIdList: [],

0 commit comments

Comments
 (0)