@tarquinen/opencode-dcp 3.2.2-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.
Files changed (132) hide show
  1. package/README.md +4 -16
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/commands/compression-targets.d.ts +1 -0
  6. package/dist/lib/commands/compression-targets.d.ts.map +1 -1
  7. package/dist/lib/commands/compression-targets.js +1 -0
  8. package/dist/lib/commands/compression-targets.js.map +1 -1
  9. package/dist/lib/commands/manual.js +1 -1
  10. package/dist/lib/commands/manual.js.map +1 -1
  11. package/dist/lib/commands/stats.d.ts.map +1 -1
  12. package/dist/lib/commands/stats.js +41 -6
  13. package/dist/lib/commands/stats.js.map +1 -1
  14. package/dist/lib/compress/message-utils.d.ts +2 -2
  15. package/dist/lib/compress/message-utils.d.ts.map +1 -1
  16. package/dist/lib/compress/message-utils.js +87 -31
  17. package/dist/lib/compress/message-utils.js.map +1 -1
  18. package/dist/lib/compress/message.d.ts.map +1 -1
  19. package/dist/lib/compress/message.js +10 -6
  20. package/dist/lib/compress/message.js.map +1 -1
  21. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  22. package/dist/lib/compress/pipeline.js +2 -0
  23. package/dist/lib/compress/pipeline.js.map +1 -1
  24. package/dist/lib/compress/range.d.ts.map +1 -1
  25. package/dist/lib/compress/range.js +6 -2
  26. package/dist/lib/compress/range.js.map +1 -1
  27. package/dist/lib/compress/state.d.ts +2 -1
  28. package/dist/lib/compress/state.d.ts.map +1 -1
  29. package/dist/lib/compress/state.js +16 -0
  30. package/dist/lib/compress/state.js.map +1 -1
  31. package/dist/lib/compress/timing.d.ts +18 -0
  32. package/dist/lib/compress/timing.d.ts.map +1 -0
  33. package/dist/lib/compress/timing.js +42 -0
  34. package/dist/lib/compress/timing.js.map +1 -0
  35. package/dist/lib/compress/types.d.ts +2 -0
  36. package/dist/lib/compress/types.d.ts.map +1 -1
  37. package/dist/lib/config.js +1 -1
  38. package/dist/lib/config.js.map +1 -1
  39. package/dist/lib/hooks.d.ts +3 -0
  40. package/dist/lib/hooks.d.ts.map +1 -1
  41. package/dist/lib/hooks.js +72 -2
  42. package/dist/lib/hooks.js.map +1 -1
  43. package/dist/lib/messages/inject/inject.js +2 -2
  44. package/dist/lib/messages/inject/utils.d.ts +0 -1
  45. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  46. package/dist/lib/messages/inject/utils.js +1 -27
  47. package/dist/lib/messages/inject/utils.js.map +1 -1
  48. package/dist/lib/messages/utils.d.ts +1 -1
  49. package/dist/lib/messages/utils.d.ts.map +1 -1
  50. package/dist/lib/messages/utils.js +5 -12
  51. package/dist/lib/messages/utils.js.map +1 -1
  52. package/dist/lib/prompts/compress-message.d.ts +1 -1
  53. package/dist/lib/prompts/compress-message.d.ts.map +1 -1
  54. package/dist/lib/prompts/compress-message.js +11 -12
  55. package/dist/lib/prompts/compress-message.js.map +1 -1
  56. package/dist/lib/prompts/compress-range.d.ts +1 -1
  57. package/dist/lib/prompts/compress-range.d.ts.map +1 -1
  58. package/dist/lib/prompts/compress-range.js +1 -1
  59. package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
  60. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  61. package/dist/lib/prompts/context-limit-nudge.js +3 -9
  62. package/dist/lib/prompts/context-limit-nudge.js.map +1 -1
  63. package/dist/lib/prompts/extensions/nudge.d.ts +5 -0
  64. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
  65. package/dist/lib/prompts/extensions/nudge.js +35 -0
  66. package/dist/lib/prompts/extensions/nudge.js.map +1 -0
  67. package/dist/lib/prompts/extensions/system.d.ts +4 -0
  68. package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
  69. package/dist/lib/prompts/extensions/system.js +30 -0
  70. package/dist/lib/prompts/extensions/system.js.map +1 -0
  71. package/dist/lib/prompts/extensions/tool.d.ts +3 -0
  72. package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
  73. package/dist/lib/prompts/extensions/tool.js +34 -0
  74. package/dist/lib/prompts/extensions/tool.js.map +1 -0
  75. package/dist/lib/prompts/index.d.ts +1 -1
  76. package/dist/lib/prompts/index.d.ts.map +1 -1
  77. package/dist/lib/prompts/index.js +12 -13
  78. package/dist/lib/prompts/index.js.map +1 -1
  79. package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
  80. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  81. package/dist/lib/prompts/iteration-nudge.js +0 -2
  82. package/dist/lib/prompts/iteration-nudge.js.map +1 -1
  83. package/dist/lib/prompts/store.d.ts +2 -2
  84. package/dist/lib/prompts/store.d.ts.map +1 -1
  85. package/dist/lib/prompts/store.js +6 -6
  86. package/dist/lib/prompts/store.js.map +1 -1
  87. package/dist/lib/prompts/system.d.ts +1 -1
  88. package/dist/lib/prompts/system.d.ts.map +1 -1
  89. package/dist/lib/prompts/system.js +0 -13
  90. package/dist/lib/prompts/system.js.map +1 -1
  91. package/dist/lib/prompts/turn-nudge.d.ts +1 -1
  92. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  93. package/dist/lib/prompts/turn-nudge.js +1 -2
  94. package/dist/lib/prompts/turn-nudge.js.map +1 -1
  95. package/dist/lib/state/persistence.d.ts.map +1 -1
  96. package/dist/lib/state/persistence.js +13 -16
  97. package/dist/lib/state/persistence.js.map +1 -1
  98. package/dist/lib/state/state.d.ts.map +1 -1
  99. package/dist/lib/state/state.js +9 -0
  100. package/dist/lib/state/state.js.map +1 -1
  101. package/dist/lib/state/types.d.ts +4 -0
  102. package/dist/lib/state/types.d.ts.map +1 -1
  103. package/dist/lib/state/utils.d.ts +7 -6
  104. package/dist/lib/state/utils.d.ts.map +1 -1
  105. package/dist/lib/state/utils.js +17 -0
  106. package/dist/lib/state/utils.js.map +1 -1
  107. package/lib/compress/index.ts +3 -0
  108. package/lib/compress/message-utils.ts +250 -0
  109. package/lib/compress/message.ts +137 -0
  110. package/lib/compress/pipeline.ts +106 -0
  111. package/lib/compress/protected-content.ts +154 -0
  112. package/lib/compress/range-utils.ts +308 -0
  113. package/lib/compress/range.ts +180 -0
  114. package/lib/compress/search.ts +267 -0
  115. package/lib/compress/state.ts +268 -0
  116. package/lib/compress/timing.ts +77 -0
  117. package/lib/compress/types.ts +108 -0
  118. package/lib/config.ts +1 -1
  119. package/lib/message-ids.ts +172 -0
  120. package/lib/state/persistence.ts +20 -24
  121. package/lib/state/state.ts +10 -0
  122. package/lib/state/types.ts +4 -0
  123. package/lib/state/utils.ts +30 -6
  124. package/package.json +12 -5
  125. package/dist/lib/prompts/internal-overlays.d.ts +0 -5
  126. package/dist/lib/prompts/internal-overlays.d.ts.map +0 -1
  127. package/dist/lib/prompts/internal-overlays.js +0 -49
  128. package/dist/lib/prompts/internal-overlays.js.map +0 -1
  129. package/dist/lib/prompts/message-priority-guidance.d.ts +0 -2
  130. package/dist/lib/prompts/message-priority-guidance.d.ts.map +0 -1
  131. package/dist/lib/prompts/message-priority-guidance.js +0 -9
  132. package/dist/lib/prompts/message-priority-guidance.js.map +0 -1
