Skip to content

Commit d9a43c2

Browse files
committed
refactor: centralize compression timing logic
1 parent 8334dec commit d9a43c2

4 files changed

Lines changed: 154 additions & 107 deletions

File tree

lib/compress/timing.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import type { SessionState } from "../state/types"
2+
import { attachCompressionDuration } from "./state"
3+
4+
export interface CompressionStart {
5+
sessionId: string
6+
messageId: string
7+
startedAt: number
8+
}
9+
10+
export interface PendingCompressionDuration {
11+
callId: string
12+
messageId: string
13+
durationMs: number
14+
}
15+
16+
export interface CompressionTimingState {
17+
startsByCallId: Map<string, CompressionStart>
18+
pendingBySessionId: Map<string, PendingCompressionDuration[]>
19+
}
20+
21+
export function createCompressionTimingState(): CompressionTimingState {
22+
return {
23+
startsByCallId: new Map(),
24+
pendingBySessionId: new Map(),
25+
}
26+
}
27+
28+
export function recordCompressionStart(
29+
state: SessionState,
30+
callId: string,
31+
sessionId: string,
32+
messageId: string,
33+
startedAt: number,
34+
): boolean {
35+
if (state.compressionTiming.startsByCallId.has(callId)) {
36+
return false
37+
}
38+
39+
state.compressionTiming.startsByCallId.set(callId, {
40+
sessionId,
41+
messageId,
42+
startedAt,
43+
})
44+
return true
45+
}
46+
47+
export function consumeCompressionStart(
48+
state: SessionState,
49+
callId: string,
50+
): CompressionStart | undefined {
51+
const start = state.compressionTiming.startsByCallId.get(callId)
52+
state.compressionTiming.startsByCallId.delete(callId)
53+
return start
54+
}
55+
56+
export function clearCompressionStart(state: SessionState, callId: string): void {
57+
state.compressionTiming.startsByCallId.delete(callId)
58+
}
59+
60+
export function resolveCompressionDuration(
61+
start: CompressionStart | undefined,
62+
eventTime: number | undefined,
63+
partTime: { start?: unknown; end?: unknown } | undefined,
64+
): number | undefined {
65+
const runningAt =
66+
typeof partTime?.start === "number" && Number.isFinite(partTime.start)
67+
? partTime.start
68+
: eventTime
69+
const pendingToRunningMs =
70+
start && typeof runningAt === "number"
71+
? Math.max(0, runningAt - start.startedAt)
72+
: undefined
73+
74+
const toolStart = partTime?.start
75+
const toolEnd = partTime?.end
76+
const runtimeMs =
77+
typeof toolStart === "number" &&
78+
Number.isFinite(toolStart) &&
79+
typeof toolEnd === "number" &&
80+
Number.isFinite(toolEnd)
81+
? Math.max(0, toolEnd - toolStart)
82+
: undefined
83+
84+
return typeof pendingToRunningMs === "number" ? pendingToRunningMs : runtimeMs
85+
}
86+
87+
export function queueCompressionDuration(
88+
state: SessionState,
89+
sessionId: string,
90+
callId: string,
91+
messageId: string,
92+
durationMs: number,
93+
): void {
94+
const queued = state.compressionTiming.pendingBySessionId.get(sessionId) || []
95+
const filtered = queued.filter((entry) => entry.callId !== callId)
96+
filtered.push({ callId, messageId, durationMs })
97+
state.compressionTiming.pendingBySessionId.set(sessionId, filtered)
98+
}
99+
100+
export function applyPendingCompressionDurations(state: SessionState, sessionId: string): number {
101+
const queued = state.compressionTiming.pendingBySessionId.get(sessionId)
102+
if (!queued || queued.length === 0) {
103+
return 0
104+
}
105+
106+
let updates = 0
107+
const remaining = []
108+
for (const entry of queued) {
109+
const applied = attachCompressionDuration(
110+
state.prune.messages,
111+
entry.callId,
112+
entry.messageId,
113+
entry.durationMs,
114+
)
115+
if (applied > 0) {
116+
updates += applied
117+
continue
118+
}
119+
remaining.push(entry)
120+
}
121+
122+
if (remaining.length > 0) {
123+
state.compressionTiming.pendingBySessionId.set(sessionId, remaining)
124+
} else {
125+
state.compressionTiming.pendingBySessionId.delete(sessionId)
126+
}
127+
128+
return updates
129+
}

