@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.1
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.
- package/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8129 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +27 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +68 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +64 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +47 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +105 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +75 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/lib/v2/usage.d.ts +44 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +180 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +494 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +320 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +70 -0
- package/lib/compress/usage.ts +168 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +321 -0
- package/lib/state/state.ts +214 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +117 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +261 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +448 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +60 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/lib/v2/usage.ts +423 -0
- package/package.json +91 -5
- package/server.js +1 -0
- 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, "&")
|
|
98
|
+
.replace(/"/g, """)
|
|
99
|
+
.replace(/</g, "<")
|
|
100
|
+
.replace(/>/g, ">")
|
|
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"
|