@tarquinen/opencode-dcp 3.1.12 → 3.1.14

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 (106) hide show
  1. package/README.md +14 -9
  2. package/dist/index.js +105 -39
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/commands/context.d.ts +15 -0
  5. package/dist/lib/commands/context.d.ts.map +1 -1
  6. package/dist/lib/commands/help.d.ts +1 -0
  7. package/dist/lib/commands/help.d.ts.map +1 -1
  8. package/dist/lib/commands/manual.d.ts +1 -1
  9. package/dist/lib/commands/manual.d.ts.map +1 -1
  10. package/dist/lib/commands/stats.d.ts +10 -0
  11. package/dist/lib/commands/stats.d.ts.map +1 -1
  12. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  13. package/dist/lib/hooks.d.ts.map +1 -1
  14. package/dist/lib/state/persistence.d.ts +3 -0
  15. package/dist/lib/state/persistence.d.ts.map +1 -1
  16. package/dist/lib/state/state.d.ts +1 -0
  17. package/dist/lib/state/state.d.ts.map +1 -1
  18. package/dist/lib/tui/commands.d.ts +3 -0
  19. package/dist/lib/tui/commands.d.ts.map +1 -0
  20. package/dist/lib/tui/data.d.ts +14 -0
  21. package/dist/lib/tui/data.d.ts.map +1 -0
  22. package/dist/lib/tui/dialogs.d.ts +30 -0
  23. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  24. package/dist/lib/tui/format.d.ts +4 -0
  25. package/dist/lib/tui/format.d.ts.map +1 -0
  26. package/dist/lib/tui/modals.d.ts +10 -0
  27. package/dist/lib/tui/modals.d.ts.map +1 -0
  28. package/dist/lib/tui/types.d.ts +15 -0
  29. package/dist/lib/tui/types.d.ts.map +1 -0
  30. package/dist/lib/tui/ui.d.ts +48 -0
  31. package/dist/lib/tui/ui.d.ts.map +1 -0
  32. package/dist/tui.d.ts +7 -0
  33. package/dist/tui.d.ts.map +1 -0
  34. package/lib/auth.ts +37 -0
  35. package/lib/commands/compression-targets.ts +137 -0
  36. package/lib/commands/context.ts +305 -0
  37. package/lib/commands/decompress.ts +275 -0
  38. package/lib/commands/help.ts +76 -0
  39. package/lib/commands/index.ts +11 -0
  40. package/lib/commands/manual.ts +127 -0
  41. package/lib/commands/recompress.ts +224 -0
  42. package/lib/commands/stats.ts +159 -0
  43. package/lib/commands/sweep.ts +268 -0
  44. package/lib/compress/index.ts +3 -0
  45. package/lib/compress/message-utils.ts +250 -0
  46. package/lib/compress/message.ts +145 -0
  47. package/lib/compress/pipeline.ts +108 -0
  48. package/lib/compress/protected-content.ts +208 -0
  49. package/lib/compress/range-utils.ts +308 -0
  50. package/lib/compress/range.ts +192 -0
  51. package/lib/compress/search.ts +267 -0
  52. package/lib/compress/state.ts +268 -0
  53. package/lib/compress/timing.ts +77 -0
  54. package/lib/compress/types.ts +108 -0
  55. package/lib/compress-permission.ts +25 -0
  56. package/lib/config.ts +1007 -0
  57. package/lib/hooks.ts +375 -0
  58. package/lib/host-permissions.ts +101 -0
  59. package/lib/logger.ts +226 -0
  60. package/lib/message-ids.ts +172 -0
  61. package/lib/messages/index.ts +8 -0
  62. package/lib/messages/inject/inject.ts +215 -0
  63. package/lib/messages/inject/subagent-results.ts +82 -0
  64. package/lib/messages/inject/utils.ts +374 -0
  65. package/lib/messages/priority.ts +102 -0
  66. package/lib/messages/prune.ts +233 -0
  67. package/lib/messages/query.ts +72 -0
  68. package/lib/messages/reasoning-strip.ts +40 -0
  69. package/lib/messages/shape.ts +50 -0
  70. package/lib/messages/sync.ts +124 -0
  71. package/lib/messages/utils.ts +185 -0
  72. package/lib/prompts/compress-message.ts +43 -0
  73. package/lib/prompts/compress-range.ts +60 -0
  74. package/lib/prompts/context-limit-nudge.ts +18 -0
  75. package/lib/prompts/extensions/nudge.ts +43 -0
  76. package/lib/prompts/extensions/system.ts +32 -0
  77. package/lib/prompts/extensions/tool.ts +35 -0
  78. package/lib/prompts/index.ts +29 -0
  79. package/lib/prompts/iteration-nudge.ts +6 -0
  80. package/lib/prompts/store.ts +467 -0
  81. package/lib/prompts/system.ts +33 -0
  82. package/lib/prompts/turn-nudge.ts +10 -0
  83. package/lib/protected-patterns.ts +128 -0
  84. package/lib/state/index.ts +4 -0
  85. package/lib/state/persistence.ts +305 -0
  86. package/lib/state/state.ts +208 -0
  87. package/lib/state/tool-cache.ts +98 -0
  88. package/lib/state/types.ts +111 -0
  89. package/lib/state/utils.ts +345 -0
  90. package/lib/strategies/deduplication.ts +127 -0
  91. package/lib/strategies/index.ts +2 -0
  92. package/lib/strategies/purge-errors.ts +88 -0
  93. package/lib/subagents/subagent-results.ts +74 -0
  94. package/lib/token-utils.ts +164 -0
  95. package/lib/tui/commands.ts +31 -0
  96. package/lib/tui/data.ts +73 -0
  97. package/lib/tui/dialogs.tsx +235 -0
  98. package/lib/tui/format.ts +25 -0
  99. package/lib/tui/modals.tsx +92 -0
  100. package/lib/tui/types.ts +16 -0
  101. package/lib/tui/ui.tsx +219 -0
  102. package/lib/ui/notification.ts +347 -0
  103. package/lib/ui/utils.ts +304 -0
  104. package/lib/update.ts +185 -0
  105. package/package.json +21 -5
  106. package/tui.tsx +26 -0
