@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.0

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 (209) 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 +7611 -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 +25 -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 +58 -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 +55 -0
  42. package/dist/lib/compress/types.d.ts.map +1 -0
  43. package/dist/lib/compress-permission.d.ts +4 -0
  44. package/dist/lib/compress-permission.d.ts.map +1 -0
  45. package/dist/lib/config.d.ts +83 -0
  46. package/dist/lib/config.d.ts.map +1 -0
  47. package/dist/lib/hooks.d.ts +22 -0
  48. package/dist/lib/hooks.d.ts.map +1 -0
  49. package/dist/lib/logger.d.ts +31 -0
  50. package/dist/lib/logger.d.ts.map +1 -0
  51. package/dist/lib/message-ids.d.ts +20 -0
  52. package/dist/lib/message-ids.d.ts.map +1 -0
  53. package/dist/lib/messages/index.d.ts +8 -0
  54. package/dist/lib/messages/index.d.ts.map +1 -0
  55. package/dist/lib/messages/inject/inject.d.ts +7 -0
  56. package/dist/lib/messages/inject/inject.d.ts.map +1 -0
  57. package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
  58. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
  59. package/dist/lib/messages/inject/utils.d.ts +23 -0
  60. package/dist/lib/messages/inject/utils.d.ts.map +1 -0
  61. package/dist/lib/messages/prune.d.ts +4 -0
  62. package/dist/lib/messages/prune.d.ts.map +1 -0
  63. package/dist/lib/messages/query.d.ts +7 -0
  64. package/dist/lib/messages/query.d.ts.map +1 -0
  65. package/dist/lib/messages/reasoning-strip.d.ts +8 -0
  66. package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
  67. package/dist/lib/messages/shape.d.ts +5 -0
  68. package/dist/lib/messages/shape.d.ts.map +1 -0
  69. package/dist/lib/messages/sync.d.ts +4 -0
  70. package/dist/lib/messages/sync.d.ts.map +1 -0
  71. package/dist/lib/messages/utils.d.ts +27 -0
  72. package/dist/lib/messages/utils.d.ts.map +1 -0
  73. package/dist/lib/prompts/compress-rc.d.ts +4 -0
  74. package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
  75. package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
  76. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
  77. package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
  78. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  79. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  80. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  81. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  82. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  83. package/dist/lib/prompts/index.d.ts +4 -0
  84. package/dist/lib/prompts/index.d.ts.map +1 -0
  85. package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
  86. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
  87. package/dist/lib/prompts/store.d.ts +26 -0
  88. package/dist/lib/prompts/store.d.ts.map +1 -0
  89. package/dist/lib/prompts/system.d.ts +4 -0
  90. package/dist/lib/prompts/system.d.ts.map +1 -0
  91. package/dist/lib/prompts/turn-nudge.d.ts +2 -0
  92. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
  93. package/dist/lib/protected-patterns.d.ts +6 -0
  94. package/dist/lib/protected-patterns.d.ts.map +1 -0
  95. package/dist/lib/state/index.d.ts +5 -0
  96. package/dist/lib/state/index.d.ts.map +1 -0
  97. package/dist/lib/state/persistence.d.ts +45 -0
  98. package/dist/lib/state/persistence.d.ts.map +1 -0
  99. package/dist/lib/state/state.d.ts +9 -0
  100. package/dist/lib/state/state.d.ts.map +1 -0
  101. package/dist/lib/state/tool-cache.d.ts +13 -0
  102. package/dist/lib/state/tool-cache.d.ts.map +1 -0
  103. package/dist/lib/state/types.d.ts +102 -0
  104. package/dist/lib/state/types.d.ts.map +1 -0
  105. package/dist/lib/state/utils.d.ts +22 -0
  106. package/dist/lib/state/utils.d.ts.map +1 -0
  107. package/dist/lib/strategies/deduplication.d.ts +10 -0
  108. package/dist/lib/strategies/deduplication.d.ts.map +1 -0
  109. package/dist/lib/strategies/index.d.ts +3 -0
  110. package/dist/lib/strategies/index.d.ts.map +1 -0
  111. package/dist/lib/strategies/purge-errors.d.ts +13 -0
  112. package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
  113. package/dist/lib/subagents/subagent-results.d.ts +5 -0
  114. package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
  115. package/dist/lib/token-utils.d.ts +19 -0
  116. package/dist/lib/token-utils.d.ts.map +1 -0
  117. package/dist/lib/tui/dialogs.d.ts +29 -0
  118. package/dist/lib/tui/dialogs.d.ts.map +1 -0
  119. package/dist/lib/tui/format.d.ts +4 -0
  120. package/dist/lib/tui/format.d.ts.map +1 -0
  121. package/dist/lib/tui/types.d.ts +18 -0
  122. package/dist/lib/tui/types.d.ts.map +1 -0
  123. package/dist/lib/tui/ui.d.ts +48 -0
  124. package/dist/lib/tui/ui.d.ts.map +1 -0
  125. package/dist/lib/ui/notification.d.ts +17 -0
  126. package/dist/lib/ui/notification.d.ts.map +1 -0
  127. package/dist/lib/ui/utils.d.ts +10 -0
  128. package/dist/lib/ui/utils.d.ts.map +1 -0
  129. package/dist/lib/v2/index.d.ts +5 -0
  130. package/dist/lib/v2/index.d.ts.map +1 -0
  131. package/dist/lib/v2/messages.d.ts +13 -0
  132. package/dist/lib/v2/messages.d.ts.map +1 -0
  133. package/dist/lib/v2/rpc.d.ts +55 -0
  134. package/dist/lib/v2/rpc.d.ts.map +1 -0
  135. package/dist/lib/v2/theme.d.ts +55 -0
  136. package/dist/lib/v2/theme.d.ts.map +1 -0
  137. package/dist/lib/v2/tui.d.ts +4 -0
  138. package/dist/lib/v2/tui.d.ts.map +1 -0
  139. package/dist/tui.d.ts +7 -0
  140. package/dist/tui.d.ts.map +1 -0
  141. package/lib/commands/compression-targets.ts +54 -0
  142. package/lib/commands/context.ts +305 -0
  143. package/lib/commands/decompress.ts +273 -0
  144. package/lib/commands/help.ts +76 -0
  145. package/lib/commands/index.ts +11 -0
  146. package/lib/commands/manual.ts +126 -0
  147. package/lib/commands/recompress.ts +222 -0
  148. package/lib/commands/stats.ts +159 -0
  149. package/lib/commands/sweep.ts +266 -0
  150. package/lib/compress/index.ts +2 -0
  151. package/lib/compress/pipeline.ts +116 -0
  152. package/lib/compress/protected-content.ts +163 -0
  153. package/lib/compress/rc.ts +458 -0
  154. package/lib/compress/search.ts +277 -0
  155. package/lib/compress/state.ts +272 -0
  156. package/lib/compress/summary.ts +293 -0
  157. package/lib/compress/timing.ts +77 -0
  158. package/lib/compress/types.ts +57 -0
  159. package/lib/compress-permission.ts +9 -0
  160. package/lib/config.ts +995 -0
  161. package/lib/hooks.ts +205 -0
  162. package/lib/logger.ts +226 -0
  163. package/lib/message-ids.ts +181 -0
  164. package/lib/messages/index.ts +7 -0
  165. package/lib/messages/inject/inject.ts +215 -0
  166. package/lib/messages/inject/subagent-results.ts +84 -0
  167. package/lib/messages/inject/utils.ts +304 -0
  168. package/lib/messages/prune.ts +238 -0
  169. package/lib/messages/query.ts +71 -0
  170. package/lib/messages/reasoning-strip.ts +40 -0
  171. package/lib/messages/shape.ts +50 -0
  172. package/lib/messages/sync.ts +124 -0
  173. package/lib/messages/utils.ts +190 -0
  174. package/lib/prompts/compress-rc.ts +25 -0
  175. package/lib/prompts/context-limit-nudge.ts +18 -0
  176. package/lib/prompts/extensions/nudge.ts +34 -0
  177. package/lib/prompts/extensions/system.ts +32 -0
  178. package/lib/prompts/extensions/tool.ts +21 -0
  179. package/lib/prompts/index.ts +29 -0
  180. package/lib/prompts/iteration-nudge.ts +6 -0
  181. package/lib/prompts/store.ts +452 -0
  182. package/lib/prompts/system.ts +41 -0
  183. package/lib/prompts/turn-nudge.ts +10 -0
  184. package/lib/protected-patterns.ts +159 -0
  185. package/lib/state/index.ts +4 -0
  186. package/lib/state/persistence.ts +305 -0
  187. package/lib/state/state.ts +210 -0
  188. package/lib/state/tool-cache.ts +99 -0
  189. package/lib/state/types.ts +114 -0
  190. package/lib/state/utils.ts +345 -0
  191. package/lib/strategies/deduplication.ts +125 -0
  192. package/lib/strategies/index.ts +2 -0
  193. package/lib/strategies/purge-errors.ts +86 -0
  194. package/lib/subagents/subagent-results.ts +75 -0
  195. package/lib/token-utils.ts +164 -0
  196. package/lib/tui/dialogs.tsx +237 -0
  197. package/lib/tui/format.ts +25 -0
  198. package/lib/tui/types.ts +25 -0
  199. package/lib/tui/ui.tsx +245 -0
  200. package/lib/ui/notification.ts +347 -0
  201. package/lib/ui/utils.ts +304 -0
  202. package/lib/v2/index.ts +401 -0
  203. package/lib/v2/messages.ts +252 -0
  204. package/lib/v2/rpc.ts +48 -0
  205. package/lib/v2/theme.ts +67 -0
  206. package/lib/v2/tui.tsx +115 -0
  207. package/package.json +91 -5
  208. package/server.js +1 -0
  209. package/tui.tsx +7 -0
