@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.1

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 (215) hide show
  1. package/LICENSE +619 -0
  2. package/README.md +348 -2
  3. package/dist/index.d.ts +7 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +8129 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/commands/compression-targets.d.ts +13 -0
  8. package/dist/lib/commands/compression-targets.d.ts.map +1 -0
  9. package/dist/lib/commands/context.d.ts +67 -0
  10. package/dist/lib/commands/context.d.ts.map +1 -0
  11. package/dist/lib/commands/decompress.d.ts +12 -0
  12. package/dist/lib/commands/decompress.d.ts.map +1 -0
  13. package/dist/lib/commands/help.d.ts +18 -0
  14. package/dist/lib/commands/help.d.ts.map +1 -0
  15. package/dist/lib/commands/index.d.ts +8 -0
  16. package/dist/lib/commands/index.d.ts.map +1 -0
  17. package/dist/lib/commands/manual.d.ts +23 -0
  18. package/dist/lib/commands/manual.d.ts.map +1 -0
  19. package/dist/lib/commands/recompress.d.ts +12 -0
  20. package/dist/lib/commands/recompress.d.ts.map +1 -0
  21. package/dist/lib/commands/stats.d.ts +27 -0
  22. package/dist/lib/commands/stats.d.ts.map +1 -0
  23. package/dist/lib/commands/sweep.d.ts +23 -0
  24. package/dist/lib/commands/sweep.d.ts.map +1 -0
  25. package/dist/lib/compress/index.d.ts +3 -0
  26. package/dist/lib/compress/index.d.ts.map +1 -0
  27. package/dist/lib/compress/pipeline.d.ts +29 -0
  28. package/dist/lib/compress/pipeline.d.ts.map +1 -0
  29. package/dist/lib/compress/protected-content.d.ts +6 -0
  30. package/dist/lib/compress/protected-content.d.ts.map +1 -0
  31. package/dist/lib/compress/rc.d.ts +15 -0
  32. package/dist/lib/compress/rc.d.ts.map +1 -0
  33. package/dist/lib/compress/search.d.ts +12 -0
  34. package/dist/lib/compress/search.d.ts.map +1 -0
  35. package/dist/lib/compress/state.d.ts +10 -0
  36. package/dist/lib/compress/state.d.ts.map +1 -0
  37. package/dist/lib/compress/summary.d.ts +68 -0
  38. package/dist/lib/compress/summary.d.ts.map +1 -0
  39. package/dist/lib/compress/timing.d.ts +18 -0
  40. package/dist/lib/compress/timing.d.ts.map +1 -0
  41. package/dist/lib/compress/types.d.ts +64 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress/usage.d.ts +45 -0
  44. package/dist/lib/compress/usage.d.ts.map +1 -0
  45. package/dist/lib/compress-permission.d.ts +4 -0
  46. package/dist/lib/compress-permission.d.ts.map +1 -0
  47. package/dist/lib/config.d.ts +83 -0
  48. package/dist/lib/config.d.ts.map +1 -0
  49. package/dist/lib/hooks.d.ts +22 -0
  50. package/dist/lib/hooks.d.ts.map +1 -0
  51. package/dist/lib/logger.d.ts +31 -0
  52. package/dist/lib/logger.d.ts.map +1 -0
  53. package/dist/lib/message-ids.d.ts +20 -0
  54. package/dist/lib/message-ids.d.ts.map +1 -0
  55. package/dist/lib/messages/index.d.ts +8 -0
  56. package/dist/lib/messages/index.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/inject.d.ts +7 -0
  58. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  60. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  61. package/dist/lib/messages/inject/utils.d.ts +23 -0
  62. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  63. package/dist/lib/messages/prune.d.ts +4 -0
  64. package/dist/lib/messages/prune.d.ts.map +1 -0
  65. package/dist/lib/messages/query.d.ts +7 -0
  66. package/dist/lib/messages/query.d.ts.map +1 -0
  67. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  68. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  69. package/dist/lib/messages/shape.d.ts +5 -0
  70. package/dist/lib/messages/shape.d.ts.map +1 -0
  71. package/dist/lib/messages/sync.d.ts +4 -0
  72. package/dist/lib/messages/sync.d.ts.map +1 -0
  73. package/dist/lib/messages/utils.d.ts +27 -0
  74. package/dist/lib/messages/utils.d.ts.map +1 -0
  75. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  76. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  77. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  78. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  82. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  83. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  84. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  85. package/dist/lib/prompts/index.d.ts +4 -0
  86. package/dist/lib/prompts/index.d.ts.map +1 -0
  87. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  88. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  89. package/dist/lib/prompts/store.d.ts +26 -0
  90. package/dist/lib/prompts/store.d.ts.map +1 -0
  91. package/dist/lib/prompts/system.d.ts +4 -0
  92. package/dist/lib/prompts/system.d.ts.map +1 -0
  93. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  94. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  95. package/dist/lib/protected-patterns.d.ts +6 -0
  96. package/dist/lib/protected-patterns.d.ts.map +1 -0
  97. package/dist/lib/state/index.d.ts +5 -0
  98. package/dist/lib/state/index.d.ts.map +1 -0
  99. package/dist/lib/state/persistence.d.ts +47 -0
  100. package/dist/lib/state/persistence.d.ts.map +1 -0
  101. package/dist/lib/state/state.d.ts +9 -0
  102. package/dist/lib/state/state.d.ts.map +1 -0
  103. package/dist/lib/state/tool-cache.d.ts +13 -0
  104. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  105. package/dist/lib/state/types.d.ts +105 -0
  106. package/dist/lib/state/types.d.ts.map +1 -0
  107. package/dist/lib/state/utils.d.ts +22 -0
  108. package/dist/lib/state/utils.d.ts.map +1 -0
  109. package/dist/lib/strategies/deduplication.d.ts +10 -0
  110. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  111. package/dist/lib/strategies/index.d.ts +3 -0
  112. package/dist/lib/strategies/index.d.ts.map +1 -0
  113. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  114. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  115. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  116. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  117. package/dist/lib/token-utils.d.ts +19 -0
  118. package/dist/lib/token-utils.d.ts.map +1 -0
  119. package/dist/lib/tui/dialogs.d.ts +29 -0
  120. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  121. package/dist/lib/tui/format.d.ts +4 -0
  122. package/dist/lib/tui/format.d.ts.map +1 -0
  123. package/dist/lib/tui/types.d.ts +18 -0
  124. package/dist/lib/tui/types.d.ts.map +1 -0
  125. package/dist/lib/tui/ui.d.ts +48 -0
  126. package/dist/lib/tui/ui.d.ts.map +1 -0
  127. package/dist/lib/ui/notification.d.ts +17 -0
  128. package/dist/lib/ui/notification.d.ts.map +1 -0
  129. package/dist/lib/ui/utils.d.ts +10 -0
  130. package/dist/lib/ui/utils.d.ts.map +1 -0
  131. package/dist/lib/v2/index.d.ts +5 -0
  132. package/dist/lib/v2/index.d.ts.map +1 -0
  133. package/dist/lib/v2/messages.d.ts +13 -0
  134. package/dist/lib/v2/messages.d.ts.map +1 -0
  135. package/dist/lib/v2/rpc.d.ts +75 -0
  136. package/dist/lib/v2/rpc.d.ts.map +1 -0
  137. package/dist/lib/v2/theme.d.ts +55 -0
  138. package/dist/lib/v2/theme.d.ts.map +1 -0
  139. package/dist/lib/v2/tui.d.ts +4 -0
  140. package/dist/lib/v2/tui.d.ts.map +1 -0
  141. package/dist/lib/v2/usage.d.ts +44 -0
  142. package/dist/lib/v2/usage.d.ts.map +1 -0
  143. package/dist/tui.d.ts +7 -0
  144. package/dist/tui.d.ts.map +1 -0
  145. package/lib/commands/compression-targets.ts +54 -0
  146. package/lib/commands/context.ts +305 -0
  147. package/lib/commands/decompress.ts +273 -0
  148. package/lib/commands/help.ts +76 -0
  149. package/lib/commands/index.ts +11 -0
  150. package/lib/commands/manual.ts +126 -0
  151. package/lib/commands/recompress.ts +222 -0
  152. package/lib/commands/stats.ts +180 -0
  153. package/lib/commands/sweep.ts +266 -0
  154. package/lib/compress/index.ts +2 -0
  155. package/lib/compress/pipeline.ts +116 -0
  156. package/lib/compress/protected-content.ts +163 -0
  157. package/lib/compress/rc.ts +494 -0
  158. package/lib/compress/search.ts +277 -0
  159. package/lib/compress/state.ts +272 -0
  160. package/lib/compress/summary.ts +320 -0
  161. package/lib/compress/timing.ts +77 -0
  162. package/lib/compress/types.ts +70 -0
  163. package/lib/compress/usage.ts +168 -0
  164. package/lib/compress-permission.ts +9 -0
  165. package/lib/config.ts +995 -0
  166. package/lib/hooks.ts +205 -0
  167. package/lib/logger.ts +226 -0
  168. package/lib/message-ids.ts +181 -0
  169. package/lib/messages/index.ts +7 -0
  170. package/lib/messages/inject/inject.ts +215 -0
  171. package/lib/messages/inject/subagent-results.ts +84 -0
  172. package/lib/messages/inject/utils.ts +304 -0
  173. package/lib/messages/prune.ts +238 -0
  174. package/lib/messages/query.ts +71 -0
  175. package/lib/messages/reasoning-strip.ts +40 -0
  176. package/lib/messages/shape.ts +50 -0
  177. package/lib/messages/sync.ts +124 -0
  178. package/lib/messages/utils.ts +190 -0
  179. package/lib/prompts/compress-rc.ts +25 -0
  180. package/lib/prompts/context-limit-nudge.ts +18 -0
  181. package/lib/prompts/extensions/nudge.ts +34 -0
  182. package/lib/prompts/extensions/system.ts +32 -0
  183. package/lib/prompts/extensions/tool.ts +21 -0
  184. package/lib/prompts/index.ts +29 -0
  185. package/lib/prompts/iteration-nudge.ts +6 -0
  186. package/lib/prompts/store.ts +452 -0
  187. package/lib/prompts/system.ts +41 -0
  188. package/lib/prompts/turn-nudge.ts +10 -0
  189. package/lib/protected-patterns.ts +159 -0
  190. package/lib/state/index.ts +4 -0
  191. package/lib/state/persistence.ts +321 -0
  192. package/lib/state/state.ts +214 -0
  193. package/lib/state/tool-cache.ts +99 -0
  194. package/lib/state/types.ts +117 -0
  195. package/lib/state/utils.ts +345 -0
  196. package/lib/strategies/deduplication.ts +125 -0
  197. package/lib/strategies/index.ts +2 -0
  198. package/lib/strategies/purge-errors.ts +86 -0
  199. package/lib/subagents/subagent-results.ts +75 -0
  200. package/lib/token-utils.ts +164 -0
  201. package/lib/tui/dialogs.tsx +261 -0
  202. package/lib/tui/format.ts +25 -0
  203. package/lib/tui/types.ts +25 -0
  204. package/lib/tui/ui.tsx +245 -0
  205. package/lib/ui/notification.ts +347 -0
  206. package/lib/ui/utils.ts +304 -0
  207. package/lib/v2/index.ts +448 -0
  208. package/lib/v2/messages.ts +252 -0
  209. package/lib/v2/rpc.ts +60 -0
  210. package/lib/v2/theme.ts +67 -0
  211. package/lib/v2/tui.tsx +115 -0
  212. package/lib/v2/usage.ts +423 -0
  213. package/package.json +91 -5
  214. package/server.js +1 -0
  215. package/tui.tsx +7 -0
