@tarquinen/opencode-dcp 3.2.5-beta0 → 3.2.7-beta0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/lib/token-utils.js +2 -2
  2. package/dist/lib/token-utils.js.map +1 -1
  3. package/index.ts +141 -0
  4. package/lib/analysis/tokens.ts +225 -0
  5. package/lib/auth.ts +37 -0
  6. package/lib/commands/compression-targets.ts +137 -0
  7. package/lib/commands/context.ts +132 -0
  8. package/lib/commands/decompress.ts +275 -0
  9. package/lib/commands/help.ts +76 -0
  10. package/lib/commands/index.ts +11 -0
  11. package/lib/commands/manual.ts +125 -0
  12. package/lib/commands/recompress.ts +224 -0
  13. package/lib/commands/stats.ts +148 -0
  14. package/lib/commands/sweep.ts +268 -0
  15. package/lib/compress/index.ts +3 -0
  16. package/lib/compress/message-utils.ts +250 -0
  17. package/lib/compress/message.ts +137 -0
  18. package/lib/compress/pipeline.ts +106 -0
  19. package/lib/compress/protected-content.ts +154 -0
  20. package/lib/compress/range-utils.ts +308 -0
  21. package/lib/compress/range.ts +180 -0
  22. package/lib/compress/search.ts +267 -0
  23. package/lib/compress/state.ts +268 -0
  24. package/lib/compress/timing.ts +77 -0
  25. package/lib/compress/types.ts +108 -0
  26. package/lib/compress-permission.ts +25 -0
  27. package/lib/config.ts +1071 -0
  28. package/lib/hooks.ts +378 -0
  29. package/lib/host-permissions.ts +101 -0
  30. package/lib/logger.ts +235 -0
  31. package/lib/message-ids.ts +172 -0
  32. package/lib/messages/index.ts +8 -0
  33. package/lib/messages/inject/inject.ts +215 -0
  34. package/lib/messages/inject/subagent-results.ts +82 -0
  35. package/lib/messages/inject/utils.ts +374 -0
  36. package/lib/messages/priority.ts +102 -0
  37. package/lib/messages/prune.ts +238 -0
  38. package/lib/messages/query.ts +56 -0
  39. package/lib/messages/reasoning-strip.ts +40 -0
  40. package/lib/messages/sync.ts +124 -0
  41. package/lib/messages/utils.ts +187 -0
  42. package/lib/prompts/compress-message.ts +42 -0
  43. package/lib/prompts/compress-range.ts +60 -0
  44. package/lib/prompts/context-limit-nudge.ts +18 -0
  45. package/lib/prompts/extensions/nudge.ts +43 -0
  46. package/lib/prompts/extensions/system.ts +32 -0
  47. package/lib/prompts/extensions/tool.ts +35 -0
  48. package/lib/prompts/index.ts +29 -0
  49. package/lib/prompts/iteration-nudge.ts +6 -0
  50. package/lib/prompts/store.ts +467 -0
  51. package/lib/prompts/system.ts +33 -0
  52. package/lib/prompts/turn-nudge.ts +10 -0
  53. package/lib/protected-patterns.ts +128 -0
  54. package/lib/state/index.ts +4 -0
  55. package/lib/state/persistence.ts +256 -0
  56. package/lib/state/state.ts +190 -0
  57. package/lib/state/tool-cache.ts +98 -0
  58. package/lib/state/types.ts +112 -0
  59. package/lib/state/utils.ts +334 -0
  60. package/lib/strategies/deduplication.ts +127 -0
  61. package/lib/strategies/index.ts +2 -0
  62. package/lib/strategies/purge-errors.ts +88 -0
  63. package/lib/subagents/subagent-results.ts +74 -0
  64. package/lib/token-utils.ts +162 -0
  65. package/lib/ui/notification.ts +346 -0
  66. package/lib/ui/utils.ts +287 -0
  67. package/package.json +12 -3
  68. package/tui/data/context.ts +177 -0
  69. package/tui/index.tsx +34 -0
  70. package/tui/routes/summary.tsx +175 -0
  71. package/tui/shared/names.ts +9 -0
  72. package/tui/shared/theme.ts +58 -0
  73. package/tui/shared/types.ts +38 -0
  74. package/tui/slots/sidebar-content.tsx +502 -0
