@tarquinen/opencode-dcp 3.2.5-beta0 → 3.2.7-beta0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/token-utils.js +2 -2
- package/dist/lib/token-utils.js.map +1 -1
- package/index.ts +141 -0
- package/lib/analysis/tokens.ts +225 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +132 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +125 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +148 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress/index.ts +3 -0
- package/lib/compress/message-utils.ts +250 -0
- package/lib/compress/message.ts +137 -0
- package/lib/compress/pipeline.ts +106 -0
- package/lib/compress/protected-content.ts +154 -0
- package/lib/compress/range-utils.ts +308 -0
- package/lib/compress/range.ts +180 -0
- package/lib/compress/search.ts +267 -0
- package/lib/compress/state.ts +268 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +108 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +1071 -0
- package/lib/hooks.ts +378 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/logger.ts +235 -0
- package/lib/message-ids.ts +172 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +56 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +187 -0
- package/lib/prompts/compress-message.ts +42 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +256 -0
- package/lib/state/state.ts +190 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +112 -0
- package/lib/state/utils.ts +334 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/token-utils.ts +162 -0
- package/lib/ui/notification.ts +346 -0
- package/lib/ui/utils.ts +287 -0
- package/package.json +12 -3
- package/tui/data/context.ts +177 -0
- package/tui/index.tsx +34 -0
- package/tui/routes/summary.tsx +175 -0
- package/tui/shared/names.ts +9 -0
- package/tui/shared/theme.ts +58 -0
- package/tui/shared/types.ts +38 -0
- package/tui/slots/sidebar-content.tsx +502 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import type { SessionState, WithParts } from "../state"
|
|
2
|
+
import { formatBlockRef, parseBoundaryId } from "../message-ids"
|
|
3
|
+
import { isIgnoredUserMessage } from "../messages/query"
|
|
4
|
+
import { countAllMessageTokens } from "../token-utils"
|
|
5
|
+
import type { BoundaryReference, SearchContext, SelectionResolution } from "./types"
|
|
6
|
+
|
|
7
|
+
export async function fetchSessionMessages(client: any, sessionId: string): Promise<WithParts[]> {
|
|
8
|
+
const response = await client.session.messages({
|
|
9
|
+
path: { id: sessionId },
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const payload = (response?.data || response) as WithParts[]
|
|
13
|
+
return Array.isArray(payload) ? payload : []
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildSearchContext(state: SessionState, rawMessages: WithParts[]): SearchContext {
|
|
17
|
+
const rawMessagesById = new Map<string, WithParts>()
|
|
18
|
+
const rawIndexById = new Map<string, number>()
|
|
19
|
+
for (const msg of rawMessages) {
|
|
20
|
+
rawMessagesById.set(msg.info.id, msg)
|
|
21
|
+
}
|
|
22
|
+
for (let index = 0; index < rawMessages.length; index++) {
|
|
23
|
+
const message = rawMessages[index]
|
|
24
|
+
if (!message) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
rawIndexById.set(message.info.id, index)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const summaryByBlockId = new Map()
|
|
31
|
+
for (const [blockId, block] of state.prune.messages.blocksById) {
|
|
32
|
+
if (!block.active) {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
summaryByBlockId.set(blockId, block)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
rawMessages,
|
|
40
|
+
rawMessagesById,
|
|
41
|
+
rawIndexById,
|
|
42
|
+
summaryByBlockId,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveBoundaryIds(
|
|
47
|
+
context: SearchContext,
|
|
48
|
+
state: SessionState,
|
|
49
|
+
startId: string,
|
|
50
|
+
endId: string,
|
|
51
|
+
): { startReference: BoundaryReference; endReference: BoundaryReference } {
|
|
52
|
+
const lookup = buildBoundaryLookup(context, state)
|
|
53
|
+
const issues: string[] = []
|
|
54
|
+
const parsedStartId = parseBoundaryId(startId)
|
|
55
|
+
const parsedEndId = parseBoundaryId(endId)
|
|
56
|
+
|
|
57
|
+
if (parsedStartId === null) {
|
|
58
|
+
issues.push("startId is invalid. Use an injected message ID (mNNNN) or block ID (bN).")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (parsedEndId === null) {
|
|
62
|
+
issues.push("endId is invalid. Use an injected message ID (mNNNN) or block ID (bN).")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (issues.length > 0) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n"),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!parsedStartId || !parsedEndId) {
|
|
72
|
+
throw new Error("Invalid boundary ID(s)")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const startReference = lookup.get(parsedStartId.ref)
|
|
76
|
+
const endReference = lookup.get(parsedEndId.ref)
|
|
77
|
+
|
|
78
|
+
if (!startReference) {
|
|
79
|
+
issues.push(
|
|
80
|
+
`startId ${parsedStartId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!endReference) {
|
|
85
|
+
issues.push(
|
|
86
|
+
`endId ${parsedEndId.ref} is not available in the current conversation context. Choose an injected ID visible in context.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (issues.length > 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
issues.length === 1 ? issues[0] : issues.map((issue) => `- ${issue}`).join("\n"),
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!startReference || !endReference) {
|
|
97
|
+
throw new Error("Failed to resolve boundary IDs")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (startReference.rawIndex > endReference.rawIndex) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`startId ${parsedStartId.ref} appears after endId ${parsedEndId.ref} in the conversation. Start must come before end.`,
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { startReference, endReference }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resolveSelection(
|
|
110
|
+
context: SearchContext,
|
|
111
|
+
startReference: BoundaryReference,
|
|
112
|
+
endReference: BoundaryReference,
|
|
113
|
+
): SelectionResolution {
|
|
114
|
+
const startRawIndex = startReference.rawIndex
|
|
115
|
+
const endRawIndex = endReference.rawIndex
|
|
116
|
+
const messageIds: string[] = []
|
|
117
|
+
const messageSeen = new Set<string>()
|
|
118
|
+
const toolIds: string[] = []
|
|
119
|
+
const toolSeen = new Set<string>()
|
|
120
|
+
const requiredBlockIds: number[] = []
|
|
121
|
+
const requiredBlockSeen = new Set<number>()
|
|
122
|
+
const messageTokenById = new Map<string, number>()
|
|
123
|
+
|
|
124
|
+
for (let index = startRawIndex; index <= endRawIndex; index++) {
|
|
125
|
+
const rawMessage = context.rawMessages[index]
|
|
126
|
+
if (!rawMessage) {
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (isIgnoredUserMessage(rawMessage)) {
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const messageId = rawMessage.info.id
|
|
134
|
+
if (!messageSeen.has(messageId)) {
|
|
135
|
+
messageSeen.add(messageId)
|
|
136
|
+
messageIds.push(messageId)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!messageTokenById.has(messageId)) {
|
|
140
|
+
messageTokenById.set(messageId, countAllMessageTokens(rawMessage))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parts = Array.isArray(rawMessage.parts) ? rawMessage.parts : []
|
|
144
|
+
for (const part of parts) {
|
|
145
|
+
if (part.type !== "tool" || !part.callID) {
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (toolSeen.has(part.callID)) {
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
toolSeen.add(part.callID)
|
|
152
|
+
toolIds.push(part.callID)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const selectedMessageIds = new Set(messageIds)
|
|
157
|
+
const summariesInSelection: Array<{ blockId: number; rawIndex: number }> = []
|
|
158
|
+
for (const summary of context.summaryByBlockId.values()) {
|
|
159
|
+
if (!selectedMessageIds.has(summary.anchorMessageId)) {
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const anchorIndex = context.rawIndexById.get(summary.anchorMessageId)
|
|
164
|
+
if (anchorIndex === undefined) {
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
summariesInSelection.push({
|
|
169
|
+
blockId: summary.blockId,
|
|
170
|
+
rawIndex: anchorIndex,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
summariesInSelection.sort((a, b) => a.rawIndex - b.rawIndex || a.blockId - b.blockId)
|
|
175
|
+
for (const summary of summariesInSelection) {
|
|
176
|
+
if (requiredBlockSeen.has(summary.blockId)) {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
requiredBlockSeen.add(summary.blockId)
|
|
180
|
+
requiredBlockIds.push(summary.blockId)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (messageIds.length === 0) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"Failed to map boundary matches back to raw messages. Choose boundaries that include original conversation messages.",
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
startReference,
|
|
191
|
+
endReference,
|
|
192
|
+
messageIds,
|
|
193
|
+
messageTokenById,
|
|
194
|
+
toolIds,
|
|
195
|
+
requiredBlockIds,
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function resolveAnchorMessageId(startReference: BoundaryReference): string {
|
|
200
|
+
if (startReference.kind === "compressed-block") {
|
|
201
|
+
if (!startReference.anchorMessageId) {
|
|
202
|
+
throw new Error("Failed to map boundary matches back to raw messages")
|
|
203
|
+
}
|
|
204
|
+
return startReference.anchorMessageId
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!startReference.messageId) {
|
|
208
|
+
throw new Error("Failed to map boundary matches back to raw messages")
|
|
209
|
+
}
|
|
210
|
+
return startReference.messageId
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildBoundaryLookup(
|
|
214
|
+
context: SearchContext,
|
|
215
|
+
state: SessionState,
|
|
216
|
+
): Map<string, BoundaryReference> {
|
|
217
|
+
const lookup = new Map<string, BoundaryReference>()
|
|
218
|
+
|
|
219
|
+
for (const [messageRef, messageId] of state.messageIds.byRef) {
|
|
220
|
+
const rawMessage = context.rawMessagesById.get(messageId)
|
|
221
|
+
if (!rawMessage) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (isIgnoredUserMessage(rawMessage)) {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const rawIndex = context.rawIndexById.get(messageId)
|
|
229
|
+
if (rawIndex === undefined) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
lookup.set(messageRef, {
|
|
233
|
+
kind: "message",
|
|
234
|
+
rawIndex,
|
|
235
|
+
messageId,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const summaries = Array.from(context.summaryByBlockId.values()).sort(
|
|
240
|
+
(a, b) => a.blockId - b.blockId,
|
|
241
|
+
)
|
|
242
|
+
for (const summary of summaries) {
|
|
243
|
+
const anchorMessage = context.rawMessagesById.get(summary.anchorMessageId)
|
|
244
|
+
if (!anchorMessage) {
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
if (isIgnoredUserMessage(anchorMessage)) {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const rawIndex = context.rawIndexById.get(summary.anchorMessageId)
|
|
252
|
+
if (rawIndex === undefined) {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
const blockRef = formatBlockRef(summary.blockId)
|
|
256
|
+
if (!lookup.has(blockRef)) {
|
|
257
|
+
lookup.set(blockRef, {
|
|
258
|
+
kind: "compressed-block",
|
|
259
|
+
rawIndex,
|
|
260
|
+
blockId: summary.blockId,
|
|
261
|
+
anchorMessageId: summary.anchorMessageId,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return lookup
|
|
267
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { CompressionBlock, PruneMessagesState, SessionState } from "../state"
|
|
2
|
+
import { formatBlockRef, formatMessageIdTag } from "../message-ids"
|
|
3
|
+
import type { AppliedCompressionResult, CompressionStateInput, SelectionResolution } from "./types"
|
|
4
|
+
|
|
5
|
+
export const COMPRESSED_BLOCK_HEADER = "[Compressed conversation section]"
|
|
6
|
+
|
|
7
|
+
export function allocateBlockId(state: SessionState): number {
|
|
8
|
+
const next = state.prune.messages.nextBlockId
|
|
9
|
+
if (!Number.isInteger(next) || next < 1) {
|
|
10
|
+
state.prune.messages.nextBlockId = 2
|
|
11
|
+
return 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
state.prune.messages.nextBlockId = next + 1
|
|
15
|
+
return next
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function allocateRunId(state: SessionState): number {
|
|
19
|
+
const next = state.prune.messages.nextRunId
|
|
20
|
+
if (!Number.isInteger(next) || next < 1) {
|
|
21
|
+
state.prune.messages.nextRunId = 2
|
|
22
|
+
return 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
state.prune.messages.nextRunId = next + 1
|
|
26
|
+
return next
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function attachCompressionDuration(
|
|
30
|
+
messagesState: PruneMessagesState,
|
|
31
|
+
messageId: string,
|
|
32
|
+
callId: string,
|
|
33
|
+
durationMs: number,
|
|
34
|
+
): number {
|
|
35
|
+
if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) {
|
|
36
|
+
return 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let updates = 0
|
|
40
|
+
for (const block of messagesState.blocksById.values()) {
|
|
41
|
+
if (block.compressMessageId !== messageId || block.compressCallId !== callId) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
block.durationMs = durationMs
|
|
46
|
+
updates++
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return updates
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function wrapCompressedSummary(blockId: number, summary: string): string {
|
|
53
|
+
const header = COMPRESSED_BLOCK_HEADER
|
|
54
|
+
const footer = formatMessageIdTag(formatBlockRef(blockId))
|
|
55
|
+
const body = summary.trim()
|
|
56
|
+
if (body.length === 0) {
|
|
57
|
+
return `${header}\n${footer}`
|
|
58
|
+
}
|
|
59
|
+
return `${header}\n${body}\n\n${footer}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyCompressionState(
|
|
63
|
+
state: SessionState,
|
|
64
|
+
input: CompressionStateInput,
|
|
65
|
+
selection: SelectionResolution,
|
|
66
|
+
anchorMessageId: string,
|
|
67
|
+
blockId: number,
|
|
68
|
+
summary: string,
|
|
69
|
+
consumedBlockIds: number[],
|
|
70
|
+
): AppliedCompressionResult {
|
|
71
|
+
const messagesState = state.prune.messages
|
|
72
|
+
const consumed = [...new Set(consumedBlockIds.filter((id) => Number.isInteger(id) && id > 0))]
|
|
73
|
+
const included = [...consumed]
|
|
74
|
+
|
|
75
|
+
const effectiveMessageIds = new Set<string>(selection.messageIds)
|
|
76
|
+
const effectiveToolIds = new Set<string>(selection.toolIds)
|
|
77
|
+
|
|
78
|
+
for (const consumedBlockId of consumed) {
|
|
79
|
+
const consumedBlock = messagesState.blocksById.get(consumedBlockId)
|
|
80
|
+
if (!consumedBlock) {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
for (const messageId of consumedBlock.effectiveMessageIds) {
|
|
84
|
+
effectiveMessageIds.add(messageId)
|
|
85
|
+
}
|
|
86
|
+
for (const toolId of consumedBlock.effectiveToolIds) {
|
|
87
|
+
effectiveToolIds.add(toolId)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const initiallyActiveMessages = new Set<string>()
|
|
92
|
+
for (const messageId of effectiveMessageIds) {
|
|
93
|
+
const entry = messagesState.byMessageId.get(messageId)
|
|
94
|
+
if (entry && entry.activeBlockIds.length > 0) {
|
|
95
|
+
initiallyActiveMessages.add(messageId)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const initiallyActiveToolIds = new Set<string>()
|
|
100
|
+
for (const activeBlockId of messagesState.activeBlockIds) {
|
|
101
|
+
const activeBlock = messagesState.blocksById.get(activeBlockId)
|
|
102
|
+
if (!activeBlock || !activeBlock.active) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const toolId of activeBlock.effectiveToolIds) {
|
|
107
|
+
initiallyActiveToolIds.add(toolId)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const createdAt = Date.now()
|
|
112
|
+
const block: CompressionBlock = {
|
|
113
|
+
blockId,
|
|
114
|
+
runId: input.runId,
|
|
115
|
+
active: true,
|
|
116
|
+
deactivatedByUser: false,
|
|
117
|
+
compressedTokens: 0,
|
|
118
|
+
summaryTokens: input.summaryTokens,
|
|
119
|
+
durationMs: 0,
|
|
120
|
+
mode: input.mode,
|
|
121
|
+
topic: input.topic,
|
|
122
|
+
batchTopic: input.batchTopic,
|
|
123
|
+
startId: input.startId,
|
|
124
|
+
endId: input.endId,
|
|
125
|
+
anchorMessageId,
|
|
126
|
+
compressMessageId: input.compressMessageId,
|
|
127
|
+
compressCallId: input.compressCallId,
|
|
128
|
+
includedBlockIds: included,
|
|
129
|
+
consumedBlockIds: consumed,
|
|
130
|
+
parentBlockIds: [],
|
|
131
|
+
directMessageIds: [],
|
|
132
|
+
directToolIds: [],
|
|
133
|
+
effectiveMessageIds: [...effectiveMessageIds],
|
|
134
|
+
effectiveToolIds: [...effectiveToolIds],
|
|
135
|
+
createdAt,
|
|
136
|
+
summary,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
messagesState.blocksById.set(blockId, block)
|
|
140
|
+
messagesState.activeBlockIds.add(blockId)
|
|
141
|
+
messagesState.activeByAnchorMessageId.set(anchorMessageId, blockId)
|
|
142
|
+
|
|
143
|
+
const deactivatedAt = Date.now()
|
|
144
|
+
for (const consumedBlockId of consumed) {
|
|
145
|
+
const consumedBlock = messagesState.blocksById.get(consumedBlockId)
|
|
146
|
+
if (!consumedBlock || !consumedBlock.active) {
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
consumedBlock.active = false
|
|
151
|
+
consumedBlock.deactivatedAt = deactivatedAt
|
|
152
|
+
consumedBlock.deactivatedByBlockId = blockId
|
|
153
|
+
if (!consumedBlock.parentBlockIds.includes(blockId)) {
|
|
154
|
+
consumedBlock.parentBlockIds.push(blockId)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
messagesState.activeBlockIds.delete(consumedBlockId)
|
|
158
|
+
const mappedBlockId = messagesState.activeByAnchorMessageId.get(
|
|
159
|
+
consumedBlock.anchorMessageId,
|
|
160
|
+
)
|
|
161
|
+
if (mappedBlockId === consumedBlockId) {
|
|
162
|
+
messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const removeActiveBlockId = (
|
|
167
|
+
entry: { activeBlockIds: number[] },
|
|
168
|
+
blockIdToRemove: number,
|
|
169
|
+
): void => {
|
|
170
|
+
if (entry.activeBlockIds.length === 0) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
entry.activeBlockIds = entry.activeBlockIds.filter((id) => id !== blockIdToRemove)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const consumedBlockId of consumed) {
|
|
177
|
+
const consumedBlock = messagesState.blocksById.get(consumedBlockId)
|
|
178
|
+
if (!consumedBlock) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
for (const messageId of consumedBlock.effectiveMessageIds) {
|
|
182
|
+
const entry = messagesState.byMessageId.get(messageId)
|
|
183
|
+
if (!entry) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
removeActiveBlockId(entry, consumedBlockId)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const messageId of selection.messageIds) {
|
|
191
|
+
const tokenCount = selection.messageTokenById.get(messageId) || 0
|
|
192
|
+
const existing = messagesState.byMessageId.get(messageId)
|
|
193
|
+
|
|
194
|
+
if (!existing) {
|
|
195
|
+
messagesState.byMessageId.set(messageId, {
|
|
196
|
+
tokenCount,
|
|
197
|
+
allBlockIds: [blockId],
|
|
198
|
+
activeBlockIds: [blockId],
|
|
199
|
+
})
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
existing.tokenCount = Math.max(existing.tokenCount, tokenCount)
|
|
204
|
+
if (!existing.allBlockIds.includes(blockId)) {
|
|
205
|
+
existing.allBlockIds.push(blockId)
|
|
206
|
+
}
|
|
207
|
+
if (!existing.activeBlockIds.includes(blockId)) {
|
|
208
|
+
existing.activeBlockIds.push(blockId)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const messageId of block.effectiveMessageIds) {
|
|
213
|
+
if (selection.messageTokenById.has(messageId)) {
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const existing = messagesState.byMessageId.get(messageId)
|
|
218
|
+
if (!existing) {
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (!existing.allBlockIds.includes(blockId)) {
|
|
222
|
+
existing.allBlockIds.push(blockId)
|
|
223
|
+
}
|
|
224
|
+
if (!existing.activeBlockIds.includes(blockId)) {
|
|
225
|
+
existing.activeBlockIds.push(blockId)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let compressedTokens = 0
|
|
230
|
+
const newlyCompressedMessageIds: string[] = []
|
|
231
|
+
for (const messageId of effectiveMessageIds) {
|
|
232
|
+
const entry = messagesState.byMessageId.get(messageId)
|
|
233
|
+
if (!entry) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const isNowActive = entry.activeBlockIds.length > 0
|
|
238
|
+
const wasActive = initiallyActiveMessages.has(messageId)
|
|
239
|
+
|
|
240
|
+
if (isNowActive && !wasActive) {
|
|
241
|
+
compressedTokens += entry.tokenCount
|
|
242
|
+
newlyCompressedMessageIds.push(messageId)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const newlyCompressedToolIds: string[] = []
|
|
247
|
+
for (const toolId of effectiveToolIds) {
|
|
248
|
+
if (!initiallyActiveToolIds.has(toolId)) {
|
|
249
|
+
newlyCompressedToolIds.push(toolId)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
block.directMessageIds = [...newlyCompressedMessageIds]
|
|
254
|
+
block.directToolIds = [...newlyCompressedToolIds]
|
|
255
|
+
|
|
256
|
+
block.compressedTokens = compressedTokens
|
|
257
|
+
|
|
258
|
+
state.stats.pruneTokenCounter += compressedTokens
|
|
259
|
+
state.stats.totalPruneTokens += state.stats.pruneTokenCounter
|
|
260
|
+
state.stats.pruneTokenCounter = 0
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
compressedTokens,
|
|
264
|
+
messageIds: selection.messageIds,
|
|
265
|
+
newlyCompressedMessageIds,
|
|
266
|
+
newlyCompressedToolIds,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SessionState } from "../state/types"
|
|
2
|
+
import { attachCompressionDuration } from "./state"
|
|
3
|
+
|
|
4
|
+
export interface PendingCompressionDuration {
|
|
5
|
+
messageId: string
|
|
6
|
+
callId: string
|
|
7
|
+
durationMs: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CompressionTimingState {
|
|
11
|
+
startsByCallId: Map<string, number>
|
|
12
|
+
pendingByCallId: Map<string, PendingCompressionDuration>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildCompressionTimingKey(messageId: string, callId: string): string {
|
|
16
|
+
return `${messageId}:${callId}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function consumeCompressionStart(
|
|
20
|
+
state: SessionState,
|
|
21
|
+
messageId: string,
|
|
22
|
+
callId: string,
|
|
23
|
+
): number | undefined {
|
|
24
|
+
const key = buildCompressionTimingKey(messageId, callId)
|
|
25
|
+
const start = state.compressionTiming.startsByCallId.get(key)
|
|
26
|
+
state.compressionTiming.startsByCallId.delete(key)
|
|
27
|
+
return start
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveCompressionDuration(
|
|
31
|
+
startedAt: number | undefined,
|
|
32
|
+
eventTime: number | undefined,
|
|
33
|
+
partTime: { start?: unknown; end?: unknown } | undefined,
|
|
34
|
+
): number | undefined {
|
|
35
|
+
const runningAt =
|
|
36
|
+
typeof partTime?.start === "number" && Number.isFinite(partTime.start)
|
|
37
|
+
? partTime.start
|
|
38
|
+
: eventTime
|
|
39
|
+
const pendingToRunningMs =
|
|
40
|
+
typeof startedAt === "number" && typeof runningAt === "number"
|
|
41
|
+
? Math.max(0, runningAt - startedAt)
|
|
42
|
+
: undefined
|
|
43
|
+
|
|
44
|
+
const toolStart = partTime?.start
|
|
45
|
+
const toolEnd = partTime?.end
|
|
46
|
+
const runtimeMs =
|
|
47
|
+
typeof toolStart === "number" &&
|
|
48
|
+
Number.isFinite(toolStart) &&
|
|
49
|
+
typeof toolEnd === "number" &&
|
|
50
|
+
Number.isFinite(toolEnd)
|
|
51
|
+
? Math.max(0, toolEnd - toolStart)
|
|
52
|
+
: undefined
|
|
53
|
+
|
|
54
|
+
return typeof pendingToRunningMs === "number" ? pendingToRunningMs : runtimeMs
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function applyPendingCompressionDurations(state: SessionState): number {
|
|
58
|
+
if (state.compressionTiming.pendingByCallId.size === 0) {
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let updates = 0
|
|
63
|
+
for (const [key, entry] of state.compressionTiming.pendingByCallId) {
|
|
64
|
+
const applied = attachCompressionDuration(
|
|
65
|
+
state.prune.messages,
|
|
66
|
+
entry.messageId,
|
|
67
|
+
entry.callId,
|
|
68
|
+
entry.durationMs,
|
|
69
|
+
)
|
|
70
|
+
if (applied > 0) {
|
|
71
|
+
updates += applied
|
|
72
|
+
state.compressionTiming.pendingByCallId.delete(key)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return updates
|
|
77
|
+
}
|