@tarquinen/opencode-dcp 3.2.0-beta0 → 3.2.2-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.
@@ -0,0 +1,180 @@
1
+ import type { SessionState, ToolParameterEntry, WithParts } from "./types"
2
+ import type { Logger } from "../logger"
3
+ import { loadSessionState, saveSessionState } from "./persistence"
4
+ import {
5
+ isSubAgentSession,
6
+ findLastCompactionTimestamp,
7
+ countTurns,
8
+ resetOnCompaction,
9
+ createPruneMessagesState,
10
+ loadPruneMessagesState,
11
+ loadPruneMap,
12
+ collectTurnNudgeAnchors,
13
+ } from "./utils"
14
+ import { getLastUserMessage } from "../messages/query"
15
+
16
+ export const checkSession = async (
17
+ client: any,
18
+ state: SessionState,
19
+ logger: Logger,
20
+ messages: WithParts[],
21
+ manualModeDefault: boolean,
22
+ ): Promise<void> => {
23
+ const lastUserMessage = getLastUserMessage(messages)
24
+ if (!lastUserMessage) {
25
+ return
26
+ }
27
+
28
+ const lastSessionId = lastUserMessage.info.sessionID
29
+
30
+ if (state.sessionId === null || state.sessionId !== lastSessionId) {
31
+ logger.info(`Session changed: ${state.sessionId} -> ${lastSessionId}`)
32
+ try {
33
+ await ensureSessionInitialized(
34
+ client,
35
+ state,
36
+ lastSessionId,
37
+ logger,
38
+ messages,
39
+ manualModeDefault,
40
+ )
41
+ } catch (err: any) {
42
+ logger.error("Failed to initialize session state", { error: err.message })
43
+ }
44
+ }
45
+
46
+ const lastCompactionTimestamp = findLastCompactionTimestamp(messages)
47
+ if (lastCompactionTimestamp > state.lastCompaction) {
48
+ state.lastCompaction = lastCompactionTimestamp
49
+ resetOnCompaction(state)
50
+ logger.info("Detected compaction - reset stale state", {
51
+ timestamp: lastCompactionTimestamp,
52
+ })
53
+
54
+ saveSessionState(state, logger).catch((error) => {
55
+ logger.warn("Failed to persist state reset after compaction", {
56
+ error: error instanceof Error ? error.message : String(error),
57
+ })
58
+ })
59
+ }
60
+
61
+ state.currentTurn = countTurns(state, messages)
62
+ }
63
+
64
+ export function createSessionState(): SessionState {
65
+ return {
66
+ sessionId: null,
67
+ isSubAgent: false,
68
+ manualMode: false,
69
+ compressPermission: undefined,
70
+ pendingManualTrigger: null,
71
+ prune: {
72
+ tools: new Map<string, number>(),
73
+ messages: createPruneMessagesState(),
74
+ },
75
+ nudges: {
76
+ contextLimitAnchors: new Set<string>(),
77
+ turnNudgeAnchors: new Set<string>(),
78
+ iterationNudgeAnchors: new Set<string>(),
79
+ },
80
+ stats: {
81
+ pruneTokenCounter: 0,
82
+ totalPruneTokens: 0,
83
+ },
84
+ toolParameters: new Map<string, ToolParameterEntry>(),
85
+ subAgentResultCache: new Map<string, string>(),
86
+ toolIdList: [],
87
+ messageIds: {
88
+ byRawId: new Map<string, string>(),
89
+ byRef: new Map<string, string>(),
90
+ nextRef: 1,
91
+ },
92
+ lastCompaction: 0,
93
+ currentTurn: 0,
94
+ variant: undefined,
95
+ modelContextLimit: undefined,
96
+ systemPromptTokens: undefined,
97
+ }
98
+ }
99
+
100
+ export function resetSessionState(state: SessionState): void {
101
+ state.sessionId = null
102
+ state.isSubAgent = false
103
+ state.manualMode = false
104
+ state.compressPermission = undefined
105
+ state.pendingManualTrigger = null
106
+ state.prune = {
107
+ tools: new Map<string, number>(),
108
+ messages: createPruneMessagesState(),
109
+ }
110
+ state.nudges = {
111
+ contextLimitAnchors: new Set<string>(),
112
+ turnNudgeAnchors: new Set<string>(),
113
+ iterationNudgeAnchors: new Set<string>(),
114
+ }
115
+ state.stats = {
116
+ pruneTokenCounter: 0,
117
+ totalPruneTokens: 0,
118
+ }
119
+ state.toolParameters.clear()
120
+ state.subAgentResultCache.clear()
121
+ state.toolIdList = []
122
+ state.messageIds = {
123
+ byRawId: new Map<string, string>(),
124
+ byRef: new Map<string, string>(),
125
+ nextRef: 1,
126
+ }
127
+ state.lastCompaction = 0
128
+ state.currentTurn = 0
129
+ state.variant = undefined
130
+ state.modelContextLimit = undefined
131
+ state.systemPromptTokens = undefined
132
+ }
133
+
134
+ export async function ensureSessionInitialized(
135
+ client: any,
136
+ state: SessionState,
137
+ sessionId: string,
138
+ logger: Logger,
139
+ messages: WithParts[],
140
+ manualModeEnabled: boolean,
141
+ ): Promise<void> {
142
+ if (state.sessionId === sessionId) {
143
+ return
144
+ }
145
+
146
+ // logger.info("session ID = " + sessionId)
147
+ // logger.info("Initializing session state", { sessionId: sessionId })
148
+
149
+ resetSessionState(state)
150
+ state.manualMode = manualModeEnabled ? "active" : false
151
+ state.sessionId = sessionId
152
+
153
+ const isSubAgent = await isSubAgentSession(client, sessionId)
154
+ state.isSubAgent = isSubAgent
155
+ // logger.info("isSubAgent = " + isSubAgent)
156
+
157
+ state.lastCompaction = findLastCompactionTimestamp(messages)
158
+ state.currentTurn = countTurns(state, messages)
159
+ state.nudges.turnNudgeAnchors = collectTurnNudgeAnchors(messages)
160
+
161
+ const persisted = await loadSessionState(sessionId, logger)
162
+ if (persisted === null) {
163
+ return
164
+ }
165
+
166
+ state.prune.tools = loadPruneMap(persisted.prune.tools)
167
+ state.prune.messages = loadPruneMessagesState(persisted.prune.messages)
168
+ state.nudges.contextLimitAnchors = new Set<string>(persisted.nudges.contextLimitAnchors || [])
169
+ state.nudges.turnNudgeAnchors = new Set<string>([
170
+ ...state.nudges.turnNudgeAnchors,
171
+ ...(persisted.nudges.turnNudgeAnchors || []),
172
+ ])
173
+ state.nudges.iterationNudgeAnchors = new Set<string>(
174
+ persisted.nudges.iterationNudgeAnchors || [],
175
+ )
176
+ state.stats = {
177
+ pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
178
+ totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
179
+ }
180
+ }
@@ -0,0 +1,98 @@
1
+ import type { SessionState, ToolStatus, WithParts } from "./index"
2
+ import type { Logger } from "../logger"
3
+ import { PluginConfig } from "../config"
4
+ import { isMessageCompacted } from "./utils"
5
+ import { countToolTokens } from "../token-utils"
6
+
7
+ const MAX_TOOL_CACHE_SIZE = 1000
8
+
9
+ /**
10
+ * Sync tool parameters from session messages.
11
+ */
12
+ export function syncToolCache(
13
+ state: SessionState,
14
+ config: PluginConfig,
15
+ logger: Logger,
16
+ messages: WithParts[],
17
+ ): void {
18
+ try {
19
+ logger.info("Syncing tool parameters from OpenCode messages")
20
+
21
+ let turnCounter = 0
22
+
23
+ for (const msg of messages) {
24
+ if (isMessageCompacted(state, msg)) {
25
+ continue
26
+ }
27
+
28
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
29
+ for (const part of parts) {
30
+ if (part.type === "step-start") {
31
+ turnCounter++
32
+ continue
33
+ }
34
+
35
+ if (part.type !== "tool" || !part.callID) {
36
+ continue
37
+ }
38
+
39
+ const turnProtectionEnabled = config.turnProtection.enabled
40
+ const turnProtectionTurns = config.turnProtection.turns
41
+ const isProtectedByTurn =
42
+ turnProtectionEnabled &&
43
+ turnProtectionTurns > 0 &&
44
+ state.currentTurn - turnCounter < turnProtectionTurns
45
+
46
+ if (state.toolParameters.has(part.callID)) {
47
+ continue
48
+ }
49
+
50
+ if (isProtectedByTurn) {
51
+ continue
52
+ }
53
+
54
+ const tokenCount = countToolTokens(part)
55
+
56
+ state.toolParameters.set(part.callID, {
57
+ tool: part.tool,
58
+ parameters: part.state?.input ?? {},
59
+ status: part.state.status as ToolStatus | undefined,
60
+ error: part.state.status === "error" ? part.state.error : undefined,
61
+ turn: turnCounter,
62
+ tokenCount,
63
+ })
64
+ logger.info(
65
+ `Cached tool id: ${part.callID} (turn ${turnCounter}${tokenCount !== undefined ? `, ${tokenCount} tokens` : ""})`,
66
+ )
67
+ }
68
+ }
69
+
70
+ logger.info(
71
+ `Synced cache - size: ${state.toolParameters.size}, currentTurn: ${state.currentTurn}`,
72
+ )
73
+ trimToolParametersCache(state)
74
+ } catch (error) {
75
+ logger.warn("Failed to sync tool parameters from OpenCode", {
76
+ error: error instanceof Error ? error.message : String(error),
77
+ })
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Trim the tool parameters cache to prevent unbounded memory growth.
83
+ * Uses FIFO eviction - removes oldest entries first.
84
+ */
85
+ export function trimToolParametersCache(state: SessionState): void {
86
+ if (state.toolParameters.size <= MAX_TOOL_CACHE_SIZE) {
87
+ return
88
+ }
89
+
90
+ const keysToRemove = Array.from(state.toolParameters.keys()).slice(
91
+ 0,
92
+ state.toolParameters.size - MAX_TOOL_CACHE_SIZE,
93
+ )
94
+
95
+ for (const key of keysToRemove) {
96
+ state.toolParameters.delete(key)
97
+ }
98
+ }
@@ -0,0 +1,108 @@
1
+ import { Message, Part } from "@opencode-ai/sdk/v2"
2
+
3
+ export interface WithParts {
4
+ info: Message
5
+ parts: Part[]
6
+ }
7
+
8
+ export type ToolStatus = "pending" | "running" | "completed" | "error"
9
+
10
+ export interface ToolParameterEntry {
11
+ tool: string
12
+ parameters: any
13
+ status?: ToolStatus
14
+ error?: string
15
+ turn: number
16
+ tokenCount?: number
17
+ }
18
+
19
+ export interface SessionStats {
20
+ pruneTokenCounter: number
21
+ totalPruneTokens: number
22
+ }
23
+
24
+ export interface PrunedMessageEntry {
25
+ tokenCount: number
26
+ allBlockIds: number[]
27
+ activeBlockIds: number[]
28
+ }
29
+
30
+ export type CompressionMode = "range" | "message"
31
+
32
+ export interface CompressionBlock {
33
+ blockId: number
34
+ runId: number
35
+ active: boolean
36
+ deactivatedByUser: boolean
37
+ compressedTokens: number
38
+ summaryTokens: number
39
+ mode?: CompressionMode
40
+ topic: string
41
+ batchTopic?: string
42
+ startId: string
43
+ endId: string
44
+ anchorMessageId: string
45
+ compressMessageId: string
46
+ includedBlockIds: number[]
47
+ consumedBlockIds: number[]
48
+ parentBlockIds: number[]
49
+ directMessageIds: string[]
50
+ directToolIds: string[]
51
+ effectiveMessageIds: string[]
52
+ effectiveToolIds: string[]
53
+ createdAt: number
54
+ deactivatedAt?: number
55
+ deactivatedByBlockId?: number
56
+ summary: string
57
+ }
58
+
59
+ export interface PruneMessagesState {
60
+ byMessageId: Map<string, PrunedMessageEntry>
61
+ blocksById: Map<number, CompressionBlock>
62
+ activeBlockIds: Set<number>
63
+ activeByAnchorMessageId: Map<string, number>
64
+ nextBlockId: number
65
+ nextRunId: number
66
+ }
67
+
68
+ export interface Prune {
69
+ tools: Map<string, number>
70
+ messages: PruneMessagesState
71
+ }
72
+
73
+ export interface PendingManualTrigger {
74
+ sessionId: string
75
+ prompt: string
76
+ }
77
+
78
+ export interface MessageIdState {
79
+ byRawId: Map<string, string>
80
+ byRef: Map<string, string>
81
+ nextRef: number
82
+ }
83
+
84
+ export interface Nudges {
85
+ contextLimitAnchors: Set<string>
86
+ turnNudgeAnchors: Set<string>
87
+ iterationNudgeAnchors: Set<string>
88
+ }
89
+
90
+ export interface SessionState {
91
+ sessionId: string | null
92
+ isSubAgent: boolean
93
+ manualMode: false | "active" | "compress-pending"
94
+ compressPermission: "ask" | "allow" | "deny" | undefined
95
+ pendingManualTrigger: PendingManualTrigger | null
96
+ prune: Prune
97
+ nudges: Nudges
98
+ stats: SessionStats
99
+ toolParameters: Map<string, ToolParameterEntry>
100
+ subAgentResultCache: Map<string, string>
101
+ toolIdList: string[]
102
+ messageIds: MessageIdState
103
+ lastCompaction: number
104
+ currentTurn: number
105
+ variant: string | undefined
106
+ modelContextLimit: number | undefined
107
+ systemPromptTokens: number | undefined
108
+ }
@@ -0,0 +1,310 @@
1
+ import type {
2
+ CompressionBlock,
3
+ PruneMessagesState,
4
+ PrunedMessageEntry,
5
+ SessionState,
6
+ WithParts,
7
+ } from "./types"
8
+ import { isIgnoredUserMessage, messageHasCompress } from "../messages/query"
9
+ import { countTokens } from "../token-utils"
10
+
11
+ export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean => {
12
+ if (msg.info.time.created < state.lastCompaction) {
13
+ return true
14
+ }
15
+ const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id)
16
+ if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
17
+ return true
18
+ }
19
+ return false
20
+ }
21
+
22
+ interface PersistedPruneMessagesState {
23
+ byMessageId?: Record<string, PrunedMessageEntry>
24
+ blocksById?: Record<string, CompressionBlock>
25
+ activeBlockIds?: number[]
26
+ activeByAnchorMessageId?: Record<string, number>
27
+ nextBlockId?: number
28
+ nextRunId?: number
29
+ }
30
+
31
+ export async function isSubAgentSession(client: any, sessionID: string): Promise<boolean> {
32
+ try {
33
+ const result = await client.session.get({ path: { id: sessionID } })
34
+ return !!result.data?.parentID
35
+ } catch (error: any) {
36
+ return false
37
+ }
38
+ }
39
+
40
+ export function findLastCompactionTimestamp(messages: WithParts[]): number {
41
+ for (let i = messages.length - 1; i >= 0; i--) {
42
+ const msg = messages[i]
43
+ if (msg.info.role === "assistant" && msg.info.summary === true) {
44
+ return msg.info.time.created
45
+ }
46
+ }
47
+ return 0
48
+ }
49
+
50
+ export function countTurns(state: SessionState, messages: WithParts[]): number {
51
+ let turnCount = 0
52
+ for (const msg of messages) {
53
+ if (isMessageCompacted(state, msg)) {
54
+ continue
55
+ }
56
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
57
+ for (const part of parts) {
58
+ if (part.type === "step-start") {
59
+ turnCount++
60
+ }
61
+ }
62
+ }
63
+ return turnCount
64
+ }
65
+
66
+ export function loadPruneMap(obj?: Record<string, number>): Map<string, number> {
67
+ if (!obj || typeof obj !== "object") {
68
+ return new Map()
69
+ }
70
+
71
+ const entries = Object.entries(obj).filter(
72
+ (entry): entry is [string, number] =>
73
+ typeof entry[0] === "string" && typeof entry[1] === "number",
74
+ )
75
+ return new Map(entries)
76
+ }
77
+
78
+ export function createPruneMessagesState(): PruneMessagesState {
79
+ return {
80
+ byMessageId: new Map<string, PrunedMessageEntry>(),
81
+ blocksById: new Map<number, CompressionBlock>(),
82
+ activeBlockIds: new Set<number>(),
83
+ activeByAnchorMessageId: new Map<string, number>(),
84
+ nextBlockId: 1,
85
+ nextRunId: 1,
86
+ }
87
+ }
88
+
89
+ export function loadPruneMessagesState(
90
+ persisted?: PersistedPruneMessagesState,
91
+ ): PruneMessagesState {
92
+ const state = createPruneMessagesState()
93
+ if (!persisted || typeof persisted !== "object") {
94
+ return state
95
+ }
96
+
97
+ if (typeof persisted.nextBlockId === "number" && Number.isInteger(persisted.nextBlockId)) {
98
+ state.nextBlockId = Math.max(1, persisted.nextBlockId)
99
+ }
100
+ if (typeof persisted.nextRunId === "number" && Number.isInteger(persisted.nextRunId)) {
101
+ state.nextRunId = Math.max(1, persisted.nextRunId)
102
+ }
103
+
104
+ if (persisted.byMessageId && typeof persisted.byMessageId === "object") {
105
+ for (const [messageId, entry] of Object.entries(persisted.byMessageId)) {
106
+ if (!entry || typeof entry !== "object") {
107
+ continue
108
+ }
109
+
110
+ const tokenCount = typeof entry.tokenCount === "number" ? entry.tokenCount : 0
111
+ const allBlockIds = Array.isArray(entry.allBlockIds)
112
+ ? [
113
+ ...new Set(
114
+ entry.allBlockIds.filter(
115
+ (id): id is number => Number.isInteger(id) && id > 0,
116
+ ),
117
+ ),
118
+ ]
119
+ : []
120
+ const activeBlockIds = Array.isArray(entry.activeBlockIds)
121
+ ? [
122
+ ...new Set(
123
+ entry.activeBlockIds.filter(
124
+ (id): id is number => Number.isInteger(id) && id > 0,
125
+ ),
126
+ ),
127
+ ]
128
+ : []
129
+
130
+ state.byMessageId.set(messageId, {
131
+ tokenCount,
132
+ allBlockIds,
133
+ activeBlockIds,
134
+ })
135
+ }
136
+ }
137
+
138
+ if (persisted.blocksById && typeof persisted.blocksById === "object") {
139
+ for (const [blockIdStr, block] of Object.entries(persisted.blocksById)) {
140
+ const blockId = Number.parseInt(blockIdStr, 10)
141
+ if (!Number.isInteger(blockId) || blockId < 1 || !block || typeof block !== "object") {
142
+ continue
143
+ }
144
+
145
+ const toNumberArray = (value: unknown): number[] =>
146
+ Array.isArray(value)
147
+ ? [
148
+ ...new Set(
149
+ value.filter(
150
+ (item): item is number => Number.isInteger(item) && item > 0,
151
+ ),
152
+ ),
153
+ ]
154
+ : []
155
+ const toStringArray = (value: unknown): string[] =>
156
+ Array.isArray(value)
157
+ ? [...new Set(value.filter((item): item is string => typeof item === "string"))]
158
+ : []
159
+
160
+ state.blocksById.set(blockId, {
161
+ blockId,
162
+ runId:
163
+ typeof block.runId === "number" &&
164
+ Number.isInteger(block.runId) &&
165
+ block.runId > 0
166
+ ? block.runId
167
+ : blockId,
168
+ active: block.active === true,
169
+ deactivatedByUser: block.deactivatedByUser === true,
170
+ compressedTokens:
171
+ typeof block.compressedTokens === "number" &&
172
+ Number.isFinite(block.compressedTokens)
173
+ ? Math.max(0, block.compressedTokens)
174
+ : 0,
175
+ summaryTokens:
176
+ typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens)
177
+ ? Math.max(0, block.summaryTokens)
178
+ : typeof block.summary === "string"
179
+ ? countTokens(block.summary)
180
+ : 0,
181
+ mode: block.mode === "range" || block.mode === "message" ? block.mode : undefined,
182
+ topic: typeof block.topic === "string" ? block.topic : "",
183
+ batchTopic:
184
+ typeof block.batchTopic === "string"
185
+ ? block.batchTopic
186
+ : typeof block.topic === "string"
187
+ ? block.topic
188
+ : "",
189
+ startId: typeof block.startId === "string" ? block.startId : "",
190
+ endId: typeof block.endId === "string" ? block.endId : "",
191
+ anchorMessageId:
192
+ typeof block.anchorMessageId === "string" ? block.anchorMessageId : "",
193
+ compressMessageId:
194
+ typeof block.compressMessageId === "string" ? block.compressMessageId : "",
195
+ includedBlockIds: toNumberArray(block.includedBlockIds),
196
+ consumedBlockIds: toNumberArray(block.consumedBlockIds),
197
+ parentBlockIds: toNumberArray(block.parentBlockIds),
198
+ directMessageIds: toStringArray(block.directMessageIds),
199
+ directToolIds: toStringArray(block.directToolIds),
200
+ effectiveMessageIds: toStringArray(block.effectiveMessageIds),
201
+ effectiveToolIds: toStringArray(block.effectiveToolIds),
202
+ createdAt: typeof block.createdAt === "number" ? block.createdAt : 0,
203
+ deactivatedAt:
204
+ typeof block.deactivatedAt === "number" ? block.deactivatedAt : undefined,
205
+ deactivatedByBlockId:
206
+ typeof block.deactivatedByBlockId === "number" &&
207
+ Number.isInteger(block.deactivatedByBlockId)
208
+ ? block.deactivatedByBlockId
209
+ : undefined,
210
+ summary: typeof block.summary === "string" ? block.summary : "",
211
+ })
212
+ }
213
+ }
214
+
215
+ if (Array.isArray(persisted.activeBlockIds)) {
216
+ for (const blockId of persisted.activeBlockIds) {
217
+ if (!Number.isInteger(blockId) || blockId < 1) {
218
+ continue
219
+ }
220
+ state.activeBlockIds.add(blockId)
221
+ }
222
+ }
223
+
224
+ if (
225
+ persisted.activeByAnchorMessageId &&
226
+ typeof persisted.activeByAnchorMessageId === "object"
227
+ ) {
228
+ for (const [anchorMessageId, blockId] of Object.entries(
229
+ persisted.activeByAnchorMessageId,
230
+ )) {
231
+ if (typeof blockId !== "number" || !Number.isInteger(blockId) || blockId < 1) {
232
+ continue
233
+ }
234
+ state.activeByAnchorMessageId.set(anchorMessageId, blockId)
235
+ }
236
+ }
237
+
238
+ for (const [blockId, block] of state.blocksById) {
239
+ if (block.active) {
240
+ state.activeBlockIds.add(blockId)
241
+ if (block.anchorMessageId) {
242
+ state.activeByAnchorMessageId.set(block.anchorMessageId, blockId)
243
+ }
244
+ }
245
+ if (blockId >= state.nextBlockId) {
246
+ state.nextBlockId = blockId + 1
247
+ }
248
+ if (block.runId >= state.nextRunId) {
249
+ state.nextRunId = block.runId + 1
250
+ }
251
+ }
252
+
253
+ return state
254
+ }
255
+
256
+ export function collectTurnNudgeAnchors(messages: WithParts[]): Set<string> {
257
+ const anchors = new Set<string>()
258
+ let pendingUserMessageId: string | null = null
259
+
260
+ for (let i = messages.length - 1; i >= 0; i--) {
261
+ const message = messages[i]
262
+
263
+ if (messageHasCompress(message)) {
264
+ break
265
+ }
266
+
267
+ if (message.info.role === "user") {
268
+ if (!isIgnoredUserMessage(message)) {
269
+ pendingUserMessageId = message.info.id
270
+ }
271
+ continue
272
+ }
273
+
274
+ if (message.info.role === "assistant" && pendingUserMessageId) {
275
+ anchors.add(message.info.id)
276
+ anchors.add(pendingUserMessageId)
277
+ pendingUserMessageId = null
278
+ }
279
+ }
280
+
281
+ return anchors
282
+ }
283
+
284
+ export function getActiveSummaryTokenUsage(state: SessionState): number {
285
+ let total = 0
286
+ for (const blockId of state.prune.messages.activeBlockIds) {
287
+ const block = state.prune.messages.blocksById.get(blockId)
288
+ if (!block || !block.active) {
289
+ continue
290
+ }
291
+ total += block.summaryTokens
292
+ }
293
+ return total
294
+ }
295
+
296
+ export function resetOnCompaction(state: SessionState): void {
297
+ state.toolParameters.clear()
298
+ state.prune.tools = new Map<string, number>()
299
+ state.prune.messages = createPruneMessagesState()
300
+ state.messageIds = {
301
+ byRawId: new Map<string, string>(),
302
+ byRef: new Map<string, string>(),
303
+ nextRef: 1,
304
+ }
305
+ state.nudges = {
306
+ contextLimitAnchors: new Set<string>(),
307
+ turnNudgeAnchors: new Set<string>(),
308
+ iterationNudgeAnchors: new Set<string>(),
309
+ }
310
+ }