Skip to content

Commit ca84e89

Browse files
fix: make compression timing robust
1 parent ee50658 commit ca84e89

File tree

3 files changed

+111
-8
lines changed

3 files changed

+111
-8
lines changed

lib/commands/stats.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,20 @@ function formatStatsMessage(
5050
}
5151

5252
function formatCompressionTime(ms: number): string {
53-
const totalSeconds = Math.max(0, Math.round(ms / 1000))
53+
const safeMs = Math.max(0, Math.round(ms))
54+
if (safeMs < 1000) {
55+
return `${safeMs} ms`
56+
}
57+
58+
const totalSeconds = safeMs / 1000
5459
if (totalSeconds < 60) {
55-
return `${totalSeconds} s`
60+
return `${totalSeconds.toFixed(1)} s`
5661
}
5762

58-
const hours = Math.floor(totalSeconds / 3600)
59-
const minutes = Math.floor((totalSeconds % 3600) / 60)
60-
const seconds = totalSeconds % 60
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
6167

6268
if (hours > 0) {
6369
return `${hours}h ${minutes}m ${seconds}s`

lib/hooks.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,14 @@ export function createTextCompleteHandler() {
269269

270270
export function createEventHandler(state: SessionState, logger: Logger) {
271271
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+
272280
if (input.event.type !== "message.part.updated") {
273281
return
274282
}
@@ -287,7 +295,7 @@ export function createEventHandler(state: SessionState, logger: Logger) {
287295
return
288296
}
289297

290-
const startedAt = Date.now()
298+
const startedAt = eventTime ?? Date.now()
291299
state.compressionStarts.set(part.callID, {
292300
messageId: part.messageID,
293301
startedAt,
@@ -310,8 +318,11 @@ export function createEventHandler(state: SessionState, logger: Logger) {
310318
return
311319
}
312320

313-
const runningAt = part.state.time?.start
314-
if (typeof runningAt !== "number" || !Number.isFinite(runningAt)) {
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") {
315326
return
316327
}
317328

@@ -332,6 +343,33 @@ export function createEventHandler(state: SessionState, logger: Logger) {
332343
return
333344
}
334345

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+
335373
const updates = attachCompressionDuration(state, part.callID, part.messageID)
336374
if (updates === 0) {
337375
return

tests/hooks-permission.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,65 @@ test("event hook attaches durations to matching blocks by call id", async () =>
403403
assert.equal(state.compressionDurations.size, 0)
404404
})
405405

406+
test("event hook falls back to completed runtime when running duration missing", async () => {
407+
const state = createSessionState()
408+
state.sessionId = "session-1"
409+
const handler = createEventHandler(state, new Logger(false))
410+
411+
state.prune.messages.blocksById.set(1, {
412+
blockId: 1,
413+
runId: 1,
414+
active: true,
415+
deactivatedByUser: false,
416+
compressedTokens: 0,
417+
summaryTokens: 0,
418+
durationMs: 0,
419+
mode: "message",
420+
topic: "one",
421+
batchTopic: "one",
422+
startId: "m0001",
423+
endId: "m0001",
424+
anchorMessageId: "msg-a",
425+
compressMessageId: "message-1",
426+
compressCallId: undefined,
427+
includedBlockIds: [],
428+
consumedBlockIds: [],
429+
parentBlockIds: [],
430+
directMessageIds: [],
431+
directToolIds: [],
432+
effectiveMessageIds: ["msg-a"],
433+
effectiveToolIds: [],
434+
createdAt: 1,
435+
summary: "a",
436+
})
437+
438+
await handler({
439+
event: {
440+
type: "message.part.updated",
441+
properties: {
442+
part: {
443+
type: "tool",
444+
tool: "compress",
445+
callID: "call-3",
446+
messageID: "message-1",
447+
sessionID: "session-1",
448+
state: {
449+
status: "completed",
450+
input: {},
451+
output: "done",
452+
title: "",
453+
metadata: {},
454+
time: { start: 500, end: 940 },
455+
},
456+
},
457+
},
458+
},
459+
})
460+
461+
assert.equal(state.prune.messages.blocksById.get(1)?.durationMs, 440)
462+
assert.equal(state.compressionDurations.size, 0)
463+
})
464+
406465
test("event hook ignores non-compress tool parts", async () => {
407466
const state = createSessionState()
408467
state.sessionId = "session-1"

0 commit comments

Comments
 (0)