@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,215 @@
1
+ import type { SessionState, WithParts } from "../../state"
2
+ import type { Logger } from "../../logger"
3
+ import type { PluginConfig } from "../../config"
4
+ import type { RuntimePrompts } from "../../prompts/store"
5
+ import { formatMessageIdTag } from "../../message-ids"
6
+ import { compressPermission } from "../../compress-permission"
7
+ import {
8
+ getLastUserMessage,
9
+ isIgnoredUserMessage,
10
+ isProtectedUserMessage,
11
+ messageHasCompress,
12
+ } from "../query"
13
+ import { saveSessionState } from "../../state/persistence"
14
+ import {
15
+ appendToTextPart,
16
+ appendToLastTextPart,
17
+ appendToAllToolParts,
18
+ createSyntheticTextPart,
19
+ hasContent,
20
+ } from "../utils"
21
+ import {
22
+ addAnchor,
23
+ applyAnchoredNudges,
24
+ countMessagesAfterIndex,
25
+ findLastNonIgnoredMessage,
26
+ getIterationNudgeThreshold,
27
+ getNudgeFrequency,
28
+ getModelInfo,
29
+ isContextOverLimits,
30
+ } from "./utils"
31
+
32
+ export const injectCompressNudges = (
33
+ state: SessionState,
34
+ config: PluginConfig,
35
+ logger: Logger,
36
+ messages: WithParts[],
37
+ prompts: RuntimePrompts,
38
+ createAnchors = true,
39
+ ): void => {
40
+ if (compressPermission(state, config) === "deny") {
41
+ return
42
+ }
43
+
44
+ if (state.manualMode) {
45
+ return
46
+ }
47
+
48
+ if (!createAnchors) {
49
+ applyAnchoredNudges(state, config, messages, prompts)
50
+ return
51
+ }
52
+
53
+ const lastMessage = findLastNonIgnoredMessage(messages)
54
+ const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant")
55
+
56
+ if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
57
+ state.nudges.contextLimitAnchors.clear()
58
+ state.nudges.turnNudgeAnchors.clear()
59
+ state.nudges.iterationNudgeAnchors.clear()
60
+ void saveSessionState(state, logger)
61
+ return
62
+ }
63
+
64
+ const { providerId, modelId } = getModelInfo(messages)
65
+ let anchorsChanged = false
66
+
67
+ const { overMaxLimit, overMinLimit } = isContextOverLimits(
68
+ config,
69
+ state,
70
+ providerId,
71
+ modelId,
72
+ messages,
73
+ )
74
+
75
+ if (!overMinLimit) {
76
+ const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0
77
+ const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0
78
+
79
+ if (hadTurnAnchors || hadIterationAnchors) {
80
+ state.nudges.turnNudgeAnchors.clear()
81
+ state.nudges.iterationNudgeAnchors.clear()
82
+ anchorsChanged = true
83
+ }
84
+ }
85
+
86
+ if (overMaxLimit) {
87
+ if (lastMessage) {
88
+ const interval = getNudgeFrequency(config)
89
+ const added = addAnchor(
90
+ state.nudges.contextLimitAnchors,
91
+ lastMessage.message.info.id,
92
+ lastMessage.index,
93
+ messages,
94
+ interval,
95
+ )
96
+ if (added) {
97
+ anchorsChanged = true
98
+ }
99
+ }
100
+ } else if (overMinLimit) {
101
+ const isLastMessageUser = lastMessage?.message.info.role === "user"
102
+
103
+ if (isLastMessageUser && lastAssistantMessage) {
104
+ const previousSize = state.nudges.turnNudgeAnchors.size
105
+ state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id)
106
+ state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id)
107
+ if (state.nudges.turnNudgeAnchors.size !== previousSize) {
108
+ anchorsChanged = true
109
+ }
110
+ }
111
+
112
+ const lastUserMessage = getLastUserMessage(messages)
113
+ if (lastUserMessage && lastMessage) {
114
+ const lastUserMessageIndex = messages.findIndex(
115
+ (message) => message.info.id === lastUserMessage.info.id,
116
+ )
117
+ if (lastUserMessageIndex >= 0) {
118
+ const messagesSinceUser = countMessagesAfterIndex(messages, lastUserMessageIndex)
119
+ const iterationThreshold = getIterationNudgeThreshold(config)
120
+
121
+ if (
122
+ lastMessage.index > lastUserMessageIndex &&
123
+ messagesSinceUser >= iterationThreshold
124
+ ) {
125
+ const interval = getNudgeFrequency(config)
126
+ const added = addAnchor(
127
+ state.nudges.iterationNudgeAnchors,
128
+ lastMessage.message.info.id,
129
+ lastMessage.index,
130
+ messages,
131
+ interval,
132
+ )
133
+
134
+ if (added) {
135
+ anchorsChanged = true
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ applyAnchoredNudges(state, config, messages, prompts)
143
+
144
+ if (anchorsChanged) {
145
+ void saveSessionState(state, logger)
146
+ }
147
+ }
148
+
149
+ export const injectMessageIds = (
150
+ state: SessionState,
151
+ config: PluginConfig,
152
+ messages: WithParts[],
153
+ ): void => {
154
+ if (compressPermission(state, config) === "deny") {
155
+ return
156
+ }
157
+
158
+ for (const message of messages) {
159
+ if (isIgnoredUserMessage(message)) {
160
+ continue
161
+ }
162
+
163
+ const messageRef = state.messageIds.byRawId.get(message.info.id)
164
+ if (!messageRef) {
165
+ continue
166
+ }
167
+
168
+ const isBlockedMessage = isProtectedUserMessage(config, message)
169
+ const tag = formatMessageIdTag(
170
+ isBlockedMessage ? "BLOCKED" : messageRef,
171
+ undefined,
172
+ state.idFormat,
173
+ )
174
+
175
+ if (message.info.role === "user") {
176
+ let injected = false
177
+ for (const part of message.parts) {
178
+ if (part.type === "text") {
179
+ injected = appendToTextPart(part, tag) || injected
180
+ }
181
+ }
182
+
183
+ if (injected) {
184
+ continue
185
+ }
186
+
187
+ message.parts.push(createSyntheticTextPart(message, tag))
188
+ continue
189
+ }
190
+
191
+ if (message.info.role !== "assistant") {
192
+ continue
193
+ }
194
+
195
+ if (!hasContent(message)) {
196
+ continue
197
+ }
198
+
199
+ if (appendToAllToolParts(message, tag)) {
200
+ continue
201
+ }
202
+
203
+ if (appendToLastTextPart(message, tag)) {
204
+ continue
205
+ }
206
+
207
+ const syntheticPart = createSyntheticTextPart(message, tag)
208
+ const firstToolIndex = message.parts.findIndex((p) => p.type === "tool")
209
+ if (firstToolIndex === -1) {
210
+ message.parts.push(syntheticPart)
211
+ } else {
212
+ message.parts.splice(firstToolIndex, 0, syntheticPart)
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,84 @@
1
+ import type { Logger } from "../../logger"
2
+ import type { SessionState, WithParts } from "../../state"
3
+ import { filterMessages } from "../shape"
4
+ import {
5
+ buildSubagentResultText,
6
+ getSubAgentId,
7
+ mergeSubagentResult,
8
+ } from "../../subagents/subagent-results"
9
+ import { stripHallucinationsFromString } from "../utils"
10
+
11
+ async function fetchSubAgentMessages(client: any, sessionId: string): Promise<WithParts[]> {
12
+ const response = await client.session.messages({
13
+ path: { id: sessionId },
14
+ })
15
+
16
+ return filterMessages(response?.data || response)
17
+ }
18
+
19
+ export const injectExtendedSubAgentResults = async (
20
+ client: any,
21
+ state: SessionState,
22
+ logger: Logger,
23
+ messages: WithParts[],
24
+ allowSubAgents: boolean,
25
+ ): Promise<void> => {
26
+ if (!allowSubAgents) {
27
+ return
28
+ }
29
+
30
+ for (const message of messages) {
31
+ const parts = Array.isArray(message.parts) ? message.parts : []
32
+
33
+ for (const part of parts) {
34
+ if (part.type !== "tool" || !["task", "subagent"].includes(part.tool) || !part.callID) {
35
+ continue
36
+ }
37
+ if (state.prune.tools.has(part.callID)) {
38
+ continue
39
+ }
40
+ if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
41
+ continue
42
+ }
43
+
44
+ const cachedResult = state.subAgentResultCache.get(part.callID)
45
+ if (cachedResult !== undefined) {
46
+ if (cachedResult) {
47
+ part.state.output = stripHallucinationsFromString(
48
+ mergeSubagentResult(part.state.output, cachedResult),
49
+ state.idFormat,
50
+ )
51
+ }
52
+ continue
53
+ }
54
+
55
+ const subAgentSessionId = getSubAgentId(part)
56
+ if (!subAgentSessionId) {
57
+ continue
58
+ }
59
+
60
+ let subAgentMessages: WithParts[] = []
61
+ try {
62
+ subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId)
63
+ } catch (error) {
64
+ logger.warn("Failed to fetch subagent session for output expansion", {
65
+ subAgentSessionId,
66
+ callID: part.callID,
67
+ error: error instanceof Error ? error.message : String(error),
68
+ })
69
+ continue
70
+ }
71
+
72
+ const subAgentResultText = buildSubagentResultText(subAgentMessages)
73
+ if (!subAgentResultText) {
74
+ continue
75
+ }
76
+
77
+ state.subAgentResultCache.set(part.callID, subAgentResultText)
78
+ part.state.output = stripHallucinationsFromString(
79
+ mergeSubagentResult(part.state.output, subAgentResultText),
80
+ state.idFormat,
81
+ )
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,304 @@
1
+ import type { SessionState, WithParts } from "../../state"
2
+ import type { PluginConfig } from "../../config"
3
+ import {
4
+ appendGuidanceToDcpTag,
5
+ buildCompressedBlockGuidance,
6
+ } from "../../prompts/extensions/nudge"
7
+ import type { RuntimePrompts } from "../../prompts/store"
8
+ import type { UserMessage } from "@opencode-ai/sdk/v2"
9
+ import {
10
+ appendToTextPart,
11
+ appendToLastTextPart,
12
+ createSyntheticTextPart,
13
+ hasContent,
14
+ } from "../utils"
15
+ import { getLastUserMessage, isIgnoredUserMessage } from "../query"
16
+ import { getCurrentTokenUsage } from "../../token-utils"
17
+ import { getActiveSummaryTokenUsage } from "../../state/utils"
18
+
19
+ export interface LastUserModelContext {
20
+ providerId: string | undefined
21
+ modelId: string | undefined
22
+ }
23
+
24
+ export interface LastNonIgnoredMessage {
25
+ message: WithParts
26
+ index: number
27
+ }
28
+
29
+ export function getNudgeFrequency(config: PluginConfig): number {
30
+ return Math.max(1, Math.floor(config.compress.nudgeFrequency || 1))
31
+ }
32
+
33
+ export function getIterationNudgeThreshold(config: PluginConfig): number {
34
+ return Math.max(1, Math.floor(config.compress.iterationNudgeThreshold || 1))
35
+ }
36
+
37
+ export function findLastNonIgnoredMessage(messages: WithParts[]): LastNonIgnoredMessage | null {
38
+ for (let i = messages.length - 1; i >= 0; i--) {
39
+ const message = messages[i]
40
+ if (isIgnoredUserMessage(message)) {
41
+ continue
42
+ }
43
+ return { message, index: i }
44
+ }
45
+
46
+ return null
47
+ }
48
+
49
+ export function countMessagesAfterIndex(messages: WithParts[], index: number): number {
50
+ let count = 0
51
+
52
+ for (let i = index + 1; i < messages.length; i++) {
53
+ const message = messages[i]
54
+ if (isIgnoredUserMessage(message)) {
55
+ continue
56
+ }
57
+ count++
58
+ }
59
+
60
+ return count
61
+ }
62
+
63
+ export function getModelInfo(messages: WithParts[]): LastUserModelContext {
64
+ const lastUserMessage = getLastUserMessage(messages)
65
+ if (!lastUserMessage) {
66
+ return {
67
+ providerId: undefined,
68
+ modelId: undefined,
69
+ }
70
+ }
71
+
72
+ const userInfo = lastUserMessage.info as UserMessage
73
+ return {
74
+ providerId: userInfo.model.providerID,
75
+ modelId: userInfo.model.modelID,
76
+ }
77
+ }
78
+
79
+ function resolveContextTokenLimit(
80
+ config: PluginConfig,
81
+ state: SessionState,
82
+ providerId: string | undefined,
83
+ modelId: string | undefined,
84
+ threshold: "max" | "min",
85
+ ): number | undefined {
86
+ const parseLimitValue = (limit: number | `${number}%` | undefined): number | undefined => {
87
+ if (limit === undefined) {
88
+ return undefined
89
+ }
90
+
91
+ if (typeof limit === "number") {
92
+ return limit
93
+ }
94
+
95
+ if (!limit.endsWith("%") || state.modelContextLimit === undefined) {
96
+ return undefined
97
+ }
98
+
99
+ const parsedPercent = parseFloat(limit.slice(0, -1))
100
+ if (isNaN(parsedPercent)) {
101
+ return undefined
102
+ }
103
+
104
+ const roundedPercent = Math.round(parsedPercent)
105
+ const clampedPercent = Math.max(0, Math.min(100, roundedPercent))
106
+ return Math.round((clampedPercent / 100) * state.modelContextLimit)
107
+ }
108
+
109
+ const modelLimits =
110
+ threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits
111
+ if (modelLimits && providerId !== undefined && modelId !== undefined) {
112
+ const providerModelId = `${providerId}/${modelId}`
113
+ const modelLimit = modelLimits[providerModelId]
114
+ if (modelLimit !== undefined) {
115
+ return parseLimitValue(modelLimit)
116
+ }
117
+ }
118
+
119
+ const globalLimit =
120
+ threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit
121
+ return parseLimitValue(globalLimit)
122
+ }
123
+
124
+ export function isContextOverLimits(
125
+ config: PluginConfig,
126
+ state: SessionState,
127
+ providerId: string | undefined,
128
+ modelId: string | undefined,
129
+ messages: WithParts[],
130
+ ) {
131
+ const summaryTokenExtension = config.compress.summaryBuffer
132
+ ? getActiveSummaryTokenUsage(state)
133
+ : 0
134
+ const resolvedMaxContextLimit = resolveContextTokenLimit(
135
+ config,
136
+ state,
137
+ providerId,
138
+ modelId,
139
+ "max",
140
+ )
141
+ const maxContextLimit =
142
+ resolvedMaxContextLimit === undefined
143
+ ? undefined
144
+ : resolvedMaxContextLimit + summaryTokenExtension
145
+ const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min")
146
+ const currentTokens = getCurrentTokenUsage(state, messages)
147
+
148
+ const overMaxLimit = maxContextLimit === undefined ? false : currentTokens > maxContextLimit
149
+ const overMinLimit = minContextLimit === undefined ? true : currentTokens >= minContextLimit
150
+
151
+ return {
152
+ overMaxLimit,
153
+ overMinLimit,
154
+ }
155
+ }
156
+
157
+ export function addAnchor(
158
+ anchorMessageIds: Set<string>,
159
+ anchorMessageId: string,
160
+ anchorMessageIndex: number,
161
+ messages: WithParts[],
162
+ interval: number,
163
+ ): boolean {
164
+ if (anchorMessageIndex < 0) {
165
+ return false
166
+ }
167
+
168
+ let latestAnchorMessageIndex = -1
169
+ for (let i = messages.length - 1; i >= 0; i--) {
170
+ if (anchorMessageIds.has(messages[i].info.id)) {
171
+ latestAnchorMessageIndex = i
172
+ break
173
+ }
174
+ }
175
+
176
+ const shouldAdd =
177
+ latestAnchorMessageIndex < 0 || anchorMessageIndex - latestAnchorMessageIndex >= interval
178
+ if (!shouldAdd) {
179
+ return false
180
+ }
181
+
182
+ const previousSize = anchorMessageIds.size
183
+ anchorMessageIds.add(anchorMessageId)
184
+ return anchorMessageIds.size !== previousSize
185
+ }
186
+
187
+ function injectAnchoredNudge(message: WithParts, nudgeText: string): void {
188
+ if (!nudgeText.trim()) {
189
+ return
190
+ }
191
+
192
+ if (message.info.role === "user") {
193
+ if (appendToLastTextPart(message, nudgeText)) {
194
+ return
195
+ }
196
+
197
+ message.parts.push(createSyntheticTextPart(message, nudgeText))
198
+ return
199
+ }
200
+
201
+ if (message.info.role !== "assistant") {
202
+ return
203
+ }
204
+
205
+ if (!hasContent(message)) {
206
+ return
207
+ }
208
+
209
+ for (const part of message.parts) {
210
+ if (part.type === "text") {
211
+ if (appendToTextPart(part, nudgeText)) {
212
+ return
213
+ }
214
+ }
215
+ }
216
+
217
+ const syntheticPart = createSyntheticTextPart(message, nudgeText)
218
+ const firstToolIndex = message.parts.findIndex((p) => p.type === "tool")
219
+ if (firstToolIndex === -1) {
220
+ message.parts.push(syntheticPart)
221
+ } else {
222
+ message.parts.splice(firstToolIndex, 0, syntheticPart)
223
+ }
224
+ }
225
+
226
+ function collectAnchoredMessages(
227
+ anchorMessageIds: Set<string>,
228
+ messages: WithParts[],
229
+ ): Array<{ message: WithParts; index: number }> {
230
+ const anchoredMessages: Array<{ message: WithParts; index: number }> = []
231
+
232
+ for (const anchorMessageId of anchorMessageIds) {
233
+ const index = messages.findIndex((message) => message.info.id === anchorMessageId)
234
+ if (index === -1) {
235
+ continue
236
+ }
237
+
238
+ anchoredMessages.push({
239
+ message: messages[index],
240
+ index,
241
+ })
242
+ }
243
+
244
+ return anchoredMessages
245
+ }
246
+
247
+ function collectTurnNudgeAnchors(
248
+ state: SessionState,
249
+ config: PluginConfig,
250
+ messages: WithParts[],
251
+ ): Set<string> {
252
+ const turnNudgeAnchors = new Set<string>()
253
+ const targetRole = config.compress.nudgeForce === "strong" ? "user" : "assistant"
254
+
255
+ for (const message of messages) {
256
+ if (!state.nudges.turnNudgeAnchors.has(message.info.id)) continue
257
+
258
+ if (message.info.role === targetRole) {
259
+ turnNudgeAnchors.add(message.info.id)
260
+ }
261
+ }
262
+
263
+ return turnNudgeAnchors
264
+ }
265
+
266
+ function applyGuidedAnchoredNudge(
267
+ anchorMessageIds: Set<string>,
268
+ messages: WithParts[],
269
+ baseNudgeText: string,
270
+ compressedBlockGuidance: string,
271
+ ): void {
272
+ const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance)
273
+ if (!nudgeText.trim()) {
274
+ return
275
+ }
276
+
277
+ for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
278
+ injectAnchoredNudge(message, nudgeText)
279
+ }
280
+ }
281
+
282
+ export function applyAnchoredNudges(
283
+ state: SessionState,
284
+ config: PluginConfig,
285
+ messages: WithParts[],
286
+ prompts: RuntimePrompts,
287
+ ): void {
288
+ const turnNudgeAnchors = collectTurnNudgeAnchors(state, config, messages)
289
+ const compressedBlockGuidance = buildCompressedBlockGuidance(state)
290
+
291
+ applyGuidedAnchoredNudge(
292
+ state.nudges.contextLimitAnchors,
293
+ messages,
294
+ prompts.contextLimitNudge,
295
+ compressedBlockGuidance,
296
+ )
297
+ applyGuidedAnchoredNudge(turnNudgeAnchors, messages, prompts.turnNudge, compressedBlockGuidance)
298
+ applyGuidedAnchoredNudge(
299
+ state.nudges.iterationNudgeAnchors,
300
+ messages,
301
+ prompts.iterationNudge,
302
+ compressedBlockGuidance,
303
+ )
304
+ }