@@ -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
@@ -711,7 +711,7 @@ const defaultConfig: PluginConfig = {
711
711
  },
712
712
  protectedFilePatterns: [],
713
713
  compress: {
714
- mode: "range",
714
+ mode: "message",
715
715
  permission: "allow",
716
716
  showCompression: false,
717
717
  summaryBuffer: true,
@@ -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
+ }
@@ -10,6 +10,7 @@ import { homedir } from "os"
10
10
  import { join } from "path"
11
11
  import type { CompressionBlock, PrunedMessageEntry, SessionState, SessionStats } from "./types"
12
12
  import type { Logger } from "../logger"
13
+ import { serializePruneMessagesState } from "./utils"
13
14
 
14
15
  /** Prune state as stored on disk */
15
16
  export interface PersistedPruneMessagesState {
@@ -58,6 +59,23 @@ function getSessionFilePath(sessionId: string): string {
58
59
  return join(STORAGE_DIR, `${sessionId}.json`)
59
60
  }
60
61
 
62
+ async function writePersistedSessionState(
63
+ sessionId: string,
64
+ state: PersistedSessionState,
65
+ logger: Logger,
66
+ ): Promise<void> {
67
+ await ensureStorageDir()
68
+
69
+ const filePath = getSessionFilePath(sessionId)
70
+ const content = JSON.stringify(state, null, 2)
71
+ await fs.writeFile(filePath, content, "utf-8")
72
+
73
+ logger.info("Saved session state to disk", {
74
+ sessionId,
75
+ totalTokensSaved: state.stats.totalPruneTokens,
76
+ })
77
+ }
78
+
61
79
  export async function saveSessionState(
62
80
  sessionState: SessionState,
63
81
  logger: Logger,
@@ -68,26 +86,11 @@ export async function saveSessionState(
68
86
  return
69
87
  }
70
88
 
71
- await ensureStorageDir()
72
-
73
89
  const state: PersistedSessionState = {
74
90
  sessionName: sessionName,
75
91
  prune: {
76
92
  tools: Object.fromEntries(sessionState.prune.tools),
77
- messages: {
78
- byMessageId: Object.fromEntries(sessionState.prune.messages.byMessageId),
79
- blocksById: Object.fromEntries(
80
- Array.from(sessionState.prune.messages.blocksById.entries()).map(
81
- ([blockId, block]) => [String(blockId), block],
82
- ),
83
- ),
84
- activeBlockIds: Array.from(sessionState.prune.messages.activeBlockIds),
85
- activeByAnchorMessageId: Object.fromEntries(
86
- sessionState.prune.messages.activeByAnchorMessageId,
87
- ),
88
- nextBlockId: sessionState.prune.messages.nextBlockId,
89
- nextRunId: sessionState.prune.messages.nextRunId,
90
- },
93
+ messages: serializePruneMessagesState(sessionState.prune.messages),
91
94
  },
92
95
  nudges: {
93
96
  contextLimitAnchors: Array.from(sessionState.nudges.contextLimitAnchors),
@@ -98,14 +101,7 @@ export async function saveSessionState(
98
101
  lastUpdated: new Date().toISOString(),
99
102
  }
100
103
 
101
- const filePath = getSessionFilePath(sessionState.sessionId)
102
- const content = JSON.stringify(state, null, 2)
103
- await fs.writeFile(filePath, content, "utf-8")
104
-
105
- logger.info("Saved session state to disk", {
106
- sessionId: sessionState.sessionId,
107
- totalTokensSaved: state.stats.totalPruneTokens,
108
- })
104
+ await writePersistedSessionState(sessionState.sessionId, state, logger)
109
105
  } catch (error: any) {
110
106
  logger.error("Failed to save session state", {
111
107
  sessionId: sessionState.sessionId,
@@ -1,5 +1,6 @@
1
1
  import type { SessionState, ToolParameterEntry, WithParts } from "./types"
2
2
  import type { Logger } from "../logger"
3
+ import { applyPendingCompressionDurations } from "../compress/timing"
3
4
  import { loadSessionState, saveSessionState } from "./persistence"
4
5
  import {
5
6
  isSubAgentSession,
@@ -81,6 +82,10 @@ export function createSessionState(): SessionState {
81
82
  pruneTokenCounter: 0,
82
83
  totalPruneTokens: 0,
83
84
  },
85
+ compressionTiming: {
86
+ startsByCallId: new Map<string, number>(),
87
+ pendingByCallId: new Map(),
88
+ },
84
89
  toolParameters: new Map<string, ToolParameterEntry>(),
85
90
  subAgentResultCache: new Map<string, string>(),
86
91
  toolIdList: [],
@@ -177,4 +182,9 @@ export async function ensureSessionInitialized(
177
182
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
178
183
  totalPruneTokens: persisted.stats?.totalPruneTokens || 0,
179
184
  }
185
+
186
+ const applied = applyPendingCompressionDurations(state)
187
+ if (applied > 0) {
188
+ await saveSessionState(state, logger)
189
+ }
180
190
  }
@@ -1,3 +1,4 @@
1
+ import type { CompressionTimingState } from "../compress/timing"
1
2
  import { Message, Part } from "@opencode-ai/sdk/v2"
2
3
 
3
4
  export interface WithParts {
@@ -36,6 +37,7 @@ export interface CompressionBlock {
36
37
  deactivatedByUser: boolean
37
38
  compressedTokens: number
38
39
  summaryTokens: number
40
+ durationMs: number
39
41
  mode?: CompressionMode
40
42
  topic: string
41
43
  batchTopic?: string
@@ -43,6 +45,7 @@ export interface CompressionBlock {
43
45
  endId: string
44
46
  anchorMessageId: string
45
47
  compressMessageId: string
48
+ compressCallId?: string
46
49
  includedBlockIds: number[]
47
50
  consumedBlockIds: number[]
48
51
  parentBlockIds: number[]
@@ -96,6 +99,7 @@ export interface SessionState {
96
99
  prune: Prune
97
100
  nudges: Nudges
98
101
  stats: SessionStats
102
+ compressionTiming: CompressionTimingState
99
103
  toolParameters: Map<string, ToolParameterEntry>
100
104
  subAgentResultCache: Map<string, string>
101
105
  toolIdList: string[]
@@ -20,12 +20,30 @@ export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean
20
20
  }
21
21
 
22
22
  interface PersistedPruneMessagesState {
23
- byMessageId?: Record<string, PrunedMessageEntry>
24
- blocksById?: Record<string, CompressionBlock>
25
- activeBlockIds?: number[]
26
- activeByAnchorMessageId?: Record<string, number>
27
- nextBlockId?: number
28
- nextRunId?: number
23
+ byMessageId: Record<string, PrunedMessageEntry>
24
+ blocksById: Record<string, CompressionBlock>
25
+ activeBlockIds: number[]
26
+ activeByAnchorMessageId: Record<string, number>
27
+ nextBlockId: number
28
+ nextRunId: number
29
+ }
30
+
31
+ export function serializePruneMessagesState(
32
+ messagesState: PruneMessagesState,
33
+ ): PersistedPruneMessagesState {
34
+ return {
35
+ byMessageId: Object.fromEntries(messagesState.byMessageId),
36
+ blocksById: Object.fromEntries(
37
+ Array.from(messagesState.blocksById.entries()).map(([blockId, block]) => [
38
+ String(blockId),
39
+ block,
40
+ ]),
41
+ ),
42
+ activeBlockIds: Array.from(messagesState.activeBlockIds),
43
+ activeByAnchorMessageId: Object.fromEntries(messagesState.activeByAnchorMessageId),
44
+ nextBlockId: messagesState.nextBlockId,
45
+ nextRunId: messagesState.nextRunId,
46
+ }
29
47
  }
30
48
 
31
49
  export async function isSubAgentSession(client: any, sessionID: string): Promise<boolean> {
@@ -178,6 +196,10 @@ export function loadPruneMessagesState(
178
196
  : typeof block.summary === "string"
179
197
  ? countTokens(block.summary)
180
198
  : 0,
199
+ durationMs:
200
+ typeof block.durationMs === "number" && Number.isFinite(block.durationMs)
201
+ ? Math.max(0, block.durationMs)
202
+ : 0,
181
203
  mode: block.mode === "range" || block.mode === "message" ? block.mode : undefined,
182
204
  topic: typeof block.topic === "string" ? block.topic : "",
183
205
  batchTopic:
@@ -192,6 +214,8 @@ export function loadPruneMessagesState(
192
214
  typeof block.anchorMessageId === "string" ? block.anchorMessageId : "",
193
215
  compressMessageId:
194
216
  typeof block.compressMessageId === "string" ? block.compressMessageId : "",
217
+ compressCallId:
218
+ typeof block.compressCallId === "string" ? block.compressCallId : undefined,
195
219
  includedBlockIds: toNumberArray(block.includedBlockIds),
196
220
  consumedBlockIds: toNumberArray(block.consumedBlockIds),
197
221
  parentBlockIds: toNumberArray(block.parentBlockIds),
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.2-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",
@@ -57,11 +57,11 @@
57
57
  "dependencies": {
58
58
  "@anthropic-ai/tokenizer": "^0.0.4",
59
59
  "@opencode-ai/sdk": "^1.3.2",
60
- "@opentui/core": "0.0.0-20260307-536c401b",
61
- "@opentui/solid": "0.0.0-20260307-536c401b",
60
+ "@opentui/core": "0.1.92",
61
+ "@opentui/solid": "0.1.92",
62
62
  "fuzzball": "^2.2.3",
63
63
  "jsonc-parser": "^3.3.1",
64
- "solid-js": "1.9.9",
64
+ "solid-js": "1.9.11",
65
65
  "zod": "^4.3.6"
66
66
  },
67
67
  "devDependencies": {
@@ -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/",
@@ -86,5 +88,10 @@
86
88
  "tui/index.tsx",
87
89
  "README.md",
88
90
  "LICENSE"
89
- ]
91
+ ],
92
+ "directories": {
93
+ "doc": "docs",
94
+ "lib": "lib",
95
+ "test": "tests"
96
+ }
90
97
  }
@@ -1,5 +0,0 @@
1
- export declare const MANUAL_MODE_SYSTEM_OVERLAY = "<dcp-system-reminder>\nManual mode is enabled. Do NOT use compress unless the user has explicitly triggered it through a manual marker.\n\nOnly use the compress tool after seeing `<compress triggered manually>` in the current user instruction context.\n\nIssue exactly ONE compress call per manual trigger. Do NOT launch multiple compress calls in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.\n\nAfter completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.\n</dcp-system-reminder>\n";
2
- export declare const SUBAGENT_SYSTEM_OVERLAY = "<dcp-system-reminder>\nYou are operating in a subagent environment.\n\nThe initial subagent instruction is imperative and must be followed exactly.\nIt is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.\nAll subsequent messages in the session will have IDs.\n</dcp-system-reminder>\n";
3
- export declare const RANGE_FORMAT_OVERLAY = "\nTHE FORMAT OF COMPRESS\n\n```\n{\n topic: string, // Short label (3-5 words) - e.g., \"Auth System Exploration\"\n content: [ // One or more ranges to compress\n {\n startId: string, // Boundary ID at range start: mNNNN or bN\n endId: string, // Boundary ID at range end: mNNNN or bN\n summary: string // Complete technical summary replacing all content in range\n }\n ]\n}\n```";
4
- export declare const MESSAGE_FORMAT_OVERLAY = "\nTHE FORMAT OF COMPRESS\n\n```\n{\n topic: string, // Short label (3-5 words) for the overall batch\n content: [ // One or more messages to compress independently\n {\n messageId: string, // Raw message ID only: mNNNN (ignore metadata attributes like priority)\n topic: string, // Short label (3-5 words) for this one message summary\n summary: string // Complete technical summary replacing that one message\n }\n ]\n}\n```";
5
- //# sourceMappingURL=internal-overlays.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"internal-overlays.d.ts","sourceRoot":"","sources":["../../../lib/prompts/internal-overlays.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,0BAA0B,mrBAStC,CAAA;AAED,eAAO,MAAM,uBAAuB,8VAOnC,CAAA;AAED,eAAO,MAAM,oBAAoB,gcAc1B,CAAA;AAEP,eAAO,MAAM,sBAAsB,2eAc5B,CAAA"}
@@ -1,49 +0,0 @@
1
- export const MANUAL_MODE_SYSTEM_OVERLAY = `<dcp-system-reminder>
2
- Manual mode is enabled. Do NOT use compress unless the user has explicitly triggered it through a manual marker.
3
-
4
- Only use the compress tool after seeing \`<compress triggered manually>\` in the current user instruction context.
5
-
6
- Issue exactly ONE compress call per manual trigger. Do NOT launch multiple compress calls in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.
7
-
8
- After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
9
- </dcp-system-reminder>
10
- `;
11
- export const SUBAGENT_SYSTEM_OVERLAY = `<dcp-system-reminder>
12
- You are operating in a subagent environment.
13
-
14
- The initial subagent instruction is imperative and must be followed exactly.
15
- It is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.
16
- All subsequent messages in the session will have IDs.
17
- </dcp-system-reminder>
18
- `;
19
- export const RANGE_FORMAT_OVERLAY = `
20
- THE FORMAT OF COMPRESS
21
-
22
- \`\`\`
23
- {
24
- topic: string, // Short label (3-5 words) - e.g., "Auth System Exploration"
25
- content: [ // One or more ranges to compress
26
- {
27
- startId: string, // Boundary ID at range start: mNNNN or bN
28
- endId: string, // Boundary ID at range end: mNNNN or bN
29
- summary: string // Complete technical summary replacing all content in range
30
- }
31
- ]
32
- }
33
- \`\`\``;
34
- export const MESSAGE_FORMAT_OVERLAY = `
35
- THE FORMAT OF COMPRESS
36
-
37
- \`\`\`
38
- {
39
- topic: string, // Short label (3-5 words) for the overall batch
40
- content: [ // One or more messages to compress independently
41
- {
42
- messageId: string, // Raw message ID only: mNNNN (ignore metadata attributes like priority)
43
- topic: string, // Short label (3-5 words) for this one message summary
44
- summary: string // Complete technical summary replacing that one message
45
- }
46
- ]
47
- }
48
- \`\`\``;
49
- //# sourceMappingURL=internal-overlays.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"internal-overlays.js","sourceRoot":"","sources":["../../../lib/prompts/internal-overlays.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,0BAA0B,GAAG;;;;;;;;;CASzC,CAAA;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG;;;;;;;CAOtC,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;OAc7B,CAAA;AAEP,MAAM,CAAC,MAAM,sBAAsB,GAAG;;;;;;;;;;;;;;OAc/B,CAAA"}
@@ -1,2 +0,0 @@
1
- export declare function renderMessagePriorityGuidance(priorityLabel: string, refs: string[]): string;
2
- //# sourceMappingURL=message-priority-guidance.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message-priority-guidance.d.ts","sourceRoot":"","sources":["../../../lib/prompts/message-priority-guidance.ts"],"names":[],"mappings":"AAAA,wBAAgB,6BAA6B,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ3F"}
@@ -1,9 +0,0 @@
1
- export function renderMessagePriorityGuidance(priorityLabel, refs) {
2
- const refList = refs.length > 0 ? refs.join(", ") : "none";
3
- return [
4
- "Message priority context:",
5
- "- Higher-priority older messages consume more context and should be compressed before lower-priority ones when safely closed.",
6
- `- ${priorityLabel}-priority message IDs before this point: ${refList}`,
7
- ].join("\n");
8
- }
9
- //# sourceMappingURL=message-priority-guidance.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"message-priority-guidance.js","sourceRoot":"","sources":["../../../lib/prompts/message-priority-guidance.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,6BAA6B,CAAC,aAAqB,EAAE,IAAc;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAA;IAE1D,OAAO;QACH,2BAA2B;QAC3B,+HAA+H;QAC/H,KAAK,aAAa,4CAA4C,OAAO,EAAE;KAC1E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAChB,CAAC"}