@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.
- package/README.md +4 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/commands/compression-targets.d.ts +1 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -1
- package/dist/lib/commands/compression-targets.js +1 -0
- package/dist/lib/commands/compression-targets.js.map +1 -1
- package/dist/lib/commands/manual.js +1 -1
- package/dist/lib/commands/manual.js.map +1 -1
- package/dist/lib/commands/stats.d.ts.map +1 -1
- package/dist/lib/commands/stats.js +41 -6
- package/dist/lib/commands/stats.js.map +1 -1
- package/dist/lib/compress/message-utils.d.ts +2 -2
- package/dist/lib/compress/message-utils.d.ts.map +1 -1
- package/dist/lib/compress/message-utils.js +87 -31
- package/dist/lib/compress/message-utils.js.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/message.js +10 -6
- package/dist/lib/compress/message.js.map +1 -1
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.js +2 -0
- package/dist/lib/compress/pipeline.js.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/range.js +6 -2
- package/dist/lib/compress/range.js.map +1 -1
- package/dist/lib/compress/state.d.ts +2 -1
- package/dist/lib/compress/state.d.ts.map +1 -1
- package/dist/lib/compress/state.js +16 -0
- package/dist/lib/compress/state.js.map +1 -1
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/timing.js +42 -0
- package/dist/lib/compress/timing.js.map +1 -0
- package/dist/lib/compress/types.d.ts +2 -0
- package/dist/lib/compress/types.d.ts.map +1 -1
- package/dist/lib/config.js +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/hooks.d.ts +3 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/hooks.js +72 -2
- package/dist/lib/hooks.js.map +1 -1
- package/dist/lib/messages/inject/inject.js +2 -2
- package/dist/lib/messages/inject/utils.d.ts +0 -1
- package/dist/lib/messages/inject/utils.d.ts.map +1 -1
- package/dist/lib/messages/inject/utils.js +1 -27
- package/dist/lib/messages/inject/utils.js.map +1 -1
- package/dist/lib/messages/utils.d.ts +1 -1
- package/dist/lib/messages/utils.d.ts.map +1 -1
- package/dist/lib/messages/utils.js +5 -12
- package/dist/lib/messages/utils.js.map +1 -1
- package/dist/lib/prompts/compress-message.d.ts +1 -1
- package/dist/lib/prompts/compress-message.d.ts.map +1 -1
- package/dist/lib/prompts/compress-message.js +11 -12
- package/dist/lib/prompts/compress-message.js.map +1 -1
- package/dist/lib/prompts/compress-range.d.ts +1 -1
- package/dist/lib/prompts/compress-range.d.ts.map +1 -1
- package/dist/lib/prompts/compress-range.js +1 -1
- package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
- package/dist/lib/prompts/context-limit-nudge.js +3 -9
- package/dist/lib/prompts/context-limit-nudge.js.map +1 -1
- package/dist/lib/prompts/extensions/nudge.d.ts +5 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.js +35 -0
- package/dist/lib/prompts/extensions/nudge.js.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/system.js +30 -0
- package/dist/lib/prompts/extensions/system.js.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/extensions/tool.js +34 -0
- package/dist/lib/prompts/extensions/tool.js.map +1 -0
- package/dist/lib/prompts/index.d.ts +1 -1
- package/dist/lib/prompts/index.d.ts.map +1 -1
- package/dist/lib/prompts/index.js +12 -13
- package/dist/lib/prompts/index.js.map +1 -1
- package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
- package/dist/lib/prompts/iteration-nudge.js +0 -2
- package/dist/lib/prompts/iteration-nudge.js.map +1 -1
- package/dist/lib/prompts/store.d.ts +2 -2
- package/dist/lib/prompts/store.d.ts.map +1 -1
- package/dist/lib/prompts/store.js +6 -6
- package/dist/lib/prompts/store.js.map +1 -1
- package/dist/lib/prompts/system.d.ts +1 -1
- package/dist/lib/prompts/system.d.ts.map +1 -1
- package/dist/lib/prompts/system.js +0 -13
- package/dist/lib/prompts/system.js.map +1 -1
- package/dist/lib/prompts/turn-nudge.d.ts +1 -1
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
- package/dist/lib/prompts/turn-nudge.js +1 -2
- package/dist/lib/prompts/turn-nudge.js.map +1 -1
- package/dist/lib/state/persistence.d.ts.map +1 -1
- package/dist/lib/state/persistence.js +13 -16
- package/dist/lib/state/persistence.js.map +1 -1
- package/dist/lib/state/state.d.ts.map +1 -1
- package/dist/lib/state/state.js +9 -0
- package/dist/lib/state/state.js.map +1 -1
- package/dist/lib/state/types.d.ts +4 -0
- package/dist/lib/state/types.d.ts.map +1 -1
- package/dist/lib/state/utils.d.ts +7 -6
- package/dist/lib/state/utils.d.ts.map +1 -1
- package/dist/lib/state/utils.js +17 -0
- package/dist/lib/state/utils.js.map +1 -1
- 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/config.ts +1 -1
- package/lib/message-ids.ts +172 -0
- package/lib/state/persistence.ts +20 -24
- package/lib/state/state.ts +10 -0
- package/lib/state/types.ts +4 -0
- package/lib/state/utils.ts +30 -6
- package/package.json +12 -5
- package/dist/lib/prompts/internal-overlays.d.ts +0 -5
- package/dist/lib/prompts/internal-overlays.d.ts.map +0 -1
- package/dist/lib/prompts/internal-overlays.js +0 -49
- package/dist/lib/prompts/internal-overlays.js.map +0 -1
- package/dist/lib/prompts/message-priority-guidance.d.ts +0 -2
- package/dist/lib/prompts/message-priority-guidance.d.ts.map +0 -1
- package/dist/lib/prompts/message-priority-guidance.js +0 -9
- 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
|
@@ -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, "&")
|
|
96
|
+
.replace(/"/g, """)
|
|
97
|
+
.replace(/</g, "<")
|
|
98
|
+
.replace(/>/g, ">")
|
|
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/lib/state/persistence.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/lib/state/state.ts
CHANGED
|
@@ -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
|
}
|
package/lib/state/types.ts
CHANGED
|
@@ -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[]
|
package/lib/state/utils.ts
CHANGED
|
@@ -20,12 +20,30 @@ export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
interface PersistedPruneMessagesState {
|
|
23
|
-
byMessageId
|
|
24
|
-
blocksById
|
|
25
|
-
activeBlockIds
|
|
26
|
-
activeByAnchorMessageId
|
|
27
|
-
nextBlockId
|
|
28
|
-
nextRunId
|
|
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.
|
|
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.
|
|
61
|
-
"@opentui/solid": "0.
|
|
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.
|
|
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 +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"}
|