@tarquinen/opencode-dcp 3.2.3-beta0 → 3.2.4-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.
@@ -0,0 +1,108 @@
1
+ import type { PluginConfig } from "../config"
2
+ import type { Logger } from "../logger"
3
+ import type { PromptStore } from "../prompts/store"
4
+ import type { CompressionBlock, CompressionMode, SessionState, WithParts } from "../state"
5
+
6
+ export interface ToolContext {
7
+ client: any
8
+ state: SessionState
9
+ logger: Logger
10
+ config: PluginConfig
11
+ prompts: PromptStore
12
+ }
13
+
14
+ export interface CompressRangeEntry {
15
+ startId: string
16
+ endId: string
17
+ summary: string
18
+ }
19
+
20
+ export interface CompressRangeToolArgs {
21
+ topic: string
22
+ content: CompressRangeEntry[]
23
+ }
24
+
25
+ export interface CompressMessageEntry {
26
+ messageId: string
27
+ topic: string
28
+ summary: string
29
+ }
30
+
31
+ export interface CompressMessageToolArgs {
32
+ topic: string
33
+ content: CompressMessageEntry[]
34
+ }
35
+
36
+ export interface BoundaryReference {
37
+ kind: "message" | "compressed-block"
38
+ rawIndex: number
39
+ messageId?: string
40
+ blockId?: number
41
+ anchorMessageId?: string
42
+ }
43
+
44
+ export interface SearchContext {
45
+ rawMessages: WithParts[]
46
+ rawMessagesById: Map<string, WithParts>
47
+ rawIndexById: Map<string, number>
48
+ summaryByBlockId: Map<number, CompressionBlock>
49
+ }
50
+
51
+ export interface SelectionResolution {
52
+ startReference: BoundaryReference
53
+ endReference: BoundaryReference
54
+ messageIds: string[]
55
+ messageTokenById: Map<string, number>
56
+ toolIds: string[]
57
+ requiredBlockIds: number[]
58
+ }
59
+
60
+ export interface ResolvedMessageCompression {
61
+ entry: CompressMessageEntry
62
+ selection: SelectionResolution
63
+ anchorMessageId: string
64
+ }
65
+
66
+ export interface ResolvedRangeCompression {
67
+ index: number
68
+ entry: CompressRangeEntry
69
+ selection: SelectionResolution
70
+ anchorMessageId: string
71
+ }
72
+
73
+ export interface ResolvedMessageCompressionsResult {
74
+ plans: ResolvedMessageCompression[]
75
+ skippedIssues: string[]
76
+ skippedCount: number
77
+ }
78
+
79
+ export interface ParsedBlockPlaceholder {
80
+ raw: string
81
+ blockId: number
82
+ startIndex: number
83
+ endIndex: number
84
+ }
85
+
86
+ export interface InjectedSummaryResult {
87
+ expandedSummary: string
88
+ consumedBlockIds: number[]
89
+ }
90
+
91
+ export interface AppliedCompressionResult {
92
+ compressedTokens: number
93
+ messageIds: string[]
94
+ newlyCompressedMessageIds: string[]
95
+ newlyCompressedToolIds: string[]
96
+ }
97
+
98
+ export interface CompressionStateInput {
99
+ topic: string
100
+ batchTopic: string
101
+ startId: string
102
+ endId: string
103
+ mode: CompressionMode
104
+ runId: number
105
+ compressMessageId: string
106
+ compressCallId?: string
107
+ summaryTokens: number
108
+ }
package/lib/config.ts CHANGED
@@ -642,7 +642,7 @@ function scheduleConfigWarning(
642
642
  if (!notify) return
643
643
  try {
644
644
  notify(title, message)
645
- } catch { }
645
+ } catch {}
646
646
  }, 7000)
647
647
  }
648
648
 
