@tarquinen/opencode-dcp 3.2.4-beta0 → 3.2.6-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 (46) hide show
  1. package/dcp.schema.json +329 -0
  2. package/dist/lib/config.js +2 -2
  3. package/dist/lib/config.js.map +1 -1
  4. package/index.ts +141 -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-permission.ts +25 -0
  16. package/lib/config.ts +2 -2
  17. package/lib/hooks.ts +378 -0
  18. package/lib/host-permissions.ts +101 -0
  19. package/lib/messages/index.ts +8 -0
  20. package/lib/messages/inject/inject.ts +215 -0
  21. package/lib/messages/inject/subagent-results.ts +82 -0
  22. package/lib/messages/inject/utils.ts +374 -0
  23. package/lib/messages/priority.ts +102 -0
  24. package/lib/messages/prune.ts +238 -0
  25. package/lib/messages/reasoning-strip.ts +40 -0
  26. package/lib/messages/sync.ts +124 -0
  27. package/lib/messages/utils.ts +187 -0
  28. package/lib/prompts/compress-message.ts +42 -0
  29. package/lib/prompts/compress-range.ts +60 -0
  30. package/lib/prompts/context-limit-nudge.ts +18 -0
  31. package/lib/prompts/extensions/nudge.ts +43 -0
  32. package/lib/prompts/extensions/system.ts +32 -0
  33. package/lib/prompts/extensions/tool.ts +35 -0
  34. package/lib/prompts/index.ts +29 -0
  35. package/lib/prompts/iteration-nudge.ts +6 -0
  36. package/lib/prompts/store.ts +467 -0
  37. package/lib/prompts/system.ts +33 -0
  38. package/lib/prompts/turn-nudge.ts +10 -0
  39. package/lib/protected-patterns.ts +128 -0
  40. package/lib/strategies/deduplication.ts +127 -0
  41. package/lib/strategies/index.ts +2 -0
  42. package/lib/strategies/purge-errors.ts +88 -0
  43. package/lib/subagents/subagent-results.ts +74 -0
  44. package/lib/ui/notification.ts +346 -0
  45. package/lib/ui/utils.ts +287 -0
  46. package/package.json +14 -19
@@ -0,0 +1,101 @@
1
+ export type PermissionAction = "ask" | "allow" | "deny"
2
+
3
+ export type PermissionValue = PermissionAction | Record<string, PermissionAction>
4
+
5
+ export type PermissionConfig = Record<string, PermissionValue> | undefined
6
+
7
+ export interface HostPermissionSnapshot {
8
+ global: PermissionConfig
9
+ agents: Record<string, PermissionConfig>
10
+ }
11
+
12
+ type PermissionRule = {
13
+ permission: string
14
+ pattern: string
15
+ action: PermissionAction
16
+ }
17
+
18
+ const findLastMatchingRule = (
19
+ rules: PermissionRule[],
20
+ predicate: (rule: PermissionRule) => boolean,
21
+ ): PermissionRule | undefined => {
22
+ for (let index = rules.length - 1; index >= 0; index -= 1) {
23
+ const rule = rules[index]
24
+ if (rule && predicate(rule)) {
25
+ return rule
26
+ }
27
+ }
28
+
29
+ return undefined
30
+ }
31
+
32
+ const wildcardMatch = (value: string, pattern: string): boolean => {
33
+ const normalizedValue = value.replaceAll("\\", "/")
34
+ let escaped = pattern
35
+ .replaceAll("\\", "/")
36
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
37
+ .replace(/\*/g, ".*")
38
+ .replace(/\?/g, ".")
39
+
40
+ if (escaped.endsWith(" .*")) {
41
+ escaped = escaped.slice(0, -3) + "( .*)?"
42
+ }
43
+
44
+ const flags = process.platform === "win32" ? "si" : "s"
45
+ return new RegExp(`^${escaped}$`, flags).test(normalizedValue)
46
+ }
47
+
48
+ const getPermissionRules = (permissionConfigs: PermissionConfig[]): PermissionRule[] => {
49
+ const rules: PermissionRule[] = []
50
+ for (const permissionConfig of permissionConfigs) {
51
+ if (!permissionConfig) {
52
+ continue
53
+ }
54
+
55
+ for (const [permission, value] of Object.entries(permissionConfig)) {
56
+ if (value === "ask" || value === "allow" || value === "deny") {
57
+ rules.push({ permission, pattern: "*", action: value })
58
+ continue
59
+ }
60
+
61
+ for (const [pattern, action] of Object.entries(value)) {
62
+ if (action === "ask" || action === "allow" || action === "deny") {
63
+ rules.push({ permission, pattern, action })
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return rules
69
+ }
70
+
71
+ export const compressDisabledByOpencode = (...permissionConfigs: PermissionConfig[]): boolean => {
72
+ const match = findLastMatchingRule(getPermissionRules(permissionConfigs), (rule) =>
73
+ wildcardMatch("compress", rule.permission),
74
+ )
75
+
76
+ return match?.pattern === "*" && match.action === "deny"
77
+ }
78
+
79
+ export const resolveEffectiveCompressPermission = (
80
+ basePermission: PermissionAction,
81
+ hostPermissions: HostPermissionSnapshot,
82
+ agentName?: string,
83
+ ): PermissionAction => {
84
+ if (basePermission === "deny") {
85
+ return "deny"
86
+ }
87
+
88
+ return compressDisabledByOpencode(
89
+ hostPermissions.global,
90
+ agentName ? hostPermissions.agents[agentName] : undefined,
91
+ )
92
+ ? "deny"
93
+ : basePermission
94
+ }
95
+
96
+ export const hasExplicitToolPermission = (
97
+ permissionConfig: PermissionConfig,
98
+ tool: string,
99
+ ): boolean => {
100
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool) : false
101
+ }
@@ -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 {
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
+ }