lib/hooks.ts

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ import {
1616
} from "./messages"
1717
import { renderSystemPrompt, type PromptStore } from "./prompts"
1818
import { buildProtectedToolsExtension } from "./prompts/extensions/system"
19+
import {
20+
applyPendingCompressionDurations,
21+
clearCompressionStart,
22+
consumeCompressionStart,
23+
queueCompressionDuration,
24+
recordCompressionStart,
25+
resolveCompressionDuration,
26+
} from "./compress/timing"
1927
import {
2028
applyPendingManualTrigger,
2129
handleContextCommand,
@@ -29,14 +37,7 @@ import {
2937
} from "./commands"
3038
import { type HostPermissionSnapshot } from "./host-permissions"
3139
import { compressPermission, syncCompressPermissionState } from "./compress-permission"
32-
import {
33-
checkSession,
34-
ensureSessionInitialized,
35-
applyPendingCompressionDurations,
36-
queueCompressionDuration,
37-
saveSessionState,
38-
syncToolCache,
39-
} from "./state"
40+
import { checkSession, ensureSessionInitialized, saveSessionState, syncToolCache } from "./state"
4041
import { cacheSystemPromptTokens } from "./ui/utils"
4142

4243
const INTERNAL_AGENT_SIGNATURES = [
@@ -307,16 +308,18 @@ export function createEventHandler(state: SessionState, logger: Logger) {
307308
return
308309
}
309310

310-
if (state.compressionTiming.startsByCallId.has(part.callID)) {
311+
const startedAt = eventTime ?? Date.now()
312+
if (
313+
!recordCompressionStart(
314+
state,
315+
part.callID,
316+
eventSessionId,
317+
part.messageID,
318+
startedAt,
319+
)
320+
) {
311321
return
312322
}
313-
314-
const startedAt = eventTime ?? Date.now()
315-
state.compressionTiming.startsByCallId.set(part.callID, {
316-
sessionId: eventSessionId,
317-
messageId: part.messageID,
318-
startedAt,
319-
})
320323
logger.debug("Recorded compression start", {
321324
sessionID: eventSessionId,
322325
callID: part.callID,
@@ -335,30 +338,8 @@ export function createEventHandler(state: SessionState, logger: Logger) {
335338
return
336339
}
337340

338-
const start = state.compressionTiming.startsByCallId.get(part.callID)
339-
state.compressionTiming.startsByCallId.delete(part.callID)
340-
341-
const runningAt =
342-
typeof part.state.time?.start === "number" && Number.isFinite(part.state.time.start)
343-
? part.state.time.start
344-
: eventTime
345-
const pendingToRunningMs =
346-
start && typeof runningAt === "number"
347-
? Math.max(0, runningAt - start.startedAt)
348-
: undefined
349-
350-
const toolStart = part.state.time?.start
351-
const toolEnd = part.state.time?.end
352-
const runtimeMs =
353-
typeof toolStart === "number" &&
354-
Number.isFinite(toolStart) &&
355-
typeof toolEnd === "number" &&
356-
Number.isFinite(toolEnd)
357-
? Math.max(0, toolEnd - toolStart)
358-
: undefined
359-
360-
const durationMs =
361-
typeof pendingToRunningMs === "number" ? pendingToRunningMs : runtimeMs
341+
const start = consumeCompressionStart(state, part.callID)
342+
const durationMs = resolveCompressionDuration(start, eventTime, part.state.time)
362343
if (typeof durationMs !== "number") {
363344
return
364345
}
@@ -390,7 +371,7 @@ export function createEventHandler(state: SessionState, logger: Logger) {
390371
}
391372

392373
if (typeof part.callID === "string") {
393-
state.compressionTiming.startsByCallId.delete(part.callID)
374+
clearCompressionStart(state, part.callID)
394375
}
395376
}
396377
}

lib/state/state.ts

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { SessionState, ToolParameterEntry, WithParts } from "./types"
22
import type { Logger } from "../logger"
3-
import { attachCompressionDuration } from "../compress/state"
3+
import { applyPendingCompressionDurations, createCompressionTimingState } from "../compress/timing"
44
import { loadSessionState, saveSessionState } from "./persistence"
55
import {
66
isSubAgentSession,
@@ -82,10 +82,7 @@ export function createSessionState(): SessionState {
8282
pruneTokenCounter: 0,
8383
totalPruneTokens: 0,
8484
},
85-
compressionTiming: {
86-
startsByCallId: new Map(),
87-
pendingBySessionId: new Map(),
88-
},
85+
compressionTiming: createCompressionTimingState(),
8986
toolParameters: new Map<string, ToolParameterEntry>(),
9087
subAgentResultCache: new Map<string, string>(),
9188
toolIdList: [],
@@ -188,47 +185,3 @@ export async function ensureSessionInitialized(
188185
await saveSessionState(state, logger)
189186
}
190187
}
191-
192-
export function queueCompressionDuration(
193-
state: SessionState,
194-
sessionId: string,
195-
callId: string,
196-
messageId: string,
197-
durationMs: number,
198-
): void {
199-
const queued = state.compressionTiming.pendingBySessionId.get(sessionId) || []
200-
const filtered = queued.filter((entry) => entry.callId !== callId)
201-
filtered.push({ callId, messageId, durationMs })
202-
state.compressionTiming.pendingBySessionId.set(sessionId, filtered)
203-
}
204-
205-
export function applyPendingCompressionDurations(state: SessionState, sessionId: string): number {
206-
const queued = state.compressionTiming.pendingBySessionId.get(sessionId)
207-
if (!queued || queued.length === 0) {
208-
return 0
209-
}
210-
211-
let updates = 0
212-
const remaining = []
213-
for (const entry of queued) {
214-
const applied = attachCompressionDuration(
215-
state.prune.messages,
216-
entry.callId,
217-
entry.messageId,
218-
entry.durationMs,
219-
)
220-
if (applied > 0) {
221-
updates += applied
222-
continue
223-
}
224-
remaining.push(entry)
225-
}
226-
227-
if (remaining.length > 0) {
228-
state.compressionTiming.pendingBySessionId.set(sessionId, remaining)
229-
} else {
230-
state.compressionTiming.pendingBySessionId.delete(sessionId)
231-
}
232-
233-
return updates
234-
}

lib/state/types.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { CompressionTimingState } from "../compress/timing"
12
import { Message, Part } from "@opencode-ai/sdk/v2"
23

34
export interface WithParts {
@@ -21,23 +22,6 @@ export interface SessionStats {
2122
totalPruneTokens: number
2223
}
2324

24-
export interface CompressionStart {
25-
sessionId: string
26-
messageId: string
27-
startedAt: number
28-
}
29-
30-
export interface PendingCompressionDuration {
31-
callId: string
32-
messageId: string
33-
durationMs: number
34-
}
35-
36-
export interface CompressionTimingState {
37-
startsByCallId: Map<string, CompressionStart>
38-
pendingBySessionId: Map<string, PendingCompressionDuration[]>
39-
}
40-
4125
export interface PrunedMessageEntry {
4226
tokenCount: number
4327
allBlockIds: number[]

0 commit comments

Comments
 (0)