@@ -0,0 +1,268 @@
1
+ import type { CompressionBlock, PruneMessagesState, SessionState } from "../state"
2
+ import { formatBlockRef, formatMessageIdTag } from "../message-ids"
3
+ import type { AppliedCompressionResult, CompressionStateInput, SelectionResolution } from "./types"
4
+
5
+ export const COMPRESSED_BLOCK_HEADER = "[Compressed conversation section]"
6
+
7
+ export function allocateBlockId(state: SessionState): number {
8
+ const next = state.prune.messages.nextBlockId
9
+ if (!Number.isInteger(next) || next < 1) {
10
+ state.prune.messages.nextBlockId = 2
11
+ return 1
12
+ }
13
+
14
+ state.prune.messages.nextBlockId = next + 1
15
+ return next
16
+ }
17
+
18
+ export function allocateRunId(state: SessionState): number {
19
+ const next = state.prune.messages.nextRunId
20
+ if (!Number.isInteger(next) || next < 1) {
21
+ state.prune.messages.nextRunId = 2
22
+ return 1
23
+ }
24
+
25
+ state.prune.messages.nextRunId = next + 1
26
+ return next
27
+ }
28
+
29
+ export function attachCompressionDuration(
30
+ messagesState: PruneMessagesState,
31
+ messageId: string,
32
+ callId: string,
33
+ durationMs: number,
34
+ ): number {
35
+ if (typeof durationMs !== "number" || !Number.isFinite(durationMs)) {
36
+ return 0
37
+ }
38
+
39
+ let updates = 0
40
+ for (const block of messagesState.blocksById.values()) {
41
+ if (block.compressMessageId !== messageId || block.compressCallId !== callId) {
42
+ continue
43
+ }
44
+
45
+ block.durationMs = durationMs
46
+ updates++
47
+ }
48
+
49
+ return updates
50
+ }
51
+
52
+ export function wrapCompressedSummary(blockId: number, summary: string): string {
53
+ const header = COMPRESSED_BLOCK_HEADER
54
+ const footer = formatMessageIdTag(formatBlockRef(blockId))
55
+ const body = summary.trim()
56
+ if (body.length === 0) {
57
+ return `${header}\n${footer}`
58
+ }
59
+ return `${header}\n${body}\n\n${footer}`
60
+ }
61
+
62
+ export function applyCompressionState(
63
+ state: SessionState,
64
+ input: CompressionStateInput,
65
+ selection: SelectionResolution,
66
+ anchorMessageId: string,
67
+ blockId: number,
68
+ summary: string,
69
+ consumedBlockIds: number[],
70
+ ): AppliedCompressionResult {
71
+ const messagesState = state.prune.messages
72
+ const consumed = [...new Set(consumedBlockIds.filter((id) => Number.isInteger(id) && id > 0))]
73
+ const included = [...consumed]
74
+
75
+ const effectiveMessageIds = new Set<string>(selection.messageIds)
76
+ const effectiveToolIds = new Set<string>(selection.toolIds)
77
+
78
+ for (const consumedBlockId of consumed) {
79
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
80
+ if (!consumedBlock) {
81
+ continue
82
+ }
83
+ for (const messageId of consumedBlock.effectiveMessageIds) {
84
+ effectiveMessageIds.add(messageId)
85
+ }
86
+ for (const toolId of consumedBlock.effectiveToolIds) {
87
+ effectiveToolIds.add(toolId)
88
+ }
89
+ }
90
+
91
+ const initiallyActiveMessages = new Set<string>()
92
+ for (const messageId of effectiveMessageIds) {
93
+ const entry = messagesState.byMessageId.get(messageId)
94
+ if (entry && entry.activeBlockIds.length > 0) {
95
+ initiallyActiveMessages.add(messageId)
96
+ }
97
+ }
98
+
99
+ const initiallyActiveToolIds = new Set<string>()
100
+ for (const activeBlockId of messagesState.activeBlockIds) {
101
+ const activeBlock = messagesState.blocksById.get(activeBlockId)
102
+ if (!activeBlock || !activeBlock.active) {
103
+ continue
104
+ }
105
+
106
+ for (const toolId of activeBlock.effectiveToolIds) {
107
+ initiallyActiveToolIds.add(toolId)
108
+ }
109
+ }
110
+
111
+ const createdAt = Date.now()
112
+ const block: CompressionBlock = {
113
+ blockId,
114
+ runId: input.runId,
115
+ active: true,
116
+ deactivatedByUser: false,
117
+ compressedTokens: 0,
118
+ summaryTokens: input.summaryTokens,
119
+ durationMs: 0,
120
+ mode: input.mode,
121
+ topic: input.topic,
122
+ batchTopic: input.batchTopic,
123
+ startId: input.startId,
124
+ endId: input.endId,
125
+ anchorMessageId,
126
+ compressMessageId: input.compressMessageId,
127
+ compressCallId: input.compressCallId,
128
+ includedBlockIds: included,
129
+ consumedBlockIds: consumed,
130
+ parentBlockIds: [],
131
+ directMessageIds: [],
132
+ directToolIds: [],
133
+ effectiveMessageIds: [...effectiveMessageIds],
134
+ effectiveToolIds: [...effectiveToolIds],
135
+ createdAt,
136
+ summary,
137
+ }
138
+
139
+ messagesState.blocksById.set(blockId, block)
140
+ messagesState.activeBlockIds.add(blockId)
141
+ messagesState.activeByAnchorMessageId.set(anchorMessageId, blockId)
142
+
143
+ const deactivatedAt = Date.now()
144
+ for (const consumedBlockId of consumed) {
145
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
146
+ if (!consumedBlock || !consumedBlock.active) {
147
+ continue
148
+ }
149
+
150
+ consumedBlock.active = false
151
+ consumedBlock.deactivatedAt = deactivatedAt
152
+ consumedBlock.deactivatedByBlockId = blockId
153
+ if (!consumedBlock.parentBlockIds.includes(blockId)) {
154
+ consumedBlock.parentBlockIds.push(blockId)
155
+ }
156
+
157
+ messagesState.activeBlockIds.delete(consumedBlockId)
158
+ const mappedBlockId = messagesState.activeByAnchorMessageId.get(
159
+ consumedBlock.anchorMessageId,
160
+ )
161
+ if (mappedBlockId === consumedBlockId) {
162
+ messagesState.activeByAnchorMessageId.delete(consumedBlock.anchorMessageId)
163
+ }
164
+ }
165
+
166
+ const removeActiveBlockId = (
167
+ entry: { activeBlockIds: number[] },
168
+ blockIdToRemove: number,
169
+ ): void => {
170
+ if (entry.activeBlockIds.length === 0) {
171
+ return
172
+ }
173
+ entry.activeBlockIds = entry.activeBlockIds.filter((id) => id !== blockIdToRemove)
174
+ }
175
+
176
+ for (const consumedBlockId of consumed) {
177
+ const consumedBlock = messagesState.blocksById.get(consumedBlockId)
178
+ if (!consumedBlock) {
179
+ continue
180
+ }
181
+ for (const messageId of consumedBlock.effectiveMessageIds) {
182
+ const entry = messagesState.byMessageId.get(messageId)
183
+ if (!entry) {
184
+ continue
185
+ }
186
+ removeActiveBlockId(entry, consumedBlockId)
187
+ }
188
+ }
189
+
190
+ for (const messageId of selection.messageIds) {
191
+ const tokenCount = selection.messageTokenById.get(messageId) || 0
192
+ const existing = messagesState.byMessageId.get(messageId)
193
+
194
+ if (!existing) {
195
+ messagesState.byMessageId.set(messageId, {
196
+ tokenCount,
197
+ allBlockIds: [blockId],
198
+ activeBlockIds: [blockId],
199
+ })
200
+ continue
201
+ }
202
+
203
+ existing.tokenCount = Math.max(existing.tokenCount, tokenCount)
204
+ if (!existing.allBlockIds.includes(blockId)) {
205
+ existing.allBlockIds.push(blockId)
206
+ }
207
+ if (!existing.activeBlockIds.includes(blockId)) {
208
+ existing.activeBlockIds.push(blockId)
209
+ }
210
+ }
211
+
212
+ for (const messageId of block.effectiveMessageIds) {
213
+ if (selection.messageTokenById.has(messageId)) {
214
+ continue
215
+ }
216
+
217
+ const existing = messagesState.byMessageId.get(messageId)
218
+ if (!existing) {
219
+ continue
220
+ }
221
+ if (!existing.allBlockIds.includes(blockId)) {
222
+ existing.allBlockIds.push(blockId)
223
+ }
224
+ if (!existing.activeBlockIds.includes(blockId)) {
225
+ existing.activeBlockIds.push(blockId)
226
+ }
227
+ }
228
+
229
+ let compressedTokens = 0
230
+ const newlyCompressedMessageIds: string[] = []
231
+ for (const messageId of effectiveMessageIds) {
232
+ const entry = messagesState.byMessageId.get(messageId)
233
+ if (!entry) {
234
+ continue
235
+ }
236
+
237
+ const isNowActive = entry.activeBlockIds.length > 0
238
+ const wasActive = initiallyActiveMessages.has(messageId)
239
+
240
+ if (isNowActive && !wasActive) {
241
+ compressedTokens += entry.tokenCount
242
+ newlyCompressedMessageIds.push(messageId)
243
+ }
244
+ }
245
+
246
+ const newlyCompressedToolIds: string[] = []
247
+ for (const toolId of effectiveToolIds) {
248
+ if (!initiallyActiveToolIds.has(toolId)) {
249
+ newlyCompressedToolIds.push(toolId)
250
+ }
251
+ }
252
+
253
+ block.directMessageIds = [...newlyCompressedMessageIds]
254
+ block.directToolIds = [...newlyCompressedToolIds]
255
+
256
+ block.compressedTokens = compressedTokens
257
+
258
+ state.stats.pruneTokenCounter += compressedTokens
259
+ state.stats.totalPruneTokens += state.stats.pruneTokenCounter
260
+ state.stats.pruneTokenCounter = 0
261
+
262
+ return {
263
+ compressedTokens,
264
+ messageIds: selection.messageIds,
265
+ newlyCompressedMessageIds,
266
+ newlyCompressedToolIds,
267
+ }
268
+ }
@@ -0,0 +1,77 @@
1
+ import type { SessionState } from "../state/types"
2
+ import { attachCompressionDuration } from "./state"
3
+
4
+ export interface PendingCompressionDuration {
5
+ messageId: string
6
+ callId: string
7
+ durationMs: number
8
+ }
9
+
10
+ export interface CompressionTimingState {
11
+ startsByCallId: Map<string, number>
12
+ pendingByCallId: Map<string, PendingCompressionDuration>
13
+ }
14
+
15
+ export function buildCompressionTimingKey(messageId: string, callId: string): string {
16
+ return `${messageId}:${callId}`
17
+ }
18
+
19
+ export function consumeCompressionStart(
20
+ state: SessionState,
21
+ messageId: string,
22
+ callId: string,
23
+ ): number | undefined {
24
+ const key = buildCompressionTimingKey(messageId, callId)
25
+ const start = state.compressionTiming.startsByCallId.get(key)
26
+ state.compressionTiming.startsByCallId.delete(key)
27
+ return start
28
+ }
29
+
30
+ export function resolveCompressionDuration(
31
+ startedAt: number | undefined,
32
+ eventTime: number | undefined,
33
+ partTime: { start?: unknown; end?: unknown } | undefined,
34
+ ): number | undefined {
35
+ const runningAt =
36
+ typeof partTime?.start === "number" && Number.isFinite(partTime.start)
37
+ ? partTime.start
38
+ : eventTime
39
+ const pendingToRunningMs =
40
+ typeof startedAt === "number" && typeof runningAt === "number"
41
+ ? Math.max(0, runningAt - startedAt)
42
+ : undefined
43
+
44
+ const toolStart = partTime?.start
45
+ const toolEnd = partTime?.end
46
+ const runtimeMs =
47
+ typeof toolStart === "number" &&
48
+ Number.isFinite(toolStart) &&
49
+ typeof toolEnd === "number" &&
50
+ Number.isFinite(toolEnd)
51
+ ? Math.max(0, toolEnd - toolStart)
52
+ : undefined
53
+
54
+ return typeof pendingToRunningMs === "number" ? pendingToRunningMs : runtimeMs
55
+ }
56
+
57
+ export function applyPendingCompressionDurations(state: SessionState): number {
58
+ if (state.compressionTiming.pendingByCallId.size === 0) {
59
+ return 0
60
+ }
61
+
62
+ let updates = 0
63
+ for (const [key, entry] of state.compressionTiming.pendingByCallId) {
64
+ const applied = attachCompressionDuration(
65
+ state.prune.messages,
66
+ entry.messageId,
67
+ entry.callId,
68
+ entry.durationMs,
69
+ )
70
+ if (applied > 0) {
71
+ updates += applied
72
+ state.compressionTiming.pendingByCallId.delete(key)
73
+ }
74
+ }
75
+
76
+ return updates
77
+ }
@@ -0,0 +1,108 @@
1
+ import type { PluginConfig } from "../config"
2
+ import type { Logger } from "../logger"
3
+ import type { PromptStore } from "../prompts/store"
4
+ import type { CompressionBlock, CompressionMode, SessionState, WithParts } from "../state"
5
+
6
+ export interface ToolContext {
7
+ client: any
8
+ state: SessionState
9
+ logger: Logger
10
+ config: PluginConfig
11
+ prompts: PromptStore
12
+ }
13
+
14
+ export interface CompressRangeEntry {
15
+ startId: string
16
+ endId: string
17
+ summary: string
18
+ }
19
+
20
+ export interface CompressRangeToolArgs {
21
+ topic: string
22
+ content: CompressRangeEntry[]
23
+ }
24
+
25
+ export interface CompressMessageEntry {
26
+ messageId: string
27
+ topic: string
28
+ summary: string
29
+ }
30
+
31
+ export interface CompressMessageToolArgs {
32
+ topic: string
33
+ content: CompressMessageEntry[]
34
+ }
35
+
36
+ export interface BoundaryReference {
37
+ kind: "message" | "compressed-block"
38
+ rawIndex: number
39
+ messageId?: string
40
+ blockId?: number
41
+ anchorMessageId?: string
42
+ }
43
+
44
+ export interface SearchContext {
45
+ rawMessages: WithParts[]
46
+ rawMessagesById: Map<string, WithParts>
47
+ rawIndexById: Map<string, number>
48
+ summaryByBlockId: Map<number, CompressionBlock>
49
+ }
50
+
51
+ export interface SelectionResolution {
52
+ startReference: BoundaryReference
53
+ endReference: BoundaryReference
54
+ messageIds: string[]
55
+ messageTokenById: Map<string, number>
56
+ toolIds: string[]
57
+ requiredBlockIds: number[]
58
+ }
59
+
60
+ export interface ResolvedMessageCompression {
61
+ entry: CompressMessageEntry
62
+ selection: SelectionResolution
63
+ anchorMessageId: string
64
+ }
65
+
66
+ export interface ResolvedRangeCompression {
67
+ index: number
68
+ entry: CompressRangeEntry
69
+ selection: SelectionResolution
70
+ anchorMessageId: string
71
+ }
72
+
73
+ export interface ResolvedMessageCompressionsResult {
74
+ plans: ResolvedMessageCompression[]
75
+ skippedIssues: string[]
76
+ skippedCount: number
77
+ }
78
+
79
+ export interface ParsedBlockPlaceholder {
80
+ raw: string
81
+ blockId: number
82
+ startIndex: number
83
+ endIndex: number
84
+ }
85
+
86
+ export interface InjectedSummaryResult {
87
+ expandedSummary: string
88
+ consumedBlockIds: number[]
89
+ }
90
+
91
+ export interface AppliedCompressionResult {
92
+ compressedTokens: number
93
+ messageIds: string[]
94
+ newlyCompressedMessageIds: string[]
95
+ newlyCompressedToolIds: string[]
96
+ }
97
+
98
+ export interface CompressionStateInput {
99
+ topic: string
100
+ batchTopic: string
101
+ startId: string
102
+ endId: string
103
+ mode: CompressionMode
104
+ runId: number
105
+ compressMessageId: string
106
+ compressCallId?: string
107
+ summaryTokens: number
108
+ }
@@ -0,0 +1,25 @@
1
+ import type { PluginConfig } from "./config"
2
+ import { type HostPermissionSnapshot, resolveEffectiveCompressPermission } from "./host-permissions"
3
+ import type { SessionState, WithParts } from "./state"
4
+ import { getLastUserMessage } from "./messages/query"
5
+
6
+ export const compressPermission = (
7
+ state: SessionState,
8
+ config: PluginConfig,
9
+ ): "ask" | "allow" | "deny" => {
10
+ return state.compressPermission ?? config.compress.permission
11
+ }
12
+
13
+ export const syncCompressPermissionState = (
14
+ state: SessionState,
15
+ config: PluginConfig,
16
+ hostPermissions: HostPermissionSnapshot,
17
+ messages: WithParts[],
18
+ ): void => {
19
+ const activeAgent = getLastUserMessage(messages)?.info.agent
20
+ state.compressPermission = resolveEffectiveCompressPermission(
21
+ config.compress.permission,
22
+ hostPermissions,
23
+ activeAgent,
24
+ )
25
+ }