@tarquinen/opencode-dcp 3.2.2-beta0 → 3.2.4-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.
Files changed (132) hide show
  1. package/README.md +4 -16
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/commands/compression-targets.d.ts +1 -0
  6. package/dist/lib/commands/compression-targets.d.ts.map +1 -1
  7. package/dist/lib/commands/compression-targets.js +1 -0
  8. package/dist/lib/commands/compression-targets.js.map +1 -1
  9. package/dist/lib/commands/manual.js +1 -1
  10. package/dist/lib/commands/manual.js.map +1 -1
  11. package/dist/lib/commands/stats.d.ts.map +1 -1
  12. package/dist/lib/commands/stats.js +41 -6
  13. package/dist/lib/commands/stats.js.map +1 -1
  14. package/dist/lib/compress/message-utils.d.ts +2 -2
  15. package/dist/lib/compress/message-utils.d.ts.map +1 -1
  16. package/dist/lib/compress/message-utils.js +87 -31
  17. package/dist/lib/compress/message-utils.js.map +1 -1
  18. package/dist/lib/compress/message.d.ts.map +1 -1
  19. package/dist/lib/compress/message.js +10 -6
  20. package/dist/lib/compress/message.js.map +1 -1
  21. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  22. package/dist/lib/compress/pipeline.js +2 -0
  23. package/dist/lib/compress/pipeline.js.map +1 -1
  24. package/dist/lib/compress/range.d.ts.map +1 -1
  25. package/dist/lib/compress/range.js +6 -2
  26. package/dist/lib/compress/range.js.map +1 -1
  27. package/dist/lib/compress/state.d.ts +2 -1
  28. package/dist/lib/compress/state.d.ts.map +1 -1
  29. package/dist/lib/compress/state.js +16 -0
  30. package/dist/lib/compress/state.js.map +1 -1
  31. package/dist/lib/compress/timing.d.ts +18 -0
  32. package/dist/lib/compress/timing.d.ts.map +1 -0
  33. package/dist/lib/compress/timing.js +42 -0
  34. package/dist/lib/compress/timing.js.map +1 -0
  35. package/dist/lib/compress/types.d.ts +2 -0
  36. package/dist/lib/compress/types.d.ts.map +1 -1
  37. package/dist/lib/config.js +1 -1
  38. package/dist/lib/config.js.map +1 -1
  39. package/dist/lib/hooks.d.ts +3 -0
  40. package/dist/lib/hooks.d.ts.map +1 -1
  41. package/dist/lib/hooks.js +72 -2
  42. package/dist/lib/hooks.js.map +1 -1
  43. package/dist/lib/messages/inject/inject.js +2 -2
  44. package/dist/lib/messages/inject/utils.d.ts +0 -1
  45. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  46. package/dist/lib/messages/inject/utils.js +1 -27
  47. package/dist/lib/messages/inject/utils.js.map +1 -1
  48. package/dist/lib/messages/utils.d.ts +1 -1
  49. package/dist/lib/messages/utils.d.ts.map +1 -1
  50. package/dist/lib/messages/utils.js +5 -12
  51. package/dist/lib/messages/utils.js.map +1 -1
  52. package/dist/lib/prompts/compress-message.d.ts +1 -1
  53. package/dist/lib/prompts/compress-message.d.ts.map +1 -1
  54. package/dist/lib/prompts/compress-message.js +11 -12
  55. package/dist/lib/prompts/compress-message.js.map +1 -1
  56. package/dist/lib/prompts/compress-range.d.ts +1 -1
  57. package/dist/lib/prompts/compress-range.d.ts.map +1 -1
  58. package/dist/lib/prompts/compress-range.js +1 -1
  59. package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
  60. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  61. package/dist/lib/prompts/context-limit-nudge.js +3 -9
  62. package/dist/lib/prompts/context-limit-nudge.js.map +1 -1
  63. package/dist/lib/prompts/extensions/nudge.d.ts +5 -0
  64. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  65. package/dist/lib/prompts/extensions/nudge.js +35 -0
  66. package/dist/lib/prompts/extensions/nudge.js.map +1 -0
  67. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  68. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  69. package/dist/lib/prompts/extensions/system.js +30 -0
  70. package/dist/lib/prompts/extensions/system.js.map +1 -0
  71. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  72. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  73. package/dist/lib/prompts/extensions/tool.js +34 -0
  74. package/dist/lib/prompts/extensions/tool.js.map +1 -0
  75. package/dist/lib/prompts/index.d.ts +1 -1
  76. package/dist/lib/prompts/index.d.ts.map +1 -1
  77. package/dist/lib/prompts/index.js +12 -13
  78. package/dist/lib/prompts/index.js.map +1 -1
  79. package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
  80. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  81. package/dist/lib/prompts/iteration-nudge.js +0 -2
  82. package/dist/lib/prompts/iteration-nudge.js.map +1 -1
  83. package/dist/lib/prompts/store.d.ts +2 -2
  84. package/dist/lib/prompts/store.d.ts.map +1 -1
  85. package/dist/lib/prompts/store.js +6 -6
  86. package/dist/lib/prompts/store.js.map +1 -1
  87. package/dist/lib/prompts/system.d.ts +1 -1
  88. package/dist/lib/prompts/system.d.ts.map +1 -1
  89. package/dist/lib/prompts/system.js +0 -13
  90. package/dist/lib/prompts/system.js.map +1 -1
  91. package/dist/lib/prompts/turn-nudge.d.ts +1 -1
  92. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  93. package/dist/lib/prompts/turn-nudge.js +1 -2
  94. package/dist/lib/prompts/turn-nudge.js.map +1 -1
  95. package/dist/lib/state/persistence.d.ts.map +1 -1
  96. package/dist/lib/state/persistence.js +13 -16
  97. package/dist/lib/state/persistence.js.map +1 -1
  98. package/dist/lib/state/state.d.ts.map +1 -1
  99. package/dist/lib/state/state.js +9 -0
  100. package/dist/lib/state/state.js.map +1 -1
  101. package/dist/lib/state/types.d.ts +4 -0
  102. package/dist/lib/state/types.d.ts.map +1 -1
  103. package/dist/lib/state/utils.d.ts +7 -6
  104. package/dist/lib/state/utils.d.ts.map +1 -1
  105. package/dist/lib/state/utils.js +17 -0
  106. package/dist/lib/state/utils.js.map +1 -1
  107. package/lib/compress/index.ts +3 -0
  108. package/lib/compress/message-utils.ts +250 -0
  109. package/lib/compress/message.ts +137 -0
  110. package/lib/compress/pipeline.ts +106 -0
  111. package/lib/compress/protected-content.ts +154 -0
  112. package/lib/compress/range-utils.ts +308 -0
  113. package/lib/compress/range.ts +180 -0
  114. package/lib/compress/search.ts +267 -0
  115. package/lib/compress/state.ts +268 -0
  116. package/lib/compress/timing.ts +77 -0
  117. package/lib/compress/types.ts +108 -0
  118. package/lib/config.ts +1 -1
  119. package/lib/message-ids.ts +172 -0
  120. package/lib/state/persistence.ts +20 -24
  121. package/lib/state/state.ts +10 -0
  122. package/lib/state/types.ts +4 -0
  123. package/lib/state/utils.ts +30 -6
  124. package/package.json +12 -5
  125. package/dist/lib/prompts/internal-overlays.d.ts +0 -5
  126. package/dist/lib/prompts/internal-overlays.d.ts.map +0 -1
  127. package/dist/lib/prompts/internal-overlays.js +0 -49
  128. package/dist/lib/prompts/internal-overlays.js.map +0 -1
  129. package/dist/lib/prompts/message-priority-guidance.d.ts +0 -2
  130. package/dist/lib/prompts/message-priority-guidance.d.ts.map +0 -1
  131. package/dist/lib/prompts/message-priority-guidance.js +0 -9
  132. package/dist/lib/prompts/message-priority-guidance.js.map +0 -1
@@ -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
+ }