package/lib/hooks.ts ADDED
@@ -0,0 +1,205 @@
1
+ import type { SessionState, WithParts } from "./state"
2
+ import type { Logger } from "./logger"
3
+ import type { PluginConfig } from "./config"
4
+ import { renderSystemPrompt, type PromptStore } from "./prompts"
5
+ import { buildProtectedToolsExtension } from "./prompts/extensions/system"
6
+ import { filterMessages } from "./messages/shape"
7
+ import {
8
+ handleContextCommand,
9
+ handleDecompressCommand,
10
+ handleHelpCommand,
11
+ handleManualToggleCommand,
12
+ handleManualTriggerCommand,
13
+ handleRecompressCommand,
14
+ handleStatsCommand,
15
+ handleSweepCommand,
16
+ } from "./commands"
17
+ import { compressPermission } from "./compress-permission"
18
+ import { ensureSessionInitialized } from "./state"
19
+
20
+ const INTERNAL_AGENT_SIGNATURES = [
21
+ "You are a title generator",
22
+ "You are a helpful AI assistant tasked with summarizing conversations",
23
+ "You are an anchored context summarization assistant for coding sessions",
24
+ "Summarize what was done in this conversation",
25
+ ]
26
+
27
+ function isInternalAgentCall(systemPrompts: string[]): boolean {
28
+ const primaryPrompt = systemPrompts[0]
29
+ if (typeof primaryPrompt !== "string" || primaryPrompt.length === 0) {
30
+ return false
31
+ }
32
+
33
+ return INTERNAL_AGENT_SIGNATURES.some((signature) => primaryPrompt.includes(signature))
34
+ }
35
+
36
+ export function createSystemPromptHandler(
37
+ state: SessionState,
38
+ logger: Logger,
39
+ config: PluginConfig,
40
+ prompts: PromptStore,
41
+ ) {
42
+ return async (
43
+ input: { sessionID?: string; model: { limit: { context: number } } },
44
+ output: { system: string[] },
45
+ ) => {
46
+ if (input.model?.limit?.context) {
47
+ state.modelContextLimit = input.model.limit.context
48
+ logger.debug("Cached model context limit", { limit: state.modelContextLimit })
49
+ }
50
+
51
+ if (state.isSubAgent && !config.experimental.allowSubAgents) {
52
+ return
53
+ }
54
+
55
+ if (isInternalAgentCall(output.system)) {
56
+ logger.info("Skipping DCP system prompt injection for internal agent")
57
+ return
58
+ }
59
+
60
+ const effectivePermission =
61
+ input.sessionID && state.sessionId === input.sessionID
62
+ ? compressPermission(state, config)
63
+ : config.compress.permission
64
+
65
+ if (effectivePermission === "deny") {
66
+ return
67
+ }
68
+
69
+ prompts.reload()
70
+ const runtimePrompts = prompts.getRuntimePrompts()
71
+ const newPrompt = renderSystemPrompt(
72
+ runtimePrompts,
73
+ buildProtectedToolsExtension(config.compress.protectedTools),
74
+ !!state.manualMode,
75
+ state.isSubAgent && config.experimental.allowSubAgents,
76
+ )
77
+ if (output.system.length > 0) {
78
+ output.system[output.system.length - 1] += "\n\n" + newPrompt
79
+ } else {
80
+ output.system.push(newPrompt)
81
+ }
82
+ }
83
+ }
84
+
85
+ export function createCommandExecuteHandler(
86
+ client: any,
87
+ state: SessionState,
88
+ logger: Logger,
89
+ config: PluginConfig,
90
+ workingDirectory: string,
91
+ ) {
92
+ return async (
93
+ input: { command: string; sessionID: string; arguments: string },
94
+ output: { parts: any[] },
95
+ ) => {
96
+ if (!config.commands.enabled) {
97
+ return
98
+ }
99
+
100
+ if (input.command === "dcp" || input.command === "dcp-compress") {
101
+ const messagesResponse = await client.session.messages({
102
+ path: { id: input.sessionID },
103
+ })
104
+ const messages: WithParts[] = filterMessages(messagesResponse.data || messagesResponse)
105
+
106
+ await ensureSessionInitialized(
107
+ client,
108
+ state,
109
+ input.sessionID,
110
+ logger,
111
+ messages,
112
+ config.manualMode.enabled,
113
+ )
114
+
115
+ const effectivePermission = compressPermission(state, config)
116
+ if (effectivePermission === "deny") {
117
+ return
118
+ }
119
+
120
+ const args = (input.arguments || "").trim().split(/\s+/).filter(Boolean)
121
+ const isCompressCommand = input.command === "dcp-compress"
122
+ const subcommand = isCompressCommand ? "compress" : args[0]?.toLowerCase() || ""
123
+ const subArgs = isCompressCommand ? args : args.slice(1)
124
+
125
+ const commandCtx = {
126
+ client,
127
+ state,
128
+ config,
129
+ logger,
130
+ sessionId: input.sessionID,
131
+ messages,
132
+ }
133
+
134
+ if (subcommand === "context") {
135
+ await handleContextCommand(commandCtx)
136
+ return
137
+ }
138
+
139
+ if (subcommand === "stats") {
140
+ await handleStatsCommand(commandCtx)
141
+ return
142
+ }
143
+
144
+ if (subcommand === "sweep") {
145
+ await handleSweepCommand({
146
+ ...commandCtx,
147
+ args: subArgs,
148
+ workingDirectory,
149
+ })
150
+ return
151
+ }
152
+
153
+ if (subcommand === "manual") {
154
+ await handleManualToggleCommand(commandCtx, subArgs[0]?.toLowerCase())
155
+ return
156
+ }
157
+
158
+ if (subcommand === "compress") {
159
+ const userFocus = subArgs.join(" ").trim()
160
+ const prompt = await handleManualTriggerCommand(commandCtx, "compress", userFocus)
161
+ if (!prompt) {
162
+ throw new Error("__DCP_MANUAL_TRIGGER_BLOCKED__")
163
+ }
164
+
165
+ state.manualMode = "compress-pending"
166
+ state.pendingManualTrigger = {
167
+ sessionId: input.sessionID,
168
+ prompt,
169
+ }
170
+ const rawArgs = (input.arguments || "").trim()
171
+ output.parts.length = 0
172
+ output.parts.push({
173
+ type: "text",
174
+ text: isCompressCommand
175
+ ? rawArgs
176
+ ? `/dcp-compress ${rawArgs}`
177
+ : "/dcp-compress"
178
+ : rawArgs
179
+ ? `/dcp ${rawArgs}`
180
+ : `/dcp ${subcommand}`,
181
+ })
182
+ return
183
+ }
184
+
185
+ if (subcommand === "decompress") {
186
+ await handleDecompressCommand({
187
+ ...commandCtx,
188
+ args: subArgs,
189
+ })
190
+ return
191
+ }
192
+
193
+ if (subcommand === "recompress") {
194
+ await handleRecompressCommand({
195
+ ...commandCtx,
196
+ args: subArgs,
197
+ })
198
+ return
199
+ }
200
+
201
+ await handleHelpCommand(commandCtx)
202
+ return
203
+ }
204
+ }
205
+ }
package/lib/logger.ts ADDED
@@ -0,0 +1,226 @@
1
+ import { writeFile, mkdir } from "fs/promises"
2
+ import { join } from "path"
3
+ import { existsSync } from "fs"
4
+ import { homedir } from "os"
5
+
6
+ export class Logger {
7
+ private logDir: string
8
+ public enabled: boolean
9
+
10
+ constructor(enabled: boolean) {
11
+ this.enabled = enabled
12
+ const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config")
13
+ this.logDir = join(configHome, "opencode", "logs", "dcp")
14
+ }
15
+
16
+ private async ensureLogDir() {
17
+ if (!existsSync(this.logDir)) {
18
+ await mkdir(this.logDir, { recursive: true })
19
+ }
20
+ }
21
+
22
+ private formatData(data?: any): string {
23
+ if (!data) return ""
24
+
25
+ const parts: string[] = []
26
+ for (const [key, value] of Object.entries(data)) {
27
+ if (value === undefined || value === null) continue
28
+
29
+ // Format arrays compactly
30
+ if (Array.isArray(value)) {
31
+ if (value.length === 0) continue
32
+ parts.push(
33
+ `${key}=[${value.slice(0, 3).join(",")}${value.length > 3 ? `...+${value.length - 3}` : ""}]`,
34
+ )
35
+ } else if (typeof value === "object") {
36
+ const str = JSON.stringify(value)
37
+ if (str.length < 50) {
38
+ parts.push(`${key}=${str}`)
39
+ }
40
+ } else {
41
+ parts.push(`${key}=${value}`)
42
+ }
43
+ }
44
+ return parts.join(" ")
45
+ }
46
+
47
+ private getCallerFile(skipFrames: number = 3): string {
48
+ const originalPrepareStackTrace = Error.prepareStackTrace
49
+ try {
50
+ const err = new Error()
51
+ Error.prepareStackTrace = (_, stack) => stack
52
+ const stack = err.stack as unknown as NodeJS.CallSite[]
53
+ Error.prepareStackTrace = originalPrepareStackTrace
54
+
55
+ // Skip specified number of frames to get to actual caller
56
+ for (let i = skipFrames; i < stack.length; i++) {
57
+ const filename = stack[i]?.getFileName()
58
+ if (filename && !filename.includes("/logger.")) {
59
+ // Extract just the filename without path and extension
60
+ const match = filename.match(/([^/\\]+)\.[tj]s$/)
61
+ return match ? match[1] : filename
62
+ }
63
+ }
64
+ return "unknown"
65
+ } catch {
66
+ return "unknown"
67
+ }
68
+ }
69
+
70
+ private async write(level: string, component: string, message: string, data?: any) {
71
+ if (!this.enabled) return
72
+
73
+ try {
74
+ await this.ensureLogDir()
75
+
76
+ const timestamp = new Date().toISOString()
77
+ const dataStr = this.formatData(data)
78
+
79
+ const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? " | " + dataStr : ""}\n`
80
+
81
+ const dailyLogDir = join(this.logDir, "daily")
82
+ if (!existsSync(dailyLogDir)) {
83
+ await mkdir(dailyLogDir, { recursive: true })
84
+ }
85
+
86
+ const logFile = join(dailyLogDir, `${new Date().toISOString().split("T")[0]}.log`)
87
+ await writeFile(logFile, logLine, { flag: "a" })
88
+ } catch (error) {}
89
+ }
90
+
91
+ info(message: string, data?: any) {
92
+ const component = this.getCallerFile(2)
93
+ return this.write("INFO", component, message, data)
94
+ }
95
+
96
+ debug(message: string, data?: any) {
97
+ const component = this.getCallerFile(2)
98
+ return this.write("DEBUG", component, message, data)
99
+ }
100
+
101
+ warn(message: string, data?: any) {
102
+ const component = this.getCallerFile(2)
103
+ return this.write("WARN", component, message, data)
104
+ }
105
+
106
+ error(message: string, data?: any) {
107
+ const component = this.getCallerFile(2)
108
+ return this.write("ERROR", component, message, data)
109
+ }
110
+
111
+ /**
112
+ * Strips unnecessary metadata from messages for cleaner debug logs.
113
+ *
114
+ * Removed:
115
+ * - All IDs (id, sessionID, messageID, parentID)
116
+ * - summary, path, cost, model, agent, mode, finish, providerID, modelID
117
+ * - step-start and step-finish parts entirely
118
+ * - snapshot fields
119
+ * - ignored text parts
120
+ *
121
+ * Kept:
122
+ * - role, time (created only), tokens (input, output, reasoning, cache)
123
+ * - text, reasoning, tool parts with content
124
+ * - tool calls with: tool, callID, input, output, metadata
125
+ */
126
+ private minimizeForDebug(messages: any[]): any[] {
127
+ return messages.map((msg) => {
128
+ const minimized: any = {
129
+ role: msg.info?.role,
130
+ }
131
+
132
+ if (msg.info?.time?.created) {
133
+ minimized.time = msg.info.time.created
134
+ }
135
+
136
+ if (msg.info?.tokens) {
137
+ minimized.tokens = {
138
+ input: msg.info.tokens.input,
139
+ output: msg.info.tokens.output,
140
+ reasoning: msg.info.tokens.reasoning,
141
+ cache: msg.info.tokens.cache,
142
+ }
143
+ }
144
+
145
+ if (msg.parts) {
146
+ minimized.parts = msg.parts
147
+ .map((part: any) => {
148
+ if (part.type === "step-start" || part.type === "step-finish") {
149
+ return null
150
+ }
151
+
152
+ if (part.type === "text") {
153
+ if (part.ignored) return null
154
+ const textPart: any = { type: "text", text: part.text }
155
+ if (part.metadata) textPart.metadata = part.metadata
156
+ return textPart
157
+ }
158
+
159
+ if (part.type === "reasoning") {
160
+ const reasoningPart: any = { type: "reasoning", text: part.text }
161
+ if (part.metadata) reasoningPart.metadata = part.metadata
162
+ return reasoningPart
163
+ }
164
+
165
+ if (part.type === "tool") {
166
+ const toolPart: any = {
167
+ type: "tool",
168
+ tool: part.tool,
169
+ callID: part.callID,
170
+ }
171
+
172
+ if (part.state?.status) {
173
+ toolPart.status = part.state.status
174
+ }
175
+ if (part.state?.input) {
176
+ toolPart.input = part.state.input
177
+ }
178
+ if (part.state?.output) {
179
+ toolPart.output = part.state.output
180
+ }
181
+ if (part.state?.error) {
182
+ toolPart.error = part.state.error
183
+ }
184
+ if (part.metadata) {
185
+ toolPart.metadata = part.metadata
186
+ }
187
+ if (part.state?.metadata) {
188
+ toolPart.metadata = {
189
+ ...(toolPart.metadata || {}),
190
+ ...part.state.metadata,
191
+ }
192
+ }
193
+ if (part.state?.title) {
194
+ toolPart.title = part.state.title
195
+ }
196
+
197
+ return toolPart
198
+ }
199
+
200
+ return null
201
+ })
202
+ .filter(Boolean)
203
+ }
204
+
205
+ return minimized
206
+ })
207
+ }
208
+
209
+ async saveContext(sessionId: string, messages: any[]) {
210
+ if (!this.enabled) return
211
+
212
+ try {
213
+ const contextDir = join(this.logDir, "context", sessionId)
214
+ if (!existsSync(contextDir)) {
215
+ await mkdir(contextDir, { recursive: true })
216
+ }
217
+
218
+ const minimized = this.minimizeForDebug(messages).filter(
219
+ (msg) => msg.parts && msg.parts.length > 0,
220
+ )
221
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
222
+ const contextFile = join(contextDir, `${timestamp}.json`)
223
+ await writeFile(contextFile, JSON.stringify(minimized, null, 2))
224
+ } catch (error) {}
225
+ }
226
+ }
@@ -0,0 +1,181 @@
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 COMPACT_MESSAGE_REGEX = /^@([1-9]\d*)@$/
7
+ const COMPACT_BLOCK_REGEX = /^@b([1-9]\d*)@$/
8
+ const MESSAGE_ID_TAG_NAME = "dcp-message-id"
9
+
10
+ export type IdFormat = "xml" | "compact"
11
+
12
+ const MESSAGE_REF_WIDTH = 4
13
+ const MESSAGE_REF_MIN_INDEX = 1
14
+ export const MESSAGE_REF_MAX_INDEX = 9999
15
+
16
+ export type ParsedBoundaryId =
17
+ | {
18
+ kind: "message"
19
+ ref: string
20
+ index: number
21
+ }
22
+ | {
23
+ kind: "compressed-block"
24
+ ref: string
25
+ blockId: number
26
+ }
27
+
28
+ export function formatMessageRef(index: number, format: IdFormat = "xml"): string {
29
+ const max = format === "compact" ? Number.MAX_SAFE_INTEGER : MESSAGE_REF_MAX_INDEX
30
+ if (!Number.isSafeInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > max) {
31
+ throw new Error(`Message ID index out of bounds: ${index}. Supported range is 1-${max}.`)
32
+ }
33
+ return format === "compact"
34
+ ? `@${index}@`
35
+ : `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`
36
+ }
37
+
38
+ export function formatBlockRef(blockId: number, format: IdFormat = "xml"): string {
39
+ if (!Number.isInteger(blockId) || blockId < 1) {
40
+ throw new Error(`Invalid block ID: ${blockId}`)
41
+ }
42
+ return format === "compact" ? `@b${blockId}@` : `b${blockId}`
43
+ }
44
+
45
+ export function parseMessageRef(ref: string, format: IdFormat = "xml"): number | null {
46
+ const normalized = ref.trim().toLowerCase()
47
+ const match = normalized.match(format === "compact" ? COMPACT_MESSAGE_REGEX : MESSAGE_REF_REGEX)
48
+ if (!match) {
49
+ return null
50
+ }
51
+ const index = Number.parseInt(match[1], 10)
52
+ if (!Number.isSafeInteger(index)) {
53
+ return null
54
+ }
55
+ const max = format === "compact" ? Number.MAX_SAFE_INTEGER : MESSAGE_REF_MAX_INDEX
56
+ if (index < MESSAGE_REF_MIN_INDEX || index > max) {
57
+ return null
58
+ }
59
+ return index
60
+ }
61
+
62
+ export function parseBlockRef(ref: string, format: IdFormat = "xml"): number | null {
63
+ const normalized = ref.trim().toLowerCase()
64
+ const match = normalized.match(format === "compact" ? COMPACT_BLOCK_REGEX : BLOCK_REF_REGEX)
65
+ if (!match) {
66
+ return null
67
+ }
68
+ const id = Number.parseInt(match[1], 10)
69
+ return Number.isSafeInteger(id) ? id : null
70
+ }
71
+
72
+ export function parseBoundaryId(id: string, format: IdFormat = "xml"): ParsedBoundaryId | null {
73
+ const normalized = id.trim().toLowerCase()
74
+ const messageIndex = parseMessageRef(normalized, format)
75
+ if (messageIndex !== null) {
76
+ return {
77
+ kind: "message",
78
+ ref: formatMessageRef(messageIndex, format),
79
+ index: messageIndex,
80
+ }
81
+ }
82
+
83
+ const blockId = parseBlockRef(normalized, format)
84
+ if (blockId !== null) {
85
+ return {
86
+ kind: "compressed-block",
87
+ ref: formatBlockRef(blockId, format),
88
+ blockId,
89
+ }
90
+ }
91
+
92
+ return null
93
+ }
94
+
95
+ function escapeXmlAttribute(value: string): string {
96
+ return value
97
+ .replace(/&/g, "&amp;")
98
+ .replace(/"/g, "&quot;")
99
+ .replace(/</g, "&lt;")
100
+ .replace(/>/g, "&gt;")
101
+ }
102
+
103
+ export function formatMessageIdTag(
104
+ ref: string,
105
+ attributes?: Record<string, string | undefined>,
106
+ format: IdFormat = "xml",
107
+ ): string {
108
+ if (format === "compact") {
109
+ if (ref === "BLOCKED") return "\n@blocked@"
110
+ const priority = attributes?.priority
111
+ return `\n${ref}${priority ? ` [${priority}]` : ""}`
112
+ }
113
+ const serializedAttributes = Object.entries(attributes || {})
114
+ .sort(([left], [right]) => left.localeCompare(right))
115
+ .map(([name, value]) => {
116
+ if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
117
+ return ""
118
+ }
119
+
120
+ return ` ${name}="${escapeXmlAttribute(value)}"`
121
+ })
122
+ .join("")
123
+
124
+ return `\n<${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`
125
+ }
126
+
127
+ export function assignMessageRefs(state: SessionState, messages: WithParts[]): number {
128
+ let assigned = 0
129
+ let skippedSubAgentPrompt = false
130
+
131
+ for (const message of messages) {
132
+ if (isIgnoredUserMessage(message)) {
133
+ continue
134
+ }
135
+
136
+ if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
137
+ skippedSubAgentPrompt = true
138
+ continue
139
+ }
140
+
141
+ const rawMessageId = message.info.id
142
+ if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
143
+ continue
144
+ }
145
+
146
+ const existingRef = state.messageIds.byRawId.get(rawMessageId)
147
+ if (existingRef) {
148
+ if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
149
+ state.messageIds.byRef.set(existingRef, rawMessageId)
150
+ }
151
+ continue
152
+ }
153
+
154
+ const ref = allocateNextMessageRef(state)
155
+ state.messageIds.byRawId.set(rawMessageId, ref)
156
+ state.messageIds.byRef.set(ref, rawMessageId)
157
+ assigned++
158
+ }
159
+
160
+ return assigned
161
+ }
162
+
163
+ function allocateNextMessageRef(state: SessionState): string {
164
+ let candidate = Number.isInteger(state.messageIds.nextRef)
165
+ ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef)
166
+ : MESSAGE_REF_MIN_INDEX
167
+
168
+ const max = state.idFormat === "compact" ? Number.MAX_SAFE_INTEGER : MESSAGE_REF_MAX_INDEX
169
+ while (candidate <= max) {
170
+ const ref = formatMessageRef(candidate, state.idFormat)
171
+ if (!state.messageIds.byRef.has(ref)) {
172
+ state.messageIds.nextRef = candidate + 1
173
+ return ref
174
+ }
175
+ candidate++
176
+ }
177
+
178
+ throw new Error(
179
+ `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(max, state.idFormat)} aliases in this session.`,
180
+ )
181
+ }
@@ -0,0 +1,7 @@
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 { buildToolIdList, stripHallucinations, stripHallucinationsFromString } from "./utils"