@@ -0,0 +1,117 @@
1
+ import type { CompressionTimingState } from "../compress/timing"
2
+ import type { CompressionUsageTotals } from "../compress/usage"
3
+ import type { IdFormat } from "../message-ids"
4
+ import { Message, Part } from "@opencode-ai/sdk/v2"
5
+
6
+ export interface WithParts {
7
+ info: Message
8
+ parts: Part[]
9
+ }
10
+
11
+ export type ToolStatus = "pending" | "running" | "completed" | "error"
12
+
13
+ export interface ToolParameterEntry {
14
+ tool: string
15
+ parameters: any
16
+ metadata?: Record<string, unknown>
17
+ status?: ToolStatus
18
+ error?: string
19
+ turn: number
20
+ tokenCount?: number
21
+ }
22
+
23
+ export interface SessionStats {
24
+ pruneTokenCounter: number
25
+ totalPruneTokens: number
26
+ /** Token usage of the hidden compression (summary) requests. */
27
+ compressionUsage: CompressionUsageTotals
28
+ }
29
+
30
+ export interface PrunedMessageEntry {
31
+ tokenCount: number
32
+ allBlockIds: number[]
33
+ activeBlockIds: number[]
34
+ }
35
+
36
+ export type CompressionMode = "rc"
37
+
38
+ export interface CompressionBlock {
39
+ blockId: number
40
+ runId: number
41
+ active: boolean
42
+ deactivatedByUser: boolean
43
+ compressedTokens: number
44
+ summaryTokens: number
45
+ durationMs: number
46
+ mode?: CompressionMode
47
+ topic: string
48
+ batchTopic?: string
49
+ startId: string
50
+ endId: string
51
+ anchorMessageId: string
52
+ compressMessageId: string
53
+ compressCallId?: string
54
+ includedBlockIds: number[]
55
+ consumedBlockIds: number[]
56
+ parentBlockIds: number[]
57
+ directMessageIds: string[]
58
+ directToolIds: string[]
59
+ effectiveMessageIds: string[]
60
+ effectiveToolIds: string[]
61
+ createdAt: number
62
+ deactivatedAt?: number
63
+ deactivatedByBlockId?: number
64
+ summary: string
65
+ }
66
+
67
+ export interface PruneMessagesState {
68
+ byMessageId: Map<string, PrunedMessageEntry>
69
+ blocksById: Map<number, CompressionBlock>
70
+ activeBlockIds: Set<number>
71
+ activeByAnchorMessageId: Map<string, number>
72
+ nextBlockId: number
73
+ nextRunId: number
74
+ }
75
+
76
+ export interface Prune {
77
+ tools: Map<string, number>
78
+ messages: PruneMessagesState
79
+ }
80
+
81
+ export interface PendingManualTrigger {
82
+ sessionId: string
83
+ prompt: string
84
+ }
85
+
86
+ export interface MessageIdState {
87
+ byRawId: Map<string, string>
88
+ byRef: Map<string, string>
89
+ nextRef: number
90
+ }
91
+
92
+ export interface Nudges {
93
+ contextLimitAnchors: Set<string>
94
+ turnNudgeAnchors: Set<string>
95
+ iterationNudgeAnchors: Set<string>
96
+ }
97
+
98
+ export interface SessionState {
99
+ idFormat: IdFormat
100
+ sessionId: string | null
101
+ isSubAgent: boolean
102
+ manualMode: false | "active" | "compress-pending"
103
+ compressPermission: "ask" | "allow" | "deny" | undefined
104
+ pendingManualTrigger: PendingManualTrigger | null
105
+ prune: Prune
106
+ nudges: Nudges
107
+ stats: SessionStats
108
+ compressionTiming: CompressionTimingState
109
+ toolParameters: Map<string, ToolParameterEntry>
110
+ subAgentResultCache: Map<string, string>
111
+ toolIdList: string[]
112
+ messageIds: MessageIdState
113
+ lastCompaction: number
114
+ currentTurn: number
115
+ modelContextLimit: number | undefined
116
+ systemPromptTokens: number | undefined
117
+ }
@@ -0,0 +1,345 @@
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 { isMessageWithInfo } from "../messages/shape"
10
+ import { countTokens } from "../token-utils"
11
+
12
+ export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean => {
13
+ if (!isMessageWithInfo(msg)) {
14
+ return false
15
+ }
16
+
17
+ if (msg.info.time.created < state.lastCompaction) {
18
+ return true
19
+ }
20
+ const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id)
21
+ if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
22
+ return true
23
+ }
24
+ return false
25
+ }
26
+
27
+ interface PersistedPruneMessagesState {
28
+ byMessageId: Record<string, PrunedMessageEntry>
29
+ blocksById: Record<string, CompressionBlock>
30
+ activeBlockIds: number[]
31
+ activeByAnchorMessageId: Record<string, number>
32
+ nextBlockId: number
33
+ nextRunId: number
34
+ }
35
+
36
+ export function serializePruneMessagesState(
37
+ messagesState: PruneMessagesState,
38
+ ): PersistedPruneMessagesState {
39
+ return {
40
+ byMessageId: Object.fromEntries(messagesState.byMessageId),
41
+ blocksById: Object.fromEntries(
42
+ Array.from(messagesState.blocksById.entries()).map(([blockId, block]) => [
43
+ String(blockId),
44
+ block,
45
+ ]),
46
+ ),
47
+ activeBlockIds: Array.from(messagesState.activeBlockIds),
48
+ activeByAnchorMessageId: Object.fromEntries(messagesState.activeByAnchorMessageId),
49
+ nextBlockId: messagesState.nextBlockId,
50
+ nextRunId: messagesState.nextRunId,
51
+ }
52
+ }
53
+
54
+ export async function isSubAgentSession(client: any, sessionID: string): Promise<boolean> {
55
+ try {
56
+ const result = await client.session.get({ path: { id: sessionID } })
57
+ return !!result.data?.parentID
58
+ } catch (error: any) {
59
+ return false
60
+ }
61
+ }
62
+
63
+ export function findLastCompactionTimestamp(messages: WithParts[]): number {
64
+ for (let i = messages.length - 1; i >= 0; i--) {
65
+ const msg = messages[i]
66
+ if (!isMessageWithInfo(msg)) {
67
+ continue
68
+ }
69
+ if (msg.info.role === "assistant" && msg.info.summary === true) {
70
+ return msg.info.time.created
71
+ }
72
+ }
73
+ return 0
74
+ }
75
+
76
+ export function countTurns(state: SessionState, messages: WithParts[]): number {
77
+ let turnCount = 0
78
+ for (const msg of messages) {
79
+ if (!isMessageWithInfo(msg)) {
80
+ continue
81
+ }
82
+ if (isMessageCompacted(state, msg)) {
83
+ continue
84
+ }
85
+ const parts = Array.isArray(msg.parts) ? msg.parts : []
86
+ for (const part of parts) {
87
+ if (part.type === "step-start") {
88
+ turnCount++
89
+ }
90
+ }
91
+ }
92
+ return turnCount
93
+ }
94
+
95
+ export function loadPruneMap(obj?: Record<string, number>): Map<string, number> {
96
+ if (!obj || typeof obj !== "object") {
97
+ return new Map()
98
+ }
99
+
100
+ const entries = Object.entries(obj).filter(
101
+ (entry): entry is [string, number] =>
102
+ typeof entry[0] === "string" && typeof entry[1] === "number",
103
+ )
104
+ return new Map(entries)
105
+ }
106
+
107
+ export function createPruneMessagesState(): PruneMessagesState {
108
+ return {
109
+ byMessageId: new Map<string, PrunedMessageEntry>(),
110
+ blocksById: new Map<number, CompressionBlock>(),
111
+ activeBlockIds: new Set<number>(),
112
+ activeByAnchorMessageId: new Map<string, number>(),
113
+ nextBlockId: 1,
114
+ nextRunId: 1,
115
+ }
116
+ }
117
+
118
+ export function loadPruneMessagesState(
119
+ persisted?: PersistedPruneMessagesState,
120
+ ): PruneMessagesState {
121
+ const state = createPruneMessagesState()
122
+ if (!persisted || typeof persisted !== "object") {
123
+ return state
124
+ }
125
+
126
+ if (typeof persisted.nextBlockId === "number" && Number.isInteger(persisted.nextBlockId)) {
127
+ state.nextBlockId = Math.max(1, persisted.nextBlockId)
128
+ }
129
+ if (typeof persisted.nextRunId === "number" && Number.isInteger(persisted.nextRunId)) {
130
+ state.nextRunId = Math.max(1, persisted.nextRunId)
131
+ }
132
+
133
+ if (persisted.byMessageId && typeof persisted.byMessageId === "object") {
134
+ for (const [messageId, entry] of Object.entries(persisted.byMessageId)) {
135
+ if (!entry || typeof entry !== "object") {
136
+ continue
137
+ }
138
+
139
+ const tokenCount = typeof entry.tokenCount === "number" ? entry.tokenCount : 0
140
+ const allBlockIds = Array.isArray(entry.allBlockIds)
141
+ ? [
142
+ ...new Set(
143
+ entry.allBlockIds.filter(
144
+ (id): id is number => Number.isInteger(id) && id > 0,
145
+ ),
146
+ ),
147
+ ]
148
+ : []
149
+ const activeBlockIds = Array.isArray(entry.activeBlockIds)
150
+ ? [
151
+ ...new Set(
152
+ entry.activeBlockIds.filter(
153
+ (id): id is number => Number.isInteger(id) && id > 0,
154
+ ),
155
+ ),
156
+ ]
157
+ : []
158
+
159
+ state.byMessageId.set(messageId, {
160
+ tokenCount,
161
+ allBlockIds,
162
+ activeBlockIds,
163
+ })
164
+ }
165
+ }
166
+
167
+ if (persisted.blocksById && typeof persisted.blocksById === "object") {
168
+ for (const [blockIdStr, block] of Object.entries(persisted.blocksById)) {
169
+ const blockId = Number.parseInt(blockIdStr, 10)
170
+ if (!Number.isInteger(blockId) || blockId < 1 || !block || typeof block !== "object") {
171
+ continue
172
+ }
173
+
174
+ const toNumberArray = (value: unknown): number[] =>
175
+ Array.isArray(value)
176
+ ? [
177
+ ...new Set(
178
+ value.filter(
179
+ (item): item is number => Number.isInteger(item) && item > 0,
180
+ ),
181
+ ),
182
+ ]
183
+ : []
184
+ const toStringArray = (value: unknown): string[] =>
185
+ Array.isArray(value)
186
+ ? [...new Set(value.filter((item): item is string => typeof item === "string"))]
187
+ : []
188
+
189
+ state.blocksById.set(blockId, {
190
+ blockId,
191
+ runId:
192
+ typeof block.runId === "number" &&
193
+ Number.isInteger(block.runId) &&
194
+ block.runId > 0
195
+ ? block.runId
196
+ : blockId,
197
+ active: block.active === true,
198
+ deactivatedByUser: block.deactivatedByUser === true,
199
+ compressedTokens:
200
+ typeof block.compressedTokens === "number" &&
201
+ Number.isFinite(block.compressedTokens)
202
+ ? Math.max(0, block.compressedTokens)
203
+ : 0,
204
+ summaryTokens:
205
+ typeof block.summaryTokens === "number" && Number.isFinite(block.summaryTokens)
206
+ ? Math.max(0, block.summaryTokens)
207
+ : typeof block.summary === "string"
208
+ ? countTokens(block.summary)
209
+ : 0,
210
+ durationMs:
211
+ typeof block.durationMs === "number" && Number.isFinite(block.durationMs)
212
+ ? Math.max(0, block.durationMs)
213
+ : 0,
214
+ mode: block.mode === "rc" ? "rc" : undefined,
215
+ topic: typeof block.topic === "string" ? block.topic : "",
216
+ batchTopic:
217
+ typeof block.batchTopic === "string"
218
+ ? block.batchTopic
219
+ : typeof block.topic === "string"
220
+ ? block.topic
221
+ : "",
222
+ startId: typeof block.startId === "string" ? block.startId : "",
223
+ endId: typeof block.endId === "string" ? block.endId : "",
224
+ anchorMessageId:
225
+ typeof block.anchorMessageId === "string" ? block.anchorMessageId : "",
226
+ compressMessageId:
227
+ typeof block.compressMessageId === "string" ? block.compressMessageId : "",
228
+ compressCallId:
229
+ typeof block.compressCallId === "string" ? block.compressCallId : undefined,
230
+ includedBlockIds: toNumberArray(block.includedBlockIds),
231
+ consumedBlockIds: toNumberArray(block.consumedBlockIds),
232
+ parentBlockIds: toNumberArray(block.parentBlockIds),
233
+ directMessageIds: toStringArray(block.directMessageIds),
234
+ directToolIds: toStringArray(block.directToolIds),
235
+ effectiveMessageIds: toStringArray(block.effectiveMessageIds),
236
+ effectiveToolIds: toStringArray(block.effectiveToolIds),
237
+ createdAt: typeof block.createdAt === "number" ? block.createdAt : 0,
238
+ deactivatedAt:
239
+ typeof block.deactivatedAt === "number" ? block.deactivatedAt : undefined,
240
+ deactivatedByBlockId:
241
+ typeof block.deactivatedByBlockId === "number" &&
242
+ Number.isInteger(block.deactivatedByBlockId)
243
+ ? block.deactivatedByBlockId
244
+ : undefined,
245
+ summary: typeof block.summary === "string" ? block.summary : "",
246
+ })
247
+ }
248
+ }
249
+
250
+ if (Array.isArray(persisted.activeBlockIds)) {
251
+ for (const blockId of persisted.activeBlockIds) {
252
+ if (!Number.isInteger(blockId) || blockId < 1) {
253
+ continue
254
+ }
255
+ state.activeBlockIds.add(blockId)
256
+ }
257
+ }
258
+
259
+ if (
260
+ persisted.activeByAnchorMessageId &&
261
+ typeof persisted.activeByAnchorMessageId === "object"
262
+ ) {
263
+ for (const [anchorMessageId, blockId] of Object.entries(
264
+ persisted.activeByAnchorMessageId,
265
+ )) {
266
+ if (typeof blockId !== "number" || !Number.isInteger(blockId) || blockId < 1) {
267
+ continue
268
+ }
269
+ state.activeByAnchorMessageId.set(anchorMessageId, blockId)
270
+ }
271
+ }
272
+
273
+ for (const [blockId, block] of state.blocksById) {
274
+ if (block.active) {
275
+ state.activeBlockIds.add(blockId)
276
+ if (block.anchorMessageId) {
277
+ state.activeByAnchorMessageId.set(block.anchorMessageId, blockId)
278
+ }
279
+ }
280
+ if (blockId >= state.nextBlockId) {
281
+ state.nextBlockId = blockId + 1
282
+ }
283
+ if (block.runId >= state.nextRunId) {
284
+ state.nextRunId = block.runId + 1
285
+ }
286
+ }
287
+
288
+ return state
289
+ }
290
+
291
+ export function collectTurnNudgeAnchors(messages: WithParts[]): Set<string> {
292
+ const anchors = new Set<string>()
293
+ let pendingUserMessageId: string | null = null
294
+
295
+ for (let i = messages.length - 1; i >= 0; i--) {
296
+ const message = messages[i]
297
+
298
+ if (messageHasCompress(message)) {
299
+ break
300
+ }
301
+
302
+ if (message.info.role === "user") {
303
+ if (!isIgnoredUserMessage(message)) {
304
+ pendingUserMessageId = message.info.id
305
+ }
306
+ continue
307
+ }
308
+
309
+ if (message.info.role === "assistant" && pendingUserMessageId) {
310
+ anchors.add(message.info.id)
311
+ anchors.add(pendingUserMessageId)
312
+ pendingUserMessageId = null
313
+ }
314
+ }
315
+
316
+ return anchors
317
+ }
318
+
319
+ export function getActiveSummaryTokenUsage(state: SessionState): number {
320
+ let total = 0
321
+ for (const blockId of state.prune.messages.activeBlockIds) {
322
+ const block = state.prune.messages.blocksById.get(blockId)
323
+ if (!block || !block.active) {
324
+ continue
325
+ }
326
+ total += block.summaryTokens
327
+ }
328
+ return total
329
+ }
330
+
331
+ export function resetOnCompaction(state: SessionState): void {
332
+ state.toolParameters.clear()
333
+ state.prune.tools = new Map<string, number>()
334
+ state.prune.messages = createPruneMessagesState()
335
+ state.messageIds = {
336
+ byRawId: new Map<string, string>(),
337
+ byRef: new Map<string, string>(),
338
+ nextRef: 1,
339
+ }
340
+ state.nudges = {
341
+ contextLimitAnchors: new Set<string>(),
342
+ turnNudgeAnchors: new Set<string>(),
343
+ iterationNudgeAnchors: new Set<string>(),
344
+ }
345
+ }
@@ -0,0 +1,125 @@
1
+ import { PluginConfig } from "../config"
2
+ import { Logger } from "../logger"
3
+ import type { SessionState, WithParts } from "../state"
4
+ import { isToolProtected } from "../protected-patterns"
5
+ import { getTotalToolTokens } from "../token-utils"
6
+
7
+ /**
8
+ * Deduplication strategy - prunes older tool calls that have identical
9
+ * tool name and parameters, keeping only the most recent occurrence.
10
+ * Modifies the session state in place to add pruned tool call IDs.
11
+ */
12
+ export const deduplicate = (
13
+ state: SessionState,
14
+ logger: Logger,
15
+ config: PluginConfig,
16
+ messages: WithParts[],
17
+ ): void => {
18
+ if (state.manualMode && !config.manualMode.automaticStrategies) {
19
+ return
20
+ }
21
+
22
+ if (!config.strategies.deduplication.enabled) {
23
+ return
24
+ }
25
+
26
+ const allToolIds = state.toolIdList
27
+ if (allToolIds.length === 0) {
28
+ return
29
+ }
30
+
31
+ // Filter out IDs already pruned
32
+ const unprunedIds = allToolIds.filter((id) => !state.prune.tools.has(id))
33
+
34
+ if (unprunedIds.length === 0) {
35
+ return
36
+ }
37
+
38
+ const protectedTools = config.strategies.deduplication.protectedTools
39
+
40
+ // Group by signature (tool name + normalized parameters)
41
+ const signatureMap = new Map<string, string[]>()
42
+
43
+ for (const id of unprunedIds) {
44
+ const metadata = state.toolParameters.get(id)
45
+ if (!metadata) {
46
+ // logger.warn(`Missing metadata for tool call ID: ${id}`)
47
+ continue
48
+ }
49
+
50
+ if (
51
+ isToolProtected(
52
+ metadata.tool,
53
+ metadata.parameters,
54
+ protectedTools,
55
+ config.protectedFilePatterns,
56
+ metadata.metadata,
57
+ )
58
+ ) {
59
+ continue
60
+ }
61
+
62
+ const signature = createToolSignature(metadata.tool, metadata.parameters)
63
+ if (!signatureMap.has(signature)) {
64
+ signatureMap.set(signature, [])
65
+ }
66
+ const ids = signatureMap.get(signature)
67
+ if (ids) {
68
+ ids.push(id)
69
+ }
70
+ }
71
+
72
+ // Find duplicates - keep only the most recent (last) in each group
73
+ const newPruneIds: string[] = []
74
+
75
+ for (const [, ids] of signatureMap.entries()) {
76
+ if (ids.length > 1) {
77
+ // All except last (most recent) should be pruned
78
+ const idsToRemove = ids.slice(0, -1)
79
+ newPruneIds.push(...idsToRemove)
80
+ }
81
+ }
82
+
83
+ state.stats.totalPruneTokens += getTotalToolTokens(state, newPruneIds)
84
+
85
+ if (newPruneIds.length > 0) {
86
+ for (const id of newPruneIds) {
87
+ const entry = state.toolParameters.get(id)
88
+ state.prune.tools.set(id, entry?.tokenCount ?? 0)
89
+ }
90
+ logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`)
91
+ }
92
+ }
93
+
94
+ function createToolSignature(tool: string, parameters?: any): string {
95
+ if (!parameters) {
96
+ return tool
97
+ }
98
+ const normalized = normalizeParameters(parameters)
99
+ const sorted = sortObjectKeys(normalized)
100
+ return `${tool}::${JSON.stringify(sorted)}`
101
+ }
102
+
103
+ function normalizeParameters(params: any): any {
104
+ if (typeof params !== "object" || params === null) return params
105
+ if (Array.isArray(params)) return params
106
+
107
+ const normalized: any = {}
108
+ for (const [key, value] of Object.entries(params)) {
109
+ if (value !== undefined && value !== null) {
110
+ normalized[key] = value
111
+ }
112
+ }
113
+ return normalized
114
+ }
115
+
116
+ function sortObjectKeys(obj: any): any {
117
+ if (typeof obj !== "object" || obj === null) return obj
118
+ if (Array.isArray(obj)) return obj.map(sortObjectKeys)
119
+
120
+ const sorted: any = {}
121
+ for (const key of Object.keys(obj).sort()) {
122
+ sorted[key] = sortObjectKeys(obj[key])
123
+ }
124
+ return sorted
125
+ }
@@ -0,0 +1,2 @@
1
+ export { deduplicate } from "./deduplication"
2
+ export { purgeErrors } from "./purge-errors"
@@ -0,0 +1,86 @@
1
+ import { PluginConfig } from "../config"
2
+ import { Logger } from "../logger"
3
+ import type { SessionState, WithParts } from "../state"
4
+ import { isToolProtected } from "../protected-patterns"
5
+ import { getTotalToolTokens } from "../token-utils"
6
+
7
+ /**
8
+ * Purge Errors strategy - prunes tool inputs for tools that errored
9
+ * after they are older than a configurable number of turns.
10
+ * The error message is preserved, but the (potentially large) inputs
11
+ * are removed to save context.
12
+ *
13
+ * Modifies the session state in place to add pruned tool call IDs.
14
+ */
15
+ export const purgeErrors = (
16
+ state: SessionState,
17
+ logger: Logger,
18
+ config: PluginConfig,
19
+ messages: WithParts[],
20
+ ): void => {
21
+ if (state.manualMode && !config.manualMode.automaticStrategies) {
22
+ return
23
+ }
24
+
25
+ if (!config.strategies.purgeErrors.enabled) {
26
+ return
27
+ }
28
+
29
+ const allToolIds = state.toolIdList
30
+ if (allToolIds.length === 0) {
31
+ return
32
+ }
33
+
34
+ // Filter out IDs already pruned
35
+ const unprunedIds = allToolIds.filter((id) => !state.prune.tools.has(id))
36
+
37
+ if (unprunedIds.length === 0) {
38
+ return
39
+ }
40
+
41
+ const protectedTools = config.strategies.purgeErrors.protectedTools
42
+ const turnThreshold = Math.max(1, config.strategies.purgeErrors.turns)
43
+
44
+ const newPruneIds: string[] = []
45
+
46
+ for (const id of unprunedIds) {
47
+ const metadata = state.toolParameters.get(id)
48
+ if (!metadata) {
49
+ continue
50
+ }
51
+
52
+ if (
53
+ isToolProtected(
54
+ metadata.tool,
55
+ metadata.parameters,
56
+ protectedTools,
57
+ config.protectedFilePatterns,
58
+ metadata.metadata,
59
+ )
60
+ ) {
61
+ continue
62
+ }
63
+
64
+ // Only process error tools
65
+ if (metadata.status !== "error") {
66
+ continue
67
+ }
68
+
69
+ // Check if the tool is old enough to prune
70
+ const turnAge = state.currentTurn - metadata.turn
71
+ if (turnAge >= turnThreshold) {
72
+ newPruneIds.push(id)
73
+ }
74
+ }
75
+
76
+ if (newPruneIds.length > 0) {
77
+ state.stats.totalPruneTokens += getTotalToolTokens(state, newPruneIds)
78
+ for (const id of newPruneIds) {
79
+ const entry = state.toolParameters.get(id)
80
+ state.prune.tools.set(id, entry?.tokenCount ?? 0)
81
+ }
82
+ logger.debug(
83
+ `Marked ${newPruneIds.length} error tool calls for pruning (older than ${turnThreshold} turns)`,
84
+ )
85
+ }
86
+ }