@tarquinen/opencode-dcp 3.2.5-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.
- package/index.ts +141 -0
- package/lib/analysis/tokens.ts +225 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +132 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +125 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +148 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress/index.ts +3 -0
- package/lib/compress/message-utils.ts +250 -0
- package/lib/compress/message.ts +137 -0
- package/lib/compress/pipeline.ts +106 -0
- package/lib/compress/protected-content.ts +154 -0
- package/lib/compress/range-utils.ts +308 -0
- package/lib/compress/range.ts +180 -0
- package/lib/compress/search.ts +267 -0
- package/lib/compress/state.ts +268 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +108 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +1071 -0
- package/lib/hooks.ts +378 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/logger.ts +235 -0
- package/lib/message-ids.ts +172 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +56 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +187 -0
- package/lib/prompts/compress-message.ts +42 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +256 -0
- package/lib/state/state.ts +190 -0
- package/lib/state/tool-cache.ts +98 -0
- package/lib/state/types.ts +112 -0
- package/lib/state/utils.ts +334 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/token-utils.ts +162 -0
- package/lib/ui/notification.ts +346 -0
- package/lib/ui/utils.ts +287 -0
- package/package.json +9 -2
- package/tui/data/context.ts +177 -0
- package/tui/index.tsx +34 -0
- package/tui/routes/summary.tsx +175 -0
- package/tui/shared/names.ts +9 -0
- package/tui/shared/theme.ts +58 -0
- package/tui/shared/types.ts +38 -0
- package/tui/slots/sidebar-content.tsx +502 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { SessionState, WithParts } from "./state"
|
|
2
|
+
import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
|
|
3
|
+
import { Logger } from "./logger"
|
|
4
|
+
import { countTokens as anthropicCountTokens } from "@anthropic-ai/tokenizer"
|
|
5
|
+
import { getLastUserMessage } from "./messages/query"
|
|
6
|
+
|
|
7
|
+
export function getCurrentTokenUsage(state: SessionState, messages: WithParts[]): number {
|
|
8
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
9
|
+
const msg = messages[i]
|
|
10
|
+
if (msg.info.role !== "assistant") {
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const assistantInfo = msg.info as AssistantMessage
|
|
15
|
+
if ((assistantInfo.tokens?.output || 0) <= 0) {
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (
|
|
20
|
+
state.lastCompaction > 0 &&
|
|
21
|
+
(msg.info.time.created < state.lastCompaction ||
|
|
22
|
+
(msg.info.summary === true && msg.info.time.created === state.lastCompaction))
|
|
23
|
+
) {
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const input = assistantInfo.tokens?.input || 0
|
|
28
|
+
const output = assistantInfo.tokens?.output || 0
|
|
29
|
+
const reasoning = assistantInfo.tokens?.reasoning || 0
|
|
30
|
+
const cacheRead = assistantInfo.tokens?.cache?.read || 0
|
|
31
|
+
const cacheWrite = assistantInfo.tokens?.cache?.write || 0
|
|
32
|
+
return input + output + reasoning + cacheRead + cacheWrite
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getCurrentParams(
|
|
39
|
+
state: SessionState,
|
|
40
|
+
messages: WithParts[],
|
|
41
|
+
logger: Logger,
|
|
42
|
+
): {
|
|
43
|
+
providerId: string | undefined
|
|
44
|
+
modelId: string | undefined
|
|
45
|
+
agent: string | undefined
|
|
46
|
+
variant: string | undefined
|
|
47
|
+
} {
|
|
48
|
+
const userMsg = getLastUserMessage(messages)
|
|
49
|
+
if (!userMsg) {
|
|
50
|
+
logger.debug("No user message found when determining current params")
|
|
51
|
+
return {
|
|
52
|
+
providerId: undefined,
|
|
53
|
+
modelId: undefined,
|
|
54
|
+
agent: undefined,
|
|
55
|
+
variant: state.variant,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const userInfo = userMsg.info as UserMessage
|
|
59
|
+
const agent: string = userInfo.agent
|
|
60
|
+
const providerId: string | undefined = userInfo.model.providerID
|
|
61
|
+
const modelId: string | undefined = userInfo.model.modelID
|
|
62
|
+
const variant: string | undefined = state.variant ?? userInfo.variant
|
|
63
|
+
|
|
64
|
+
return { providerId, modelId, agent, variant }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function countTokens(text: string): number {
|
|
68
|
+
if (!text) return 0
|
|
69
|
+
try {
|
|
70
|
+
return anthropicCountTokens(text)
|
|
71
|
+
} catch {
|
|
72
|
+
return Math.round(text.length / 4)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function estimateTokensBatch(texts: string[]): number {
|
|
77
|
+
if (texts.length === 0) return 0
|
|
78
|
+
return countTokens(texts.join(" "))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const COMPACTED_TOOL_OUTPUT_PLACEHOLDER = "[Old tool result content cleared]"
|
|
82
|
+
|
|
83
|
+
function stringifyToolContent(value: unknown): string {
|
|
84
|
+
return typeof value === "string" ? value : JSON.stringify(value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function extractCompletedToolOutput(part: any): string | undefined {
|
|
88
|
+
if (
|
|
89
|
+
part?.type !== "tool" ||
|
|
90
|
+
part.state?.status !== "completed" ||
|
|
91
|
+
part.state?.output === undefined
|
|
92
|
+
) {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (part.state?.time?.compacted) {
|
|
97
|
+
return COMPACTED_TOOL_OUTPUT_PLACEHOLDER
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return stringifyToolContent(part.state.output)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function extractToolContent(part: any): string[] {
|
|
104
|
+
const contents: string[] = []
|
|
105
|
+
|
|
106
|
+
if (part?.type !== "tool") {
|
|
107
|
+
return contents
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (part.state?.input !== undefined) {
|
|
111
|
+
contents.push(stringifyToolContent(part.state.input))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const completedOutput = extractCompletedToolOutput(part)
|
|
115
|
+
if (completedOutput !== undefined) {
|
|
116
|
+
contents.push(completedOutput)
|
|
117
|
+
} else if (part.state?.status === "error" && part.state?.error) {
|
|
118
|
+
contents.push(stringifyToolContent(part.state.error))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return contents
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function countToolTokens(part: any): number {
|
|
125
|
+
const contents = extractToolContent(part)
|
|
126
|
+
return estimateTokensBatch(contents)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function getTotalToolTokens(state: SessionState, toolIds: string[]): number {
|
|
130
|
+
let total = 0
|
|
131
|
+
for (const id of toolIds) {
|
|
132
|
+
const entry = state.toolParameters.get(id)
|
|
133
|
+
total += entry?.tokenCount ?? 0
|
|
134
|
+
}
|
|
135
|
+
return total
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function countMessageTextTokens(msg: WithParts): number {
|
|
139
|
+
const texts: string[] = []
|
|
140
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
141
|
+
for (const part of parts) {
|
|
142
|
+
if (part.type === "text") {
|
|
143
|
+
texts.push(part.text)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (texts.length === 0) return 0
|
|
147
|
+
return estimateTokensBatch(texts)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function countAllMessageTokens(msg: WithParts): number {
|
|
151
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
152
|
+
const texts: string[] = []
|
|
153
|
+
for (const part of parts) {
|
|
154
|
+
if (part.type === "text") {
|
|
155
|
+
texts.push(part.text)
|
|
156
|
+
} else {
|
|
157
|
+
texts.push(...extractToolContent(part))
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (texts.length === 0) return 0
|
|
161
|
+
return estimateTokensBatch(texts)
|
|
162
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { Logger } from "../logger"
|
|
2
|
+
import type { SessionState } from "../state"
|
|
3
|
+
import {
|
|
4
|
+
formatPrunedItemsList,
|
|
5
|
+
formatProgressBar,
|
|
6
|
+
formatStatsHeader,
|
|
7
|
+
formatTokenCount,
|
|
8
|
+
} from "./utils"
|
|
9
|
+
import { ToolParameterEntry } from "../state"
|
|
10
|
+
import { PluginConfig } from "../config"
|
|
11
|
+
import { getActiveSummaryTokenUsage } from "../state/utils"
|
|
12
|
+
|
|
13
|
+
export type PruneReason = "completion" | "noise" | "extraction"
|
|
14
|
+
export const PRUNE_REASON_LABELS: Record<PruneReason, string> = {
|
|
15
|
+
completion: "Task Complete",
|
|
16
|
+
noise: "Noise Removal",
|
|
17
|
+
extraction: "Extraction",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CompressionNotificationEntry {
|
|
21
|
+
blockId: number
|
|
22
|
+
runId: number
|
|
23
|
+
summary: string
|
|
24
|
+
summaryTokens: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildMinimalMessage(state: SessionState, reason: PruneReason | undefined): string {
|
|
28
|
+
const reasonSuffix = reason ? ` — ${PRUNE_REASON_LABELS[reason]}` : ""
|
|
29
|
+
return (
|
|
30
|
+
formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter) +
|
|
31
|
+
reasonSuffix
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildDetailedMessage(
|
|
36
|
+
state: SessionState,
|
|
37
|
+
reason: PruneReason | undefined,
|
|
38
|
+
pruneToolIds: string[],
|
|
39
|
+
toolMetadata: Map<string, ToolParameterEntry>,
|
|
40
|
+
workingDirectory: string,
|
|
41
|
+
): string {
|
|
42
|
+
let message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter)
|
|
43
|
+
|
|
44
|
+
if (pruneToolIds.length > 0) {
|
|
45
|
+
const pruneTokenCounterStr = `~${formatTokenCount(state.stats.pruneTokenCounter)}`
|
|
46
|
+
const reasonLabel = reason ? ` — ${PRUNE_REASON_LABELS[reason]}` : ""
|
|
47
|
+
message += `\n\n▣ Pruning (${pruneTokenCounterStr})${reasonLabel}`
|
|
48
|
+
|
|
49
|
+
const itemLines = formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory)
|
|
50
|
+
message += "\n" + itemLines.join("\n")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return message.trim()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TOAST_BODY_MAX_LINES = 12
|
|
57
|
+
const TOAST_SUMMARY_MAX_CHARS = 600
|
|
58
|
+
|
|
59
|
+
function truncateToastBody(body: string, maxLines: number = TOAST_BODY_MAX_LINES): string {
|
|
60
|
+
const lines = body.split("\n")
|
|
61
|
+
if (lines.length <= maxLines) {
|
|
62
|
+
return body
|
|
63
|
+
}
|
|
64
|
+
const kept = lines.slice(0, maxLines - 1)
|
|
65
|
+
const remaining = lines.length - maxLines + 1
|
|
66
|
+
return kept.join("\n") + `\n... and ${remaining} more`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function truncateToastSummary(summary: string, maxChars: number = TOAST_SUMMARY_MAX_CHARS): string {
|
|
70
|
+
if (summary.length <= maxChars) {
|
|
71
|
+
return summary
|
|
72
|
+
}
|
|
73
|
+
return summary.slice(0, maxChars - 3) + "..."
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function truncateExtractedSection(
|
|
77
|
+
message: string,
|
|
78
|
+
maxChars: number = TOAST_SUMMARY_MAX_CHARS,
|
|
79
|
+
): string {
|
|
80
|
+
const marker = "\n\n▣ Extracted"
|
|
81
|
+
const index = message.indexOf(marker)
|
|
82
|
+
if (index === -1) {
|
|
83
|
+
return message
|
|
84
|
+
}
|
|
85
|
+
const extracted = message.slice(index)
|
|
86
|
+
if (extracted.length <= maxChars) {
|
|
87
|
+
return message
|
|
88
|
+
}
|
|
89
|
+
return message.slice(0, index) + truncateToastSummary(extracted, maxChars)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function sendUnifiedNotification(
|
|
93
|
+
client: any,
|
|
94
|
+
logger: Logger,
|
|
95
|
+
config: PluginConfig,
|
|
96
|
+
state: SessionState,
|
|
97
|
+
sessionId: string,
|
|
98
|
+
pruneToolIds: string[],
|
|
99
|
+
toolMetadata: Map<string, ToolParameterEntry>,
|
|
100
|
+
reason: PruneReason | undefined,
|
|
101
|
+
params: any,
|
|
102
|
+
workingDirectory: string,
|
|
103
|
+
): Promise<boolean> {
|
|
104
|
+
const hasPruned = pruneToolIds.length > 0
|
|
105
|
+
if (!hasPruned) {
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config.pruneNotification === "off") {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const message =
|
|
114
|
+
config.pruneNotification === "minimal"
|
|
115
|
+
? buildMinimalMessage(state, reason)
|
|
116
|
+
: buildDetailedMessage(state, reason, pruneToolIds, toolMetadata, workingDirectory)
|
|
117
|
+
|
|
118
|
+
if (config.pruneNotificationType === "toast") {
|
|
119
|
+
let toastMessage = truncateExtractedSection(message)
|
|
120
|
+
toastMessage =
|
|
121
|
+
config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage)
|
|
122
|
+
|
|
123
|
+
await client.tui.showToast({
|
|
124
|
+
body: {
|
|
125
|
+
title: "DCP: Compress Notification",
|
|
126
|
+
message: toastMessage,
|
|
127
|
+
variant: "info",
|
|
128
|
+
duration: 5000,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await sendIgnoredMessage(client, sessionId, message, params, logger)
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildCompressionSummary(
|
|
139
|
+
entries: CompressionNotificationEntry[],
|
|
140
|
+
state: SessionState,
|
|
141
|
+
): string {
|
|
142
|
+
if (entries.length === 1) {
|
|
143
|
+
return entries[0]?.summary ?? ""
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return entries
|
|
147
|
+
.map((entry) => {
|
|
148
|
+
const topic =
|
|
149
|
+
state.prune.messages.blocksById.get(entry.blockId)?.topic ?? "(unknown topic)"
|
|
150
|
+
return `### ${topic}\n${entry.summary}`
|
|
151
|
+
})
|
|
152
|
+
.join("\n\n")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getCompressionLabel(entries: CompressionNotificationEntry[]): string {
|
|
156
|
+
const runId = entries[0]?.runId
|
|
157
|
+
if (runId === undefined) {
|
|
158
|
+
return "Compression"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return `Compression #${runId}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatCompressionMetrics(removedTokens: number, summaryTokens: number): string {
|
|
165
|
+
const metrics = [`-${formatTokenCount(removedTokens, true)} removed`]
|
|
166
|
+
if (summaryTokens > 0) {
|
|
167
|
+
metrics.push(`+${formatTokenCount(summaryTokens, true)} summary`)
|
|
168
|
+
}
|
|
169
|
+
return metrics.join(", ")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function sendCompressNotification(
|
|
173
|
+
client: any,
|
|
174
|
+
logger: Logger,
|
|
175
|
+
config: PluginConfig,
|
|
176
|
+
state: SessionState,
|
|
177
|
+
sessionId: string,
|
|
178
|
+
entries: CompressionNotificationEntry[],
|
|
179
|
+
batchTopic: string | undefined,
|
|
180
|
+
sessionMessageIds: string[],
|
|
181
|
+
params: any,
|
|
182
|
+
): Promise<boolean> {
|
|
183
|
+
if (config.pruneNotification === "off") {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (entries.length === 0) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let message: string
|
|
192
|
+
const compressionLabel = getCompressionLabel(entries)
|
|
193
|
+
const summary = buildCompressionSummary(entries, state)
|
|
194
|
+
const summaryTokens = entries.reduce((total, entry) => total + entry.summaryTokens, 0)
|
|
195
|
+
const summaryTokensStr = formatTokenCount(summaryTokens)
|
|
196
|
+
const compressedTokens = entries.reduce((total, entry) => {
|
|
197
|
+
const compressionBlock = state.prune.messages.blocksById.get(entry.blockId)
|
|
198
|
+
if (!compressionBlock) {
|
|
199
|
+
logger.error("Compression block missing for notification", {
|
|
200
|
+
compressionId: entry.blockId,
|
|
201
|
+
sessionId,
|
|
202
|
+
})
|
|
203
|
+
return total
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return total + compressionBlock.compressedTokens
|
|
207
|
+
}, 0)
|
|
208
|
+
|
|
209
|
+
const newlyCompressedMessageIds: string[] = []
|
|
210
|
+
const newlyCompressedToolIds: string[] = []
|
|
211
|
+
const seenMessageIds = new Set<string>()
|
|
212
|
+
const seenToolIds = new Set<string>()
|
|
213
|
+
|
|
214
|
+
for (const entry of entries) {
|
|
215
|
+
const compressionBlock = state.prune.messages.blocksById.get(entry.blockId)
|
|
216
|
+
if (!compressionBlock) {
|
|
217
|
+
continue
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const messageId of compressionBlock.directMessageIds) {
|
|
221
|
+
if (seenMessageIds.has(messageId)) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
seenMessageIds.add(messageId)
|
|
225
|
+
newlyCompressedMessageIds.push(messageId)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const toolId of compressionBlock.directToolIds) {
|
|
229
|
+
if (seenToolIds.has(toolId)) {
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
seenToolIds.add(toolId)
|
|
233
|
+
newlyCompressedToolIds.push(toolId)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const topic =
|
|
238
|
+
batchTopic ??
|
|
239
|
+
(entries.length === 1
|
|
240
|
+
? (state.prune.messages.blocksById.get(entries[0]?.blockId ?? -1)?.topic ??
|
|
241
|
+
"(unknown topic)")
|
|
242
|
+
: "(unknown topic)")
|
|
243
|
+
|
|
244
|
+
const totalActiveSummaryTkns = getActiveSummaryTokenUsage(state)
|
|
245
|
+
const totalGross = state.stats.totalPruneTokens + state.stats.pruneTokenCounter
|
|
246
|
+
const notificationHeader = `▣ DCP | ${formatCompressionMetrics(totalGross, totalActiveSummaryTkns)}`
|
|
247
|
+
|
|
248
|
+
if (config.pruneNotification === "minimal") {
|
|
249
|
+
message = `${notificationHeader} — ${compressionLabel}`
|
|
250
|
+
} else {
|
|
251
|
+
message = notificationHeader
|
|
252
|
+
const activePrunedMessages = new Map<string, number>()
|
|
253
|
+
for (const [messageId, entry] of state.prune.messages.byMessageId) {
|
|
254
|
+
if (entry.activeBlockIds.length > 0) {
|
|
255
|
+
activePrunedMessages.set(messageId, entry.tokenCount)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const progressBar = formatProgressBar(
|
|
259
|
+
sessionMessageIds,
|
|
260
|
+
activePrunedMessages,
|
|
261
|
+
newlyCompressedMessageIds,
|
|
262
|
+
50,
|
|
263
|
+
)
|
|
264
|
+
message += `\n\n${progressBar}`
|
|
265
|
+
message += `\n▣ ${compressionLabel} ${formatCompressionMetrics(compressedTokens, summaryTokens)}`
|
|
266
|
+
message += `\n→ Topic: ${topic}`
|
|
267
|
+
message += `\n→ Items: ${newlyCompressedMessageIds.length} messages`
|
|
268
|
+
if (newlyCompressedToolIds.length > 0) {
|
|
269
|
+
message += ` and ${newlyCompressedToolIds.length} tools compressed`
|
|
270
|
+
} else {
|
|
271
|
+
message += ` compressed`
|
|
272
|
+
}
|
|
273
|
+
if (config.compress.showCompression) {
|
|
274
|
+
message += `\n→ Compression (~${summaryTokensStr}): ${summary}`
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (config.pruneNotificationType === "toast") {
|
|
279
|
+
let toastMessage = message
|
|
280
|
+
if (config.compress.showCompression) {
|
|
281
|
+
const truncatedSummary = truncateToastSummary(summary)
|
|
282
|
+
if (truncatedSummary !== summary) {
|
|
283
|
+
toastMessage = toastMessage.replace(
|
|
284
|
+
`\n→ Compression (~${summaryTokensStr}): ${summary}`,
|
|
285
|
+
`\n→ Compression (~${summaryTokensStr}): ${truncatedSummary}`,
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
toastMessage =
|
|
290
|
+
config.pruneNotification === "minimal" ? toastMessage : truncateToastBody(toastMessage)
|
|
291
|
+
|
|
292
|
+
await client.tui.showToast({
|
|
293
|
+
body: {
|
|
294
|
+
title: "DCP: Compress Notification",
|
|
295
|
+
message: toastMessage,
|
|
296
|
+
variant: "info",
|
|
297
|
+
duration: 5000,
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
return true
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await sendIgnoredMessage(client, sessionId, message, params, logger)
|
|
304
|
+
return true
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function sendIgnoredMessage(
|
|
308
|
+
client: any,
|
|
309
|
+
sessionID: string,
|
|
310
|
+
text: string,
|
|
311
|
+
params: any,
|
|
312
|
+
logger: Logger,
|
|
313
|
+
): Promise<void> {
|
|
314
|
+
const agent = params.agent || undefined
|
|
315
|
+
const variant = params.variant || undefined
|
|
316
|
+
const model =
|
|
317
|
+
params.providerId && params.modelId
|
|
318
|
+
? {
|
|
319
|
+
providerID: params.providerId,
|
|
320
|
+
modelID: params.modelId,
|
|
321
|
+
}
|
|
322
|
+
: undefined
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
await client.session.prompt({
|
|
326
|
+
path: {
|
|
327
|
+
id: sessionID,
|
|
328
|
+
},
|
|
329
|
+
body: {
|
|
330
|
+
noReply: true,
|
|
331
|
+
agent: agent,
|
|
332
|
+
model: model,
|
|
333
|
+
variant: variant,
|
|
334
|
+
parts: [
|
|
335
|
+
{
|
|
336
|
+
type: "text",
|
|
337
|
+
text: text,
|
|
338
|
+
ignored: true,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
} catch (error: any) {
|
|
344
|
+
logger.error("Failed to send notification", { error: error.message })
|
|
345
|
+
}
|
|
346
|
+
}
|