@@ -766,8 +766,8 @@ function getConfigPaths(directory?: string): {
766
766
  const global = existsSync(GLOBAL_CONFIG_PATH_JSONC)
767
767
  ? GLOBAL_CONFIG_PATH_JSONC
768
768
  : existsSync(GLOBAL_CONFIG_PATH_JSON)
769
- ? GLOBAL_CONFIG_PATH_JSON
770
- : null
769
+ ? GLOBAL_CONFIG_PATH_JSON
770
+ : null
771
771
 
772
772
  let configDir: string | null = null
773
773
  const opencodeConfigDir = process.env.OPENCODE_CONFIG_DIR
@@ -777,8 +777,8 @@ function getConfigPaths(directory?: string): {
777
777
  configDir = existsSync(configJsonc)
778
778
  ? configJsonc
779
779
  : existsSync(configJson)
780
- ? configJson
781
- : null
780
+ ? configJson
781
+ : null
782
782
  }
783
783
 
784
784
  let project: string | null = null
@@ -790,8 +790,8 @@ function getConfigPaths(directory?: string): {
790
790
  project = existsSync(projectJsonc)
791
791
  ? projectJsonc
792
792
  : existsSync(projectJson)
793
- ? projectJson
794
- : null
793
+ ? projectJson
794
+ : null
795
795
  }
796
796
  }
797
797
 