@@ -0,0 +1,82 @@
1
+ import type { Logger } from "../../logger"
2
+ import type { SessionState, WithParts } from "../../state"
3
+ import {
4
+ buildSubagentResultText,
5
+ getSubAgentId,
6
+ mergeSubagentResult,
7
+ } from "../../subagents/subagent-results"
8
+ import { stripHallucinationsFromString } from "../utils"
9
+
10
+ async function fetchSubAgentMessages(client: any, sessionId: string): Promise<WithParts[]> {
11
+ const response = await client.session.messages({
12
+ path: { id: sessionId },
13
+ })
14
+
15
+ const payload = (response?.data || response) as WithParts[]
16
+ return Array.isArray(payload) ? payload : []
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" || part.tool !== "task" || !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
+ )
50
+ }
51
+ continue
52
+ }
53
+
54
+ const subAgentSessionId = getSubAgentId(part)
55
+ if (!subAgentSessionId) {
56
+ continue
57
+ }
58
+
59
+ let subAgentMessages: WithParts[] = []
60
+ try {
61
+ subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId)
62
+ } catch (error) {
63
+ logger.warn("Failed to fetch subagent session for output expansion", {
64
+ subAgentSessionId,
65
+ callID: part.callID,
66
+ error: error instanceof Error ? error.message : String(error),
67
+ })
68
+ continue
69
+ }
70
+
71
+ const subAgentResultText = buildSubagentResultText(subAgentMessages)
72
+ if (!subAgentResultText) {
73
+ continue
74
+ }
75
+
76
+ state.subAgentResultCache.set(part.callID, subAgentResultText)
77
+ part.state.output = stripHallucinationsFromString(
78
+ mergeSubagentResult(part.state.output, subAgentResultText),
79
+ )
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,374 @@
1
+ import type { SessionState, WithParts } from "../../state"
2
+ import type { PluginConfig } from "../../config"
3
+ import {
4
+ appendGuidanceToDcpTag,
5
+ buildCompressedBlockGuidance,
6
+ renderMessagePriorityGuidance,
7
+ } from "../../prompts/extensions/nudge"
8
+ import type { RuntimePrompts } from "../../prompts/store"
9
+ import type { UserMessage } from "@opencode-ai/sdk/v2"
10
+ import {
11
+ type CompressionPriorityMap,
12
+ type MessagePriority,
13
+ listPriorityRefsBeforeIndex,
14
+ } from "../priority"
15
+ import {
16
+ appendToTextPart,
17
+ appendToLastTextPart,
18
+ createSyntheticTextPart,
19
+ hasContent,
20
+ } from "../utils"
21
+ import { getLastUserMessage, isIgnoredUserMessage } from "../query"
22
+ import { getCurrentTokenUsage } from "../../token-utils"
23
+ import { getActiveSummaryTokenUsage } from "../../state/utils"
24
+
25
+ const MESSAGE_MODE_NUDGE_PRIORITY: MessagePriority = "high"
26
+
27
+ export interface LastUserModelContext {
28
+ providerId: string | undefined
29
+ modelId: string | undefined
30
+ }
31
+
32
+ export interface LastNonIgnoredMessage {
33
+ message: WithParts
34
+ index: number
35
+ }
36
+
37
+ export function getNudgeFrequency(config: PluginConfig): number {
38
+ return Math.max(1, Math.floor(config.compress.nudgeFrequency || 1))
39
+ }
40
+
41
+ export function getIterationNudgeThreshold(config: PluginConfig): number {
42
+ return Math.max(1, Math.floor(config.compress.iterationNudgeThreshold || 1))
43
+ }
44
+
45
+ export function findLastNonIgnoredMessage(messages: WithParts[]): LastNonIgnoredMessage | null {
46
+ for (let i = messages.length - 1; i >= 0; i--) {
47
+ const message = messages[i]
48
+ if (isIgnoredUserMessage(message)) {
49
+ continue
50
+ }
51
+ return { message, index: i }
52
+ }
53
+
54
+ return null
55
+ }
56
+
57
+ export function countMessagesAfterIndex(messages: WithParts[], index: number): number {
58
+ let count = 0
59
+
60
+ for (let i = index + 1; i < messages.length; i++) {
61
+ const message = messages[i]
62
+ if (isIgnoredUserMessage(message)) {
63
+ continue
64
+ }
65
+ count++
66
+ }
67
+
68
+ return count
69
+ }
70
+
71
+ export function getModelInfo(messages: WithParts[]): LastUserModelContext {
72
+ const lastUserMessage = getLastUserMessage(messages)
73
+ if (!lastUserMessage) {
74
+ return {
75
+ providerId: undefined,
76
+ modelId: undefined,
77
+ }
78
+ }
79
+
80
+ const userInfo = lastUserMessage.info as UserMessage
81
+ return {
82
+ providerId: userInfo.model.providerID,
83
+ modelId: userInfo.model.modelID,
84
+ }
85
+ }
86
+
87
+ function resolveContextTokenLimit(
88
+ config: PluginConfig,
89
+ state: SessionState,
90
+ providerId: string | undefined,
91
+ modelId: string | undefined,
92
+ threshold: "max" | "min",
93
+ ): number | undefined {
94
+ const parseLimitValue = (limit: number | `${number}%` | undefined): number | undefined => {
95
+ if (limit === undefined) {
96
+ return undefined
97
+ }
98
+
99
+ if (typeof limit === "number") {
100
+ return limit
101
+ }
102
+
103
+ if (!limit.endsWith("%") || state.modelContextLimit === undefined) {
104
+ return undefined
105
+ }
106
+
107
+ const parsedPercent = parseFloat(limit.slice(0, -1))
108
+ if (isNaN(parsedPercent)) {
109
+ return undefined
110
+ }
111
+
112
+ const roundedPercent = Math.round(parsedPercent)
113
+ const clampedPercent = Math.max(0, Math.min(100, roundedPercent))
114
+ return Math.round((clampedPercent / 100) * state.modelContextLimit)
115
+ }
116
+
117
+ const modelLimits =
118
+ threshold === "max" ? config.compress.modelMaxLimits : config.compress.modelMinLimits
119
+ if (modelLimits && providerId !== undefined && modelId !== undefined) {
120
+ const providerModelId = `${providerId}/${modelId}`
121
+ const modelLimit = modelLimits[providerModelId]
122
+ if (modelLimit !== undefined) {
123
+ return parseLimitValue(modelLimit)
124
+ }
125
+ }
126
+
127
+ const globalLimit =
128
+ threshold === "max" ? config.compress.maxContextLimit : config.compress.minContextLimit
129
+ return parseLimitValue(globalLimit)
130
+ }
131
+
132
+ export function isContextOverLimits(
133
+ config: PluginConfig,
134
+ state: SessionState,
135
+ providerId: string | undefined,
136
+ modelId: string | undefined,
137
+ messages: WithParts[],
138
+ ) {
139
+ const summaryTokenExtension = config.compress.summaryBuffer
140
+ ? getActiveSummaryTokenUsage(state)
141
+ : 0
142
+ const resolvedMaxContextLimit = resolveContextTokenLimit(
143
+ config,
144
+ state,
145
+ providerId,
146
+ modelId,
147
+ "max",
148
+ )
149
+ const maxContextLimit =
150
+ resolvedMaxContextLimit === undefined
151
+ ? undefined
152
+ : resolvedMaxContextLimit + summaryTokenExtension
153
+ const minContextLimit = resolveContextTokenLimit(config, state, providerId, modelId, "min")
154
+ const currentTokens = getCurrentTokenUsage(state, messages)
155
+
156
+ const overMaxLimit = maxContextLimit === undefined ? false : currentTokens > maxContextLimit
157
+ const overMinLimit = minContextLimit === undefined ? true : currentTokens >= minContextLimit
158
+
159
+ return {
160
+ overMaxLimit,
161
+ overMinLimit,
162
+ }
163
+ }
164
+
165
+ export function addAnchor(
166
+ anchorMessageIds: Set<string>,
167
+ anchorMessageId: string,
168
+ anchorMessageIndex: number,
169
+ messages: WithParts[],
170
+ interval: number,
171
+ ): boolean {
172
+ if (anchorMessageIndex < 0) {
173
+ return false
174
+ }
175
+
176
+ let latestAnchorMessageIndex = -1
177
+ for (let i = messages.length - 1; i >= 0; i--) {
178
+ if (anchorMessageIds.has(messages[i].info.id)) {
179
+ latestAnchorMessageIndex = i
180
+ break
181
+ }
182
+ }
183
+
184
+ const shouldAdd =
185
+ latestAnchorMessageIndex < 0 || anchorMessageIndex - latestAnchorMessageIndex >= interval
186
+ if (!shouldAdd) {
187
+ return false
188
+ }
189
+
190
+ const previousSize = anchorMessageIds.size
191
+ anchorMessageIds.add(anchorMessageId)
192
+ return anchorMessageIds.size !== previousSize
193
+ }
194
+
195
+ function buildMessagePriorityGuidance(
196
+ messages: WithParts[],
197
+ compressionPriorities: CompressionPriorityMap | undefined,
198
+ anchorIndex: number,
199
+ priority: MessagePriority,
200
+ ): string {
201
+ if (!compressionPriorities || compressionPriorities.size === 0) {
202
+ return ""
203
+ }
204
+
205
+ const refs = listPriorityRefsBeforeIndex(messages, compressionPriorities, anchorIndex, priority)
206
+ const priorityLabel = `${priority[0].toUpperCase()}${priority.slice(1)}`
207
+
208
+ return renderMessagePriorityGuidance(priorityLabel, refs)
209
+ }
210
+
211
+ function injectAnchoredNudge(message: WithParts, nudgeText: string): void {
212
+ if (!nudgeText.trim()) {
213
+ return
214
+ }
215
+
216
+ if (message.info.role === "user") {
217
+ if (appendToLastTextPart(message, nudgeText)) {
218
+ return
219
+ }
220
+
221
+ message.parts.push(createSyntheticTextPart(message, nudgeText))
222
+ return
223
+ }
224
+
225
+ if (message.info.role !== "assistant") {
226
+ return
227
+ }
228
+
229
+ if (!hasContent(message)) {
230
+ return
231
+ }
232
+
233
+ for (const part of message.parts) {
234
+ if (part.type === "text") {
235
+ if (appendToTextPart(part, nudgeText)) {
236
+ return
237
+ }
238
+ }
239
+ }
240
+
241
+ const syntheticPart = createSyntheticTextPart(message, nudgeText)
242
+ const firstToolIndex = message.parts.findIndex((p) => p.type === "tool")
243
+ if (firstToolIndex === -1) {
244
+ message.parts.push(syntheticPart)
245
+ } else {
246
+ message.parts.splice(firstToolIndex, 0, syntheticPart)
247
+ }
248
+ }
249
+
250
+ function collectAnchoredMessages(
251
+ anchorMessageIds: Set<string>,
252
+ messages: WithParts[],
253
+ ): Array<{ message: WithParts; index: number }> {
254
+ const anchoredMessages: Array<{ message: WithParts; index: number }> = []
255
+
256
+ for (const anchorMessageId of anchorMessageIds) {
257
+ const index = messages.findIndex((message) => message.info.id === anchorMessageId)
258
+ if (index === -1) {
259
+ continue
260
+ }
261
+
262
+ anchoredMessages.push({
263
+ message: messages[index],
264
+ index,
265
+ })
266
+ }
267
+
268
+ return anchoredMessages
269
+ }
270
+
271
+ function collectTurnNudgeAnchors(
272
+ state: SessionState,
273
+ config: PluginConfig,
274
+ messages: WithParts[],
275
+ ): Set<string> {
276
+ const turnNudgeAnchors = new Set<string>()
277
+ const targetRole = config.compress.nudgeForce === "strong" ? "user" : "assistant"
278
+
279
+ for (const message of messages) {
280
+ if (!state.nudges.turnNudgeAnchors.has(message.info.id)) continue
281
+
282
+ if (message.info.role === targetRole) {
283
+ turnNudgeAnchors.add(message.info.id)
284
+ }
285
+ }
286
+
287
+ return turnNudgeAnchors
288
+ }
289
+
290
+ function applyRangeModeAnchoredNudge(
291
+ anchorMessageIds: Set<string>,
292
+ messages: WithParts[],
293
+ baseNudgeText: string,
294
+ compressedBlockGuidance: string,
295
+ ): void {
296
+ const nudgeText = appendGuidanceToDcpTag(baseNudgeText, compressedBlockGuidance)
297
+ if (!nudgeText.trim()) {
298
+ return
299
+ }
300
+
301
+ for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
302
+ injectAnchoredNudge(message, nudgeText)
303
+ }
304
+ }
305
+
306
+ function applyMessageModeAnchoredNudge(
307
+ anchorMessageIds: Set<string>,
308
+ messages: WithParts[],
309
+ baseNudgeText: string,
310
+ compressionPriorities?: CompressionPriorityMap,
311
+ ): void {
312
+ for (const { message, index } of collectAnchoredMessages(anchorMessageIds, messages)) {
313
+ const priorityGuidance = buildMessagePriorityGuidance(
314
+ messages,
315
+ compressionPriorities,
316
+ index,
317
+ MESSAGE_MODE_NUDGE_PRIORITY,
318
+ )
319
+ const nudgeText = appendGuidanceToDcpTag(baseNudgeText, priorityGuidance)
320
+ injectAnchoredNudge(message, nudgeText)
321
+ }
322
+ }
323
+
324
+ export function applyAnchoredNudges(
325
+ state: SessionState,
326
+ config: PluginConfig,
327
+ messages: WithParts[],
328
+ prompts: RuntimePrompts,
329
+ compressionPriorities?: CompressionPriorityMap,
330
+ ): void {
331
+ const turnNudgeAnchors = collectTurnNudgeAnchors(state, config, messages)
332
+
333
+ if (config.compress.mode === "message") {
334
+ applyMessageModeAnchoredNudge(
335
+ state.nudges.contextLimitAnchors,
336
+ messages,
337
+ prompts.contextLimitNudge,
338
+ compressionPriorities,
339
+ )
340
+ applyMessageModeAnchoredNudge(
341
+ turnNudgeAnchors,
342
+ messages,
343
+ prompts.turnNudge,
344
+ compressionPriorities,
345
+ )
346
+ applyMessageModeAnchoredNudge(
347
+ state.nudges.iterationNudgeAnchors,
348
+ messages,
349
+ prompts.iterationNudge,
350
+ compressionPriorities,
351
+ )
352
+ return
353
+ }
354
+
355
+ const compressedBlockGuidance = buildCompressedBlockGuidance(state)
356
+ applyRangeModeAnchoredNudge(
357
+ state.nudges.contextLimitAnchors,
358
+ messages,
359
+ prompts.contextLimitNudge,
360
+ compressedBlockGuidance,
361
+ )
362
+ applyRangeModeAnchoredNudge(
363
+ turnNudgeAnchors,
364
+ messages,
365
+ prompts.turnNudge,
366
+ compressedBlockGuidance,
367
+ )
368
+ applyRangeModeAnchoredNudge(
369
+ state.nudges.iterationNudgeAnchors,
370
+ messages,
371
+ prompts.iterationNudge,
372
+ compressedBlockGuidance,
373
+ )
374
+ }
@@ -0,0 +1,102 @@
1
+ import type { PluginConfig } from "../config"
2
+ import { countAllMessageTokens } from "../token-utils"
3
+ import { isMessageCompacted } from "../state/utils"
4
+ import type { SessionState, WithParts } from "../state"
5
+ import { isIgnoredUserMessage, isProtectedUserMessage, messageHasCompress } from "./query"
6
+
7
+ const MEDIUM_PRIORITY_MIN_TOKENS = 500
8
+ const HIGH_PRIORITY_MIN_TOKENS = 5000
9
+
10
+ export type MessagePriority = "low" | "medium" | "high"
11
+
12
+ export interface CompressionPriorityEntry {
13
+ ref: string
14
+ tokenCount: number
15
+ priority: MessagePriority
16
+ }
17
+
18
+ export type CompressionPriorityMap = Map<string, CompressionPriorityEntry>
19
+
20
+ export function buildPriorityMap(
21
+ config: PluginConfig,
22
+ state: SessionState,
23
+ messages: WithParts[],
24
+ ): CompressionPriorityMap {
25
+ if (config.compress.mode !== "message") {
26
+ return new Map()
27
+ }
28
+ const priorities: CompressionPriorityMap = new Map()
29
+
30
+ for (const message of messages) {
31
+ if (isIgnoredUserMessage(message)) {
32
+ continue
33
+ }
34
+
35
+ if (isProtectedUserMessage(config, message)) {
36
+ continue
37
+ }
38
+
39
+ if (isMessageCompacted(state, message)) {
40
+ continue
41
+ }
42
+
43
+ const rawMessageId = message.info.id
44
+ if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
45
+ continue
46
+ }
47
+
48
+ const ref = state.messageIds.byRawId.get(rawMessageId)
49
+ if (!ref) {
50
+ continue
51
+ }
52
+
53
+ const tokenCount = countAllMessageTokens(message)
54
+ priorities.set(rawMessageId, {
55
+ ref,
56
+ tokenCount,
57
+ priority: messageHasCompress(message) ? "high" : classifyMessagePriority(tokenCount),
58
+ })
59
+ }
60
+
61
+ return priorities
62
+ }
63
+
64
+ export function classifyMessagePriority(tokenCount: number): MessagePriority {
65
+ if (tokenCount >= HIGH_PRIORITY_MIN_TOKENS) {
66
+ return "high"
67
+ }
68
+
69
+ if (tokenCount >= MEDIUM_PRIORITY_MIN_TOKENS) {
70
+ return "medium"
71
+ }
72
+
73
+ return "low"
74
+ }
75
+
76
+ export function listPriorityRefsBeforeIndex(
77
+ messages: WithParts[],
78
+ priorities: CompressionPriorityMap,
79
+ anchorIndex: number,
80
+ priority: MessagePriority,
81
+ ): string[] {
82
+ const refs: string[] = []
83
+ const seen = new Set<string>()
84
+ const upperBound = Math.max(0, Math.min(anchorIndex, messages.length))
85
+
86
+ for (let index = 0; index < upperBound; index++) {
87
+ const rawMessageId = messages[index]?.info.id
88
+ if (typeof rawMessageId !== "string") {
89
+ continue
90
+ }
91
+
92
+ const entry = priorities.get(rawMessageId)
93
+ if (!entry || entry.priority !== priority || seen.has(entry.ref)) {
94
+ continue
95
+ }
96
+
97
+ seen.add(entry.ref)
98
+ refs.push(entry.ref)
99
+ }
100
+
101
+ return refs
102
+ }