Skip to content

Commit f24689e

Browse files
authored
Merge pull request #477 from Opencode-DCP/dev
2 parents 476d670 + 499a59a commit f24689e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+1814
-472
lines changed

README.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,13 @@ Automatically reduces token usage in OpenCode by managing conversation context.
99

1010
## Installation
1111

12-
Add to your OpenCode config:
12+
Install from the CLI:
1313

14-
```jsonc
15-
// opencode.jsonc
16-
{
17-
"plugin": ["@tarquinen/opencode-dcp@latest"],
18-
}
14+
```bash
15+
opencode plugin @tarquinen/opencode-dcp@latest --global
1916
```
2017

21-
Using `@latest` ensures you always get the newest version automatically when OpenCode starts.
22-
23-
Restart OpenCode. The plugin will automatically start optimizing your sessions.
18+
This installs the package and adds it to your global OpenCode config.
2419

2520
## How It Works
2621

index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createChatMessageHandler,
1414
createChatMessageTransformHandler,
1515
createCommandExecuteHandler,
16+
createEventHandler,
1617
createSystemPromptHandler,
1718
createTextCompleteHandler,
1819
} from "./lib/hooks"
@@ -57,7 +58,6 @@ const plugin: Plugin = (async (ctx) => {
5758
config,
5859
prompts,
5960
),
60-
6161
"experimental.chat.messages.transform": createChatMessageTransformHandler(
6262
ctx.client,
6363
state,
@@ -76,6 +76,7 @@ const plugin: Plugin = (async (ctx) => {
7676
ctx.directory,
7777
hostPermissions,
7878
),
79+
event: createEventHandler(state, logger),
7980
tool: {
8081
...(config.compress.permission !== "deny" && {
8182
compress:

lib/commands/compression-targets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface CompressionTarget {
55
runId: number
66
topic: string
77
compressedTokens: number
8+
durationMs: number
89
grouped: boolean
910
blocks: CompressionBlock[]
1011
}
@@ -26,6 +27,7 @@ function buildTarget(blocks: CompressionBlock[]): CompressionTarget {
2627
runId: first.runId,
2728
topic: grouped ? first.batchTopic || first.topic : first.topic,
2829
compressedTokens: ordered.reduce((total, block) => total + block.compressedTokens, 0),
30+
durationMs: ordered.reduce((total, block) => Math.max(total, block.durationMs), 0),
2931
grouped,
3032
blocks: ordered,
3133
}

lib/commands/context.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ import type { Logger } from "../logger"
4444
import type { SessionState, WithParts } from "../state"
4545
import { sendIgnoredMessage } from "../ui/notification"
4646
import { formatTokenCount } from "../ui/utils"
47-
import { isMessageCompacted } from "../shared-utils"
48-
import { isIgnoredUserMessage } from "../messages/utils"
49-
import { countTokens, getCurrentParams } from "../strategies/utils"
47+
import { isIgnoredUserMessage } from "../messages/query"
48+
import { isMessageCompacted } from "../state/utils"
49+
import { countTokens, extractCompletedToolOutput, getCurrentParams } from "../token-utils"
5050
import type { AssistantMessage, TextPart, ToolPart } from "@opencode-ai/sdk/v2"
5151

5252
export interface ContextCommandContext {
@@ -159,11 +159,8 @@ function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdo
159159
toolInputParts.push(inputStr)
160160
}
161161

162-
if (toolPart.state?.status === "completed" && toolPart.state?.output) {
163-
const outputStr =
164-
typeof toolPart.state.output === "string"
165-
? toolPart.state.output
166-
: JSON.stringify(toolPart.state.output)
162+
const outputStr = extractCompletedToolOutput(toolPart)
163+
if (outputStr !== undefined) {
167164
toolOutputParts.push(outputStr)
168165
}
169166
}

lib/commands/decompress.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Logger } from "../logger"
22
import type { CompressionBlock, PruneMessagesState, SessionState, WithParts } from "../state"
33
import { syncCompressionBlocks } from "../messages"
44
import { parseBlockRef } from "../message-ids"
5-
import { getCurrentParams } from "../strategies/utils"
5+
import { getCurrentParams } from "../token-utils"
66
import { saveSessionState } from "../state/persistence"
77
import { sendIgnoredMessage } from "../ui/notification"
88
import { formatTokenCount } from "../ui/utils"

lib/commands/help.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
import type { Logger } from "../logger"
77
import type { PluginConfig } from "../config"
88
import type { SessionState, WithParts } from "../state"
9-
import { compressPermission } from "../shared-utils"
9+
import { compressPermission } from "../compress-permission"
1010
import { sendIgnoredMessage } from "../ui/notification"
11-
import { getCurrentParams } from "../strategies/utils"
11+
import { getCurrentParams } from "../token-utils"
1212

1313
export interface HelpCommandContext {
1414
client: any

lib/commands/manual.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import type { Logger } from "../logger"
1111
import type { SessionState, WithParts } from "../state"
1212
import type { PluginConfig } from "../config"
1313
import { sendIgnoredMessage } from "../ui/notification"
14-
import { getCurrentParams } from "../strategies/utils"
15-
import { buildCompressedBlockGuidance } from "../messages/inject/utils"
16-
import { isIgnoredUserMessage } from "../messages/utils"
14+
import { getCurrentParams } from "../token-utils"
15+
import { buildCompressedBlockGuidance } from "../prompts/extensions/nudge"
16+
import { isIgnoredUserMessage } from "../messages/query"
1717

1818
const MANUAL_MODE_ON = "Manual mode is now ON. Use /dcp compress to trigger context tools manually."
1919

lib/commands/recompress.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Logger } from "../logger"
22
import type { PruneMessagesState, SessionState, WithParts } from "../state"
33
import { syncCompressionBlocks } from "../messages"
44
import { parseBlockRef } from "../message-ids"
5-
import { getCurrentParams } from "../strategies/utils"
5+
import { getCurrentParams } from "../token-utils"
66
import { saveSessionState } from "../state/persistence"
77
import { sendIgnoredMessage } from "../ui/notification"
88
import { formatTokenCount } from "../ui/utils"

lib/commands/stats.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type { SessionState, WithParts } from "../state"
88
import { sendIgnoredMessage } from "../ui/notification"
99
import { formatTokenCount } from "../ui/utils"
1010
import { loadAllSessionStats, type AggregatedStats } from "../state/persistence"
11-
import { getCurrentParams } from "../strategies/utils"
11+
import { getCurrentParams } from "../token-utils"
12+
import { getActiveCompressionTargets } from "./compression-targets"
1213

1314
export interface StatsCommandContext {
1415
client: any
@@ -20,8 +21,10 @@ export interface StatsCommandContext {
2021

2122
function formatStatsMessage(
2223
sessionTokens: number,
24+
sessionSummaryTokens: number,
2325
sessionTools: number,
2426
sessionMessages: number,
27+
sessionDurationMs: number,
2528
allTime: AggregatedStats,
2629
): string {
2730
const lines: string[] = []
@@ -30,11 +33,15 @@ function formatStatsMessage(
3033
lines.push("│ DCP Statistics │")
3134
lines.push("╰───────────────────────────────────────────────────────────╯")
3235
lines.push("")
33-
lines.push("Session:")
36+
lines.push("Compression:")
3437
lines.push("─".repeat(60))
35-
lines.push(` Tokens pruned: ~${formatTokenCount(sessionTokens)}`)
36-
lines.push(` Tools pruned: ${sessionTools}`)
37-
lines.push(` Messages pruned: ${sessionMessages}`)
38+
lines.push(
39+
` Tokens in|out: ~${formatTokenCount(sessionTokens)} | ~${formatTokenCount(sessionSummaryTokens)}`,
40+
)
41+
lines.push(` Ratio: ${formatCompressionRatio(sessionTokens, sessionSummaryTokens)}`)
42+
lines.push(` Time: ${formatCompressionTime(sessionDurationMs)}`)
43+
lines.push(` Messages: ${sessionMessages}`)
44+
lines.push(` Tools: ${sessionTools}`)
3845
lines.push("")
3946
lines.push("All-time:")
4047
lines.push("─".repeat(60))
@@ -46,11 +53,55 @@ function formatStatsMessage(
4653
return lines.join("\n")
4754
}
4855

56+
function formatCompressionRatio(inputTokens: number, outputTokens: number): string {
57+
if (inputTokens <= 0) {
58+
return "0:1"
59+
}
60+
61+
if (outputTokens <= 0) {
62+
return "∞:1"
63+
}
64+
65+
const ratio = Math.max(1, Math.round(inputTokens / outputTokens))
66+
return `${ratio}:1`
67+
}
68+
69+
function formatCompressionTime(ms: number): string {
70+
const safeMs = Math.max(0, Math.round(ms))
71+
if (safeMs < 1000) {
72+
return `${safeMs} ms`
73+
}
74+
75+
const totalSeconds = safeMs / 1000
76+
if (totalSeconds < 60) {
77+
return `${totalSeconds.toFixed(1)} s`
78+
}
79+
80+
const wholeSeconds = Math.floor(totalSeconds)
81+
const hours = Math.floor(wholeSeconds / 3600)
82+
const minutes = Math.floor((wholeSeconds % 3600) / 60)
83+
const seconds = wholeSeconds % 60
84+
85+
if (hours > 0) {
86+
return `${hours}h ${minutes}m ${seconds}s`
87+
}
88+
89+
return `${minutes}m ${seconds}s`
90+
}
91+
4992
export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void> {
5093
const { client, state, logger, sessionId, messages } = ctx
5194

5295
// Session stats from in-memory state
5396
const sessionTokens = state.stats.totalPruneTokens
97+
const sessionSummaryTokens = Array.from(state.prune.messages.blocksById.values()).reduce(
98+
(total, block) => (block.active ? total + block.summaryTokens : total),
99+
0,
100+
)
101+
const sessionDurationMs = getActiveCompressionTargets(state.prune.messages).reduce(
102+
(total, target) => total + target.durationMs,
103+
0,
104+
)
54105

55106
const prunedToolIds = new Set<string>(state.prune.tools.keys())
56107
for (const block of state.prune.messages.blocksById.values()) {
@@ -72,15 +123,24 @@ export async function handleStatsCommand(ctx: StatsCommandContext): Promise<void
72123
// All-time stats from storage files
73124
const allTime = await loadAllSessionStats(logger)
74125

75-
const message = formatStatsMessage(sessionTokens, sessionTools, sessionMessages, allTime)
126+
const message = formatStatsMessage(
127+
sessionTokens,
128+
sessionSummaryTokens,
129+
sessionTools,
130+
sessionMessages,
131+
sessionDurationMs,
132+
allTime,
133+
)
76134

77135
const params = getCurrentParams(state, messages, logger)
78136
await sendIgnoredMessage(client, sessionId, message, params, logger)
79137

80138
logger.info("Stats command executed", {
81139
sessionTokens,
140+
sessionSummaryTokens,
82141
sessionTools,
83142
sessionMessages,
143+
sessionDurationMs,
84144
allTimeTokens: allTime.totalTokens,
85145
allTimeTools: allTime.totalTools,
86146
allTimeMessages: allTime.totalMessages,

lib/commands/sweep.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import type { SessionState, WithParts, ToolParameterEntry } from "../state"
1212
import type { PluginConfig } from "../config"
1313
import { sendIgnoredMessage } from "../ui/notification"
1414
import { formatPrunedItemsList } from "../ui/utils"
15-
import { getCurrentParams, getTotalToolTokens } from "../strategies/utils"
16-
import { buildToolIdList, isIgnoredUserMessage } from "../messages/utils"
15+
import { getCurrentParams, getTotalToolTokens } from "../token-utils"
16+
import { isIgnoredUserMessage } from "../messages/query"
17+
import { buildToolIdList } from "../messages/utils"
1718
import { saveSessionState } from "../state/persistence"
18-
import { isMessageCompacted } from "../shared-utils"
19+
import { isMessageCompacted } from "../state/utils"
1920
import {
2021
getFilePathsFromParameters,
2122
isFilePathProtected,

0 commit comments

Comments
 (0)