@@ -0,0 +1,172 @@
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 MESSAGE_ID_TAG_NAME = "dcp-message-id"
7
+
8
+ const MESSAGE_REF_WIDTH = 4
9
+ const MESSAGE_REF_MIN_INDEX = 1
10
+ export const MESSAGE_REF_MAX_INDEX = 9999
11
+
12
+ export type ParsedBoundaryId =
13
+ | {
14
+ kind: "message"
15
+ ref: string
16
+ index: number
17
+ }
18
+ | {
19
+ kind: "compressed-block"
20
+ ref: string
21
+ blockId: number
22
+ }
23
+
24
+ export function formatMessageRef(index: number): string {
25
+ if (
26
+ !Number.isInteger(index) ||
27
+ index < MESSAGE_REF_MIN_INDEX ||
28
+ index > MESSAGE_REF_MAX_INDEX
29
+ ) {
30
+ throw new Error(
31
+ `Message ID index out of bounds: ${index}. Supported range is 0-${MESSAGE_REF_MAX_INDEX}.`,
32
+ )
33
+ }
34
+ return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`
35
+ }
36
+
37
+ export function formatBlockRef(blockId: number): string {
38
+ if (!Number.isInteger(blockId) || blockId < 1) {
39
+ throw new Error(`Invalid block ID: ${blockId}`)
40
+ }
41
+ return `b${blockId}`
42
+ }
43
+
44
+ export function parseMessageRef(ref: string): number | null {
45
+ const normalized = ref.trim().toLowerCase()
46
+ const match = normalized.match(MESSAGE_REF_REGEX)
47
+ if (!match) {
48
+ return null
49
+ }
50
+ const index = Number.parseInt(match[1], 10)
51
+ if (!Number.isInteger(index)) {
52
+ return null
53
+ }
54
+ if (index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
55
+ return null
56
+ }
57
+ return index
58
+ }
59
+
60
+ export function parseBlockRef(ref: string): number | null {
61
+ const normalized = ref.trim().toLowerCase()
62
+ const match = normalized.match(BLOCK_REF_REGEX)
63
+ if (!match) {
64
+ return null
65
+ }
66
+ const id = Number.parseInt(match[1], 10)
67
+ return Number.isInteger(id) ? id : null
68
+ }
69
+
70
+ export function parseBoundaryId(id: string): ParsedBoundaryId | null {
71
+ const normalized = id.trim().toLowerCase()
72
+ const messageIndex = parseMessageRef(normalized)
73
+ if (messageIndex !== null) {
74
+ return {
75
+ kind: "message",
76
+ ref: formatMessageRef(messageIndex),
77
+ index: messageIndex,
78
+ }
79
+ }
80
+
81
+ const blockId = parseBlockRef(normalized)
82
+ if (blockId !== null) {
83
+ return {
84
+ kind: "compressed-block",
85
+ ref: formatBlockRef(blockId),
86
+ blockId,
87
+ }
88
+ }
89
+
90
+ return null
91
+ }
92
+
93
+ function escapeXmlAttribute(value: string): string {
94
+ return value
95
+ .replace(/&/g, "&amp;")
96
+ .replace(/"/g, "&quot;")
97
+ .replace(/</g, "&lt;")
98
+ .replace(/>/g, "&gt;")
99
+ }
100
+
101
+ export function formatMessageIdTag(
102
+ ref: string,
103
+ attributes?: Record<string, string | undefined>,
104
+ ): string {
105
+ const serializedAttributes = Object.entries(attributes || {})
106
+ .sort(([left], [right]) => left.localeCompare(right))
107
+ .map(([name, value]) => {
108
+ if (name.trim().length === 0 || typeof value !== "string" || value.length === 0) {
109
+ return ""
110
+ }
111
+
112
+ return ` ${name}="${escapeXmlAttribute(value)}"`
113
+ })
114
+ .join("")
115
+
116
+ return `\n<${MESSAGE_ID_TAG_NAME}${serializedAttributes}>${ref}</${MESSAGE_ID_TAG_NAME}>`
117
+ }
118
+
119
+ export function assignMessageRefs(state: SessionState, messages: WithParts[]): number {
120
+ let assigned = 0
121
+ let skippedSubAgentPrompt = false
122
+
123
+ for (const message of messages) {
124
+ if (isIgnoredUserMessage(message)) {
125
+ continue
126
+ }
127
+
128
+ if (state.isSubAgent && !skippedSubAgentPrompt && message.info.role === "user") {
129
+ skippedSubAgentPrompt = true
130
+ continue
131
+ }
132
+
133
+ const rawMessageId = message.info.id
134
+ if (typeof rawMessageId !== "string" || rawMessageId.length === 0) {
135
+ continue
136
+ }
137
+
138
+ const existingRef = state.messageIds.byRawId.get(rawMessageId)
139
+ if (existingRef) {
140
+ if (state.messageIds.byRef.get(existingRef) !== rawMessageId) {
141
+ state.messageIds.byRef.set(existingRef, rawMessageId)
142
+ }
143
+ continue
144
+ }
145
+
146
+ const ref = allocateNextMessageRef(state)
147
+ state.messageIds.byRawId.set(rawMessageId, ref)
148
+ state.messageIds.byRef.set(ref, rawMessageId)
149
+ assigned++
150
+ }
151
+
152
+ return assigned
153
+ }
154
+
155
+ function allocateNextMessageRef(state: SessionState): string {
156
+ let candidate = Number.isInteger(state.messageIds.nextRef)
157
+ ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef)
158
+ : MESSAGE_REF_MIN_INDEX
159
+
160
+ while (candidate <= MESSAGE_REF_MAX_INDEX) {
161
+ const ref = formatMessageRef(candidate)
162
+ if (!state.messageIds.byRef.has(ref)) {
163
+ state.messageIds.nextRef = candidate + 1
164
+ return ref
165
+ }
166
+ candidate++
167
+ }
168
+
169
+ throw new Error(
170
+ `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`,
171
+ )
172
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@tarquinen/opencode-dcp",
4
- "version": "3.2.3-beta0",
4
+ "version": "3.2.4-beta0",
5
5
  "type": "module",
6
6
  "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
7
7
  "main": "./dist/index.js",
@@ -74,9 +74,11 @@
74
74
  "files": [
75
75
  "dist/",
76
76
  "lib/analysis/",
77
+ "lib/compress/",
77
78
  "lib/state/",
78
79
  "lib/config.ts",
79
80
  "lib/logger.ts",
81
+ "lib/message-ids.ts",
80
82
  "lib/messages/query.ts",
81
83
  "lib/token-utils.ts",
82
84
  "tui/data/",