@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,172 @@
1
+ import type { SessionState, WithParts } from "./state"
2
+ import { isIgnoredUserMessage } from "./messages/query"
3
+
4
+ const MESSAGE_REF_REGEX = /^m(\d{4})$/
5
+ const BLOCK_REF_REGEX = /^b([1-9]\d*)$/
6
+ const MESSAGE_ID_TAG_NAME = "dcp-message-id"
7
+
8
+ const MESSAGE_REF_WIDTH = 4
9
+ const MESSAGE_REF_MIN_INDEX = 1
10
+ export const MESSAGE_REF_MAX_INDEX = 9999
11
+
12
+ export type ParsedBoundaryId =
13
+ | {
14
+ kind: "message"
15
+ ref: string
16
+ index: number
17
+ }
18
+ | {
19
+ kind: "compressed-block"
20
+ ref: string
21
+ blockId: number
22
+ }
23
+
24
+ export function formatMessageRef(index: number): string {
25
+ if (
26
+ !Number.isInteger(index) ||
27
+ index < MESSAGE_REF_MIN_INDEX ||
28
+ index > MESSAGE_REF_MAX_INDEX
29
+ ) {
30
+ throw new Error(
31
+ `Message ID index out of bounds: ${index}. Supported range is 0-${MESSAGE_REF_MAX_INDEX}.`,
32
+ )
33
+ }
34
+ return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`
35
+ }
36
+
37
+ export function formatBlockRef(blockId: number): string {
38
+ if (!Number.isInteger(blockId) || blockId < 1) {
39
+ throw new Error(`Invalid block ID: ${blockId}`)
40
+ }
41
+ return `b${blockId}`
42
+ }
43
+
44
+ export function parseMessageRef(ref: string): number | null {
45
+ const normalized = ref.trim().toLowerCase()
46
+ const match = normalized.match(MESSAGE_REF_REGEX)
47
+ if (!match) {
48
+ return null
49
+ }
50
+ const index = Number.parseInt(match[1], 10)
51
+ if (!Number.isInteger(index)) {
52
+ return null
53
+ }
54
+ if (index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
55
+ return null
56
+ }
57
+ return index
58
+ }
59
+
60
+ export function parseBlockRef(ref: string): number | null {
61
+ const normalized = ref.trim().toLowerCase()
62
+ const match = normalized.match(BLOCK_REF_REGEX)
63
+ if (!match) {
64
+ return null
65
+ }
66
+ const id = Number.parseInt(match[1], 10)
67
+ return Number.isInteger(id) ? id : null
68
+ }
69
+
70
+ export function parseBoundaryId(id: string): ParsedBoundaryId | null {
71
+ const normalized = id.trim().toLowerCase()
72
+ const messageIndex = parseMessageRef(normalized)
73
+ if (messageIndex !== null) {
74
+ return {
75
+ kind: "message",
76
+ ref: formatMessageRef(messageIndex),
77
+ index: messageIndex,
78
+ }
79
+ }
80
+
81
+ const blockId = parseBlockRef(normalized)
82
+ if (blockId !== null) {
83
+ return {
84
+ kind: "compressed-block",
85
+ ref: formatBlockRef(blockId),
86
+ blockId,
87
+ }
88
+ }
89
+
90
+ return null
91
+ }
92
+
93
+ function escapeXmlAttribute(value: string): string {
94
+ return value
95
+ .replace(/&/g, "&amp;")
96
+ .replace(/"/g, "&quot;")
97
+ .replace(/</g, "&lt;")
98
+ .replace(/>/g, "&gt;")
99
+ }
100
+
101
+ export function formatMessageIdTag(
102
+ ref: string,
103
+ attributes?: Record<string, string | undefined>,
104
+ ): string {
105
+ const serializedAttributes = Object.entries(attributes || {})
106
+ .sort(([left], [right]) => left.localeCompare(right))
107
+ .map(([name, value]) => {
108
+ if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
109
+ return ""
110
+ }
111
+
112
+ return ` ${name}="${escapeXmlAttribute(value)}"`
113
+ })
114
+ .join("")
115
+
116
+ return `\n<${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`
117
+ }
118
+
119
+ export function assignMessageRefs(state: SessionState, messages: WithParts[]): number {
120
+ let assigned = 0
121
+ let skippedSubAgentPrompt = false
122
+
123
+ for (const message of messages) {
124
+ if (isIgnoredUserMessage(message)) {
125
+ continue
126
+ }
127
+
128
+ if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
129
+ skippedSubAgentPrompt = true
130
+ continue
131
+ }
132
+
133
+ const rawMessageId = message.info.id
134
+ if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
135
+ continue
136
+ }
137
+
138
+ const existingRef = state.messageIds.byRawId.get(rawMessageId)
139
+ if (existingRef) {
140
+ if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
141
+ state.messageIds.byRef.set(existingRef, rawMessageId)
142
+ }
143
+ continue
144
+ }
145
+
146
+ const ref = allocateNextMessageRef(state)
147
+ state.messageIds.byRawId.set(rawMessageId, ref)
148
+ state.messageIds.byRef.set(ref, rawMessageId)
149
+ assigned++
150
+ }
151
+
152
+ return assigned
153
+ }
154
+
155
+ function allocateNextMessageRef(state: SessionState): string {
156
+ let candidate = Number.isInteger(state.messageIds.nextRef)
157
+ ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef)
158
+ : MESSAGE_REF_MIN_INDEX
159
+
160
+ while (candidate <= MESSAGE_REF_MAX_INDEX) {
161
+ const ref = formatMessageRef(candidate)
162
+ if (!state.messageIds.byRef.has(ref)) {
163
+ state.messageIds.nextRef = candidate + 1
164
+ return ref
165
+ }
166
+ candidate++
167
+ }
168
+
169
+ throw new Error(
170
+ `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`,
171
+ )
172
+ }
@@ -0,0 +1,8 @@
1
+ export { prune } from "./prune"
2
+ export { syncCompressionBlocks } from "./sync"
3
+ export { injectCompressNudges } from "./inject/inject"
4
+ export { injectMessageIds } from "./inject/inject"
5
+ export { injectExtendedSubAgentResults } from "./inject/subagent-results"
6
+ export { stripStaleMetadata } from "./reasoning-strip"
7
+ export { buildPriorityMap } from "./priority"
8
+ export { buildToolIdList, stripHallucinations, stripHallucinationsFromString } from "./utils"
@@ -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 type { CompressionPriorityMap } from "../priority"
7
+ import { compressPermission } from "../../compress-permission"
8
+ import {
9
+ getLastUserMessage,
10
+ isIgnoredUserMessage,
11
+ isProtectedUserMessage,
12
+ messageHasCompress,
13
+ } from "../query"
14
+ import { saveSessionState } from "../../state/persistence"
15
+ import {
16
+ appendToTextPart,
17
+ appendToLastTextPart,
18
+ appendToAllToolParts,
19
+ createSyntheticTextPart,
20
+ hasContent,
21
+ } from "../utils"
22
+ import {
23
+ addAnchor,
24
+ applyAnchoredNudges,
25
+ countMessagesAfterIndex,
26
+ findLastNonIgnoredMessage,
27
+ getIterationNudgeThreshold,
28
+ getNudgeFrequency,
29
+ getModelInfo,
30
+ isContextOverLimits,
31
+ } from "./utils"
32
+
33
+ export const injectCompressNudges = (
34
+ state: SessionState,
35
+ config: PluginConfig,
36
+ logger: Logger,
37
+ messages: WithParts[],
38
+ prompts: RuntimePrompts,
39
+ compressionPriorities?: CompressionPriorityMap,
40
+ ): void => {
41
+ if (compressPermission(state, config) === "deny") {
42
+ return
43
+ }
44
+
45
+ if (state.manualMode) {
46
+ return
47
+ }
48
+
49
+ const lastMessage = findLastNonIgnoredMessage(messages)
50
+ const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant")
51
+
52
+ if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
53
+ state.nudges.contextLimitAnchors.clear()
54
+ state.nudges.turnNudgeAnchors.clear()
55
+ state.nudges.iterationNudgeAnchors.clear()
56
+ void saveSessionState(state, logger)
57
+ return
58
+ }
59
+
60
+ const { providerId, modelId } = getModelInfo(messages)
61
+ let anchorsChanged = false
62
+
63
+ const { overMaxLimit, overMinLimit } = isContextOverLimits(
64
+ config,
65
+ state,
66
+ providerId,
67
+ modelId,
68
+ messages,
69
+ )
70
+
71
+ if (!overMinLimit) {
72
+ const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0
73
+ const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0
74
+
75
+ if (hadTurnAnchors || hadIterationAnchors) {
76
+ state.nudges.turnNudgeAnchors.clear()
77
+ state.nudges.iterationNudgeAnchors.clear()
78
+ anchorsChanged = true
79
+ }
80
+ }
81
+
82
+ if (overMaxLimit) {
83
+ if (lastMessage) {
84
+ const interval = getNudgeFrequency(config)
85
+ const added = addAnchor(
86
+ state.nudges.contextLimitAnchors,
87
+ lastMessage.message.info.id,
88
+ lastMessage.index,
89
+ messages,
90
+ interval,
91
+ )
92
+ if (added) {
93
+ anchorsChanged = true
94
+ }
95
+ }
96
+ } else if (overMinLimit) {
97
+ const isLastMessageUser = lastMessage?.message.info.role === "user"
98
+
99
+ if (isLastMessageUser && lastAssistantMessage) {
100
+ const previousSize = state.nudges.turnNudgeAnchors.size
101
+ state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id)
102
+ state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id)
103
+ if (state.nudges.turnNudgeAnchors.size !== previousSize) {
104
+ anchorsChanged = true
105
+ }
106
+ }
107
+
108
+ const lastUserMessage = getLastUserMessage(messages)
109
+ if (lastUserMessage && lastMessage) {
110
+ const lastUserMessageIndex = messages.findIndex(
111
+ (message) => message.info.id === lastUserMessage.info.id,
112
+ )
113
+ if (lastUserMessageIndex >= 0) {
114
+ const messagesSinceUser = countMessagesAfterIndex(messages, lastUserMessageIndex)
115
+ const iterationThreshold = getIterationNudgeThreshold(config)
116
+
117
+ if (
118
+ lastMessage.index > lastUserMessageIndex &&
119
+ messagesSinceUser >= iterationThreshold
120
+ ) {
121
+ const interval = getNudgeFrequency(config)
122
+ const added = addAnchor(
123
+ state.nudges.iterationNudgeAnchors,
124
+ lastMessage.message.info.id,
125
+ lastMessage.index,
126
+ messages,
127
+ interval,
128
+ )
129
+
130
+ if (added) {
131
+ anchorsChanged = true
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ applyAnchoredNudges(state, config, messages, prompts, compressionPriorities)
139
+
140
+ if (anchorsChanged) {
141
+ void saveSessionState(state, logger)
142
+ }
143
+ }
144
+
145
+ export const injectMessageIds = (
146
+ state: SessionState,
147
+ config: PluginConfig,
148
+ messages: WithParts[],
149
+ compressionPriorities?: CompressionPriorityMap,
150
+ ): void => {
151
+ if (compressPermission(state, config) === "deny") {
152
+ return
153
+ }
154
+
155
+ for (const message of messages) {
156
+ if (isIgnoredUserMessage(message)) {
157
+ continue
158
+ }
159
+
160
+ const messageRef = state.messageIds.byRawId.get(message.info.id)
161
+ if (!messageRef) {
162
+ continue
163
+ }
164
+
165
+ const isBlockedMessage = isProtectedUserMessage(config, message)
166
+ const priority =
167
+ config.compress.mode === "message" && !isBlockedMessage
168
+ ? compressionPriorities?.get(message.info.id)?.priority
169
+ : undefined
170
+ const tag = formatMessageIdTag(
171
+ isBlockedMessage ? "BLOCKED" : messageRef,
172
+ priority ? { priority } : undefined,
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,82 @@
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" || 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
+ }