@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.1
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/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8129 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +27 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +68 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +64 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress/usage.d.ts +45 -0
- package/dist/lib/compress/usage.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.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/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +47 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +105 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +75 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/lib/v2/usage.d.ts +44 -0
- package/dist/lib/v2/usage.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +180 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +494 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +320 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +70 -0
- package/lib/compress/usage.ts +168 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +321 -0
- package/lib/state/state.ts +214 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +117 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +261 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +448 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +60 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/lib/v2/usage.ts +423 -0
- package/package.json +91 -5
- package/server.js +1 -0
- package/tui.tsx +7 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { WithParts } from "../state"
|
|
2
|
+
import { formatBlockRef, formatMessageRef, parseBoundaryId, type IdFormat } from "../message-ids"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Marker embedded in the hidden summary prompt so the plugin can recognize its
|
|
6
|
+
* own transient (`session.generate`) request and strip everything else from it.
|
|
7
|
+
* Requests carry a per-call suffix (`[[DCP-RC-SUMMARY:<callId>]]`) so captured
|
|
8
|
+
* provider usage can be attributed to a specific request.
|
|
9
|
+
*/
|
|
10
|
+
export const RC_SUMMARY_MARKER = "[[DCP-RC-SUMMARY]]"
|
|
11
|
+
/** Prefix shared by the static marker and its per-call variant. */
|
|
12
|
+
export const RC_SUMMARY_MARKER_PREFIX = "[[DCP-RC-SUMMARY"
|
|
13
|
+
|
|
14
|
+
function sanitizeCallId(callId: string): string {
|
|
15
|
+
return callId.replace(/[^A-Za-z0-9_-]/g, "-")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Marker line for a hidden summary request, optionally tagged with a call id. */
|
|
19
|
+
export function buildSummaryMarker(callId?: string): string {
|
|
20
|
+
if (!callId) {
|
|
21
|
+
return RC_SUMMARY_MARKER
|
|
22
|
+
}
|
|
23
|
+
return `${RC_SUMMARY_MARKER_PREFIX}:${sanitizeCallId(callId)}]]`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when the text contains a hidden summary marker, with or without a call id. */
|
|
27
|
+
export function hasSummaryMarker(text: string): boolean {
|
|
28
|
+
return text.includes(RC_SUMMARY_MARKER_PREFIX)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Extract the call id from a hidden summary marker, when present. */
|
|
32
|
+
export function extractSummaryCallId(text: string): string | undefined {
|
|
33
|
+
return text.match(/\[\[DCP-RC-SUMMARY:([A-Za-z0-9_-]+)\]\]/)?.[1]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PriorSummary {
|
|
37
|
+
/** Canonical block reference, e.g. `b2` or `@b2@`. */
|
|
38
|
+
ref: string
|
|
39
|
+
/** Summary body with the block header/footer stripped. */
|
|
40
|
+
body: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SummarySegment {
|
|
44
|
+
/** Canonical selector, e.g. `m0004`, `m0004-m0008`, or `b2-m0011`. */
|
|
45
|
+
selector: string
|
|
46
|
+
messages: WithParts[]
|
|
47
|
+
/** Summaries of active blocks consumed by this selection. */
|
|
48
|
+
priorSummaries?: PriorSummary[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SummarySection {
|
|
52
|
+
selector: string
|
|
53
|
+
summary: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ParsedSummarySections {
|
|
57
|
+
sections: SummarySection[]
|
|
58
|
+
missing: string[]
|
|
59
|
+
duplicates: string[]
|
|
60
|
+
empty: string[]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stringify(value: unknown): string {
|
|
64
|
+
if (typeof value === "string") {
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.stringify(value, null, 2)
|
|
69
|
+
} catch {
|
|
70
|
+
return String(value)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readToolState(part: any): {
|
|
75
|
+
status?: string
|
|
76
|
+
input?: unknown
|
|
77
|
+
output?: unknown
|
|
78
|
+
error?: unknown
|
|
79
|
+
} {
|
|
80
|
+
const state = part?.state
|
|
81
|
+
if (!state || typeof state !== "object") {
|
|
82
|
+
return {}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
status: typeof state.status === "string" ? state.status : undefined,
|
|
86
|
+
input: state.input,
|
|
87
|
+
output: state.output,
|
|
88
|
+
error: state.error,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Serialize one conversation message into plain text for the hidden summary
|
|
94
|
+
* request. Binary/file payloads are intentionally omitted.
|
|
95
|
+
*/
|
|
96
|
+
export function serializeMessageForSummary(message: WithParts, selector: string): string {
|
|
97
|
+
const role = message.info?.role ?? "unknown"
|
|
98
|
+
const lines: string[] = []
|
|
99
|
+
const parts = Array.isArray(message.parts) ? message.parts : []
|
|
100
|
+
|
|
101
|
+
for (const part of parts as any[]) {
|
|
102
|
+
if (!part || typeof part !== "object") {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
switch (part.type) {
|
|
106
|
+
case "text":
|
|
107
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
108
|
+
lines.push(part.text)
|
|
109
|
+
}
|
|
110
|
+
break
|
|
111
|
+
case "reasoning":
|
|
112
|
+
if (typeof part.text === "string" && part.text.length > 0) {
|
|
113
|
+
lines.push(`[reasoning] ${part.text}`)
|
|
114
|
+
}
|
|
115
|
+
break
|
|
116
|
+
case "tool": {
|
|
117
|
+
const state = readToolState(part)
|
|
118
|
+
const name = part.tool ?? "tool"
|
|
119
|
+
lines.push(`[tool_call ${name}] ${stringify(state.input)}`)
|
|
120
|
+
if (state.status === "completed") {
|
|
121
|
+
lines.push(`[tool_result ${name}] ${stringify(state.output)}`)
|
|
122
|
+
} else if (state.status === "error") {
|
|
123
|
+
lines.push(`[tool_error ${name}] ${stringify(state.error)}`)
|
|
124
|
+
} else if (state.status) {
|
|
125
|
+
lines.push(`[tool_${state.status} ${name}]`)
|
|
126
|
+
}
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
case "file":
|
|
130
|
+
lines.push(`[file ${part.mime ?? ""} ${part.filename ?? part.url ?? ""}]`.trim())
|
|
131
|
+
break
|
|
132
|
+
case "patch":
|
|
133
|
+
lines.push(`[patch] ${stringify(part.hash ?? part.files ?? part)}`)
|
|
134
|
+
break
|
|
135
|
+
case "agent":
|
|
136
|
+
lines.push(`[agent ${part.name ?? ""}]`.trim())
|
|
137
|
+
break
|
|
138
|
+
default:
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const body = lines.join("\n").trim()
|
|
144
|
+
return `### ${selector} (${role})\n${body.length > 0 ? body : "(empty message)"}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const SUMMARY_RULES = `RULES
|
|
148
|
+
- Reply with one section per selector, in the same order as listed. Nothing else: no introduction, no code fences, no tool calls, no trailing prose.
|
|
149
|
+
- Each section starts with a header line exactly \`SLEEV-SUMMARY <selector>\` using the selector as listed.
|
|
150
|
+
- The summary body continues until the next header or the end of the reply.
|
|
151
|
+
- When a segment lists previous compressed summaries, merge their information into your summary: those blocks are replaced by the section you produce.
|
|
152
|
+
- Preserve every high-signal detail: user intent (quote short user messages exactly), decisions, constraints, file paths, function and symbol names, commands run, key findings, errors, verification results, and any subagent task IDs with their scope and completion state.
|
|
153
|
+
- Drop noise: failed attempts that led nowhere, repeated tool output, exploration that produced no durable result.
|
|
154
|
+
- Be dense and precise. Prefer terse technical wording over narrative.`
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Build the hidden summarization prompt. The selected segments are embedded
|
|
158
|
+
* verbatim so the transient request does not depend on any injected metadata.
|
|
159
|
+
*/
|
|
160
|
+
export function buildSummaryPrompt(
|
|
161
|
+
selectors: SummarySegment[],
|
|
162
|
+
idFormat: IdFormat = "xml",
|
|
163
|
+
callId?: string,
|
|
164
|
+
): string {
|
|
165
|
+
const selectorList = selectors.map((segment) => segment.selector)
|
|
166
|
+
const example = selectorList[0] ?? (idFormat === "compact" ? "" : "m0004")
|
|
167
|
+
const body = selectors
|
|
168
|
+
.map((segment) => {
|
|
169
|
+
const messageBody = segment.messages
|
|
170
|
+
.map((message) => serializeMessageForSummary(message, segment.selector))
|
|
171
|
+
.join("\n\n")
|
|
172
|
+
const priorBody = (segment.priorSummaries ?? [])
|
|
173
|
+
.map(
|
|
174
|
+
(prior) =>
|
|
175
|
+
`### ${prior.ref} (previous compressed summary)\n${prior.body.trim()}`,
|
|
176
|
+
)
|
|
177
|
+
.join("\n\n")
|
|
178
|
+
return [messageBody, priorBody].filter((part) => part.length > 0).join("\n\n")
|
|
179
|
+
})
|
|
180
|
+
.join("\n\n")
|
|
181
|
+
|
|
182
|
+
return `${buildSummaryMarker(callId)}
|
|
183
|
+
|
|
184
|
+
You generate compressed-context entries for a coding session. Summarize ONLY the conversation segments listed below. Do not summarize anything else and do not ask questions.
|
|
185
|
+
|
|
186
|
+
SUMMARY FORMAT
|
|
187
|
+
SLEEV-SUMMARY ${example}
|
|
188
|
+
<summary body>
|
|
189
|
+
|
|
190
|
+
${SUMMARY_RULES.replace("one section per selector", `${selectorList.length} section(s), one per selector`)}
|
|
191
|
+
- The selectors for this request are exactly: ${selectorList.map((selector) => `\`${selector}\``).join(", ")}
|
|
192
|
+
RC-SELECTORS: ${selectorList.join(", ")}
|
|
193
|
+
|
|
194
|
+
SEGMENTS
|
|
195
|
+
${body}
|
|
196
|
+
`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function selectorPattern(): string {
|
|
200
|
+
const boundary = "(?:m[0-9]{4}|@[0-9]+@|b[1-9][0-9]*|@b[1-9][0-9]*@)"
|
|
201
|
+
return `(?:${boundary})(?:-${boundary})?`
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Normalize a selector written in either ID format (`m0004` or ``) into the
|
|
206
|
+
* session's canonical format. Returns null when the selector is malformed.
|
|
207
|
+
*/
|
|
208
|
+
export function canonicalizeSelector(selector: string, format: IdFormat = "xml"): string | null {
|
|
209
|
+
const parts = selector.trim().toLowerCase().split("-")
|
|
210
|
+
if (parts.length < 1 || parts.length > 2) {
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
const parse = (part: string) => parseBoundaryId(part, "xml") ?? parseBoundaryId(part, "compact")
|
|
214
|
+
const toRef = (parsed: NonNullable<ReturnType<typeof parse>>): string =>
|
|
215
|
+
parsed.kind === "message"
|
|
216
|
+
? formatMessageRef(parsed.index, format)
|
|
217
|
+
: formatBlockRef(parsed.blockId, format)
|
|
218
|
+
|
|
219
|
+
const start = parse(parts[0]!)
|
|
220
|
+
const end = parts.length === 2 ? parse(parts[1]!) : start
|
|
221
|
+
if (!start || !end) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
if (start.kind === "message" && end.kind === "message" && start.index > end.index) {
|
|
225
|
+
return null
|
|
226
|
+
}
|
|
227
|
+
const startRef = toRef(start)
|
|
228
|
+
const endRef = toRef(end)
|
|
229
|
+
return startRef === endRef ? startRef : `${startRef}-${endRef}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Strip the `[Compressed conversation section]` header and trailing block tag
|
|
234
|
+
* from a stored summary so it can be embedded in a new summary prompt.
|
|
235
|
+
*/
|
|
236
|
+
export function restoreSummary(summary: string): string {
|
|
237
|
+
const headerMatch = summary.match(
|
|
238
|
+
/^[ \t]*\[Compressed conversation(?: section)?(?: b[0-9]+)?\]/i,
|
|
239
|
+
)
|
|
240
|
+
if (!headerMatch) {
|
|
241
|
+
return summary
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const body = summary.slice(headerMatch[0].length).trimStart()
|
|
245
|
+
const lines = body.split("\n")
|
|
246
|
+
while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "") {
|
|
247
|
+
lines.pop()
|
|
248
|
+
}
|
|
249
|
+
const last = (lines[lines.length - 1] ?? "").trim()
|
|
250
|
+
const isXmlTag = new RegExp("^<dcp-message-id>b[0-9]+</dcp-message-id>$").test(last)
|
|
251
|
+
const isCompactTag = /^@b[1-9][0-9]*@$/.test(last)
|
|
252
|
+
if (isXmlTag || isCompactTag) {
|
|
253
|
+
lines.pop()
|
|
254
|
+
}
|
|
255
|
+
return lines.join("\n").trimEnd()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Parse `SLEEV-SUMMARY <selector>` sections from a model reply. Returns the
|
|
260
|
+
* accepted sections plus the selectors that are missing, duplicated, or have
|
|
261
|
+
* an empty body so the caller can retry only what is needed. Selectors are
|
|
262
|
+
* accepted in either ID format and normalized to the session format.
|
|
263
|
+
*/
|
|
264
|
+
export function parseSummarySections(
|
|
265
|
+
text: string,
|
|
266
|
+
expected: string[],
|
|
267
|
+
idFormat: IdFormat = "xml",
|
|
268
|
+
): ParsedSummarySections {
|
|
269
|
+
const header = new RegExp(`^SLEEV-SUMMARY (${selectorPattern()})[ \t]*$`, "gmi")
|
|
270
|
+
const matches: Array<{ selector: string; start: number; end: number }> = []
|
|
271
|
+
let match: RegExpExecArray | null
|
|
272
|
+
|
|
273
|
+
while ((match = header.exec(text)) !== null) {
|
|
274
|
+
const selector = canonicalizeSelector((match[1] ?? "").trim(), idFormat)
|
|
275
|
+
if (!selector) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
matches.push({
|
|
279
|
+
selector,
|
|
280
|
+
start: match.index,
|
|
281
|
+
end: match.index + match[0].length,
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const normalizedExpected = expected
|
|
286
|
+
.map((selector) => canonicalizeSelector(selector, idFormat))
|
|
287
|
+
.filter((selector): selector is string => Boolean(selector))
|
|
288
|
+
const expectedSet = new Set(normalizedExpected)
|
|
289
|
+
const sections: SummarySection[] = []
|
|
290
|
+
const seen = new Set<string>()
|
|
291
|
+
const duplicates: string[] = []
|
|
292
|
+
const empty: string[] = []
|
|
293
|
+
|
|
294
|
+
for (let index = 0; index < matches.length; index++) {
|
|
295
|
+
const current = matches[index]!
|
|
296
|
+
if (!expectedSet.has(current.selector)) {
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
if (seen.has(current.selector)) {
|
|
300
|
+
if (!duplicates.includes(current.selector)) {
|
|
301
|
+
duplicates.push(current.selector)
|
|
302
|
+
}
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
seen.add(current.selector)
|
|
306
|
+
|
|
307
|
+
const next = matches[index + 1]
|
|
308
|
+
const body = text.slice(current.end, next ? next.start : text.length).trim()
|
|
309
|
+
if (body.length === 0) {
|
|
310
|
+
empty.push(current.selector)
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
sections.push({ selector: current.selector, summary: body })
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const missing = normalizedExpected.filter(
|
|
317
|
+
(selector) => !seen.has(selector) && !empty.includes(selector),
|
|
318
|
+
)
|
|
319
|
+
return { sections, missing, duplicates, empty }
|
|
320
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { SessionState } from "../state/types"
|
|
2
|
+
import { attachCompressionDuration } from "./state"
|
|
3
|
+
|
|
4
|
+
export interface PendingCompressionDuration {
|
|
5
|
+
messageId: string
|
|
6
|
+
callId: string
|
|
7
|
+
durationMs: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CompressionTimingState {
|
|
11
|
+
startsByCallId: Map<string, number>
|
|
12
|
+
pendingByCallId: Map<string, PendingCompressionDuration>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildCompressionTimingKey(messageId: string, callId: string): string {
|
|
16
|
+
return `${messageId}:${callId}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function consumeCompressionStart(
|
|
20
|
+
state: SessionState,
|
|
21
|
+
messageId: string,
|
|
22
|
+
callId: string,
|
|
23
|
+
): number | undefined {
|
|
24
|
+
const key = buildCompressionTimingKey(messageId, callId)
|
|
25
|
+
const start = state.compressionTiming.startsByCallId.get(key)
|
|
26
|
+
state.compressionTiming.startsByCallId.delete(key)
|
|
27
|
+
return start
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveCompressionDuration(
|
|
31
|
+
startedAt: number | undefined,
|
|
32
|
+
eventTime: number | undefined,
|
|
33
|
+
partTime: { start?: unknown; end?: unknown } | undefined,
|
|
34
|
+
): number | undefined {
|
|
35
|
+
const runningAt =
|
|
36
|
+
typeof partTime?.start === "number" && Number.isFinite(partTime.start)
|
|
37
|
+
? partTime.start
|
|
38
|
+
: eventTime
|
|
39
|
+
const pendingToRunningMs =
|
|
40
|
+
typeof startedAt === "number" && typeof runningAt === "number"
|
|
41
|
+
? Math.max(0, runningAt - startedAt)
|
|
42
|
+
: undefined
|
|
43
|
+
|
|
44
|
+
const toolStart = partTime?.start
|
|
45
|
+
const toolEnd = partTime?.end
|
|
46
|
+
const runtimeMs =
|
|
47
|
+
typeof toolStart === "number" &&
|
|
48
|
+
Number.isFinite(toolStart) &&
|
|
49
|
+
typeof toolEnd === "number" &&
|
|
50
|
+
Number.isFinite(toolEnd)
|
|
51
|
+
? Math.max(0, toolEnd - toolStart)
|
|
52
|
+
: undefined
|
|
53
|
+
|
|
54
|
+
return typeof pendingToRunningMs === "number" ? pendingToRunningMs : runtimeMs
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function applyPendingCompressionDurations(state: SessionState): number {
|
|
58
|
+
if (state.compressionTiming.pendingByCallId.size === 0) {
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let updates = 0
|
|
63
|
+
for (const [key, entry] of state.compressionTiming.pendingByCallId) {
|
|
64
|
+
const applied = attachCompressionDuration(
|
|
65
|
+
state.prune.messages,
|
|
66
|
+
entry.messageId,
|
|
67
|
+
entry.callId,
|
|
68
|
+
entry.durationMs,
|
|
69
|
+
)
|
|
70
|
+
if (applied > 0) {
|
|
71
|
+
updates += applied
|
|
72
|
+
state.compressionTiming.pendingByCallId.delete(key)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return updates
|
|
77
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
import type { CompressionUsage } from "./usage"
|
|
6
|
+
|
|
7
|
+
/** Result of the hidden transient model call (OpenCode V2 `session.generate`). */
|
|
8
|
+
export interface GenerateSummaryResult {
|
|
9
|
+
text: string
|
|
10
|
+
/** Provider usage when captured, otherwise a local estimate. */
|
|
11
|
+
usage?: CompressionUsage
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ToolContext {
|
|
15
|
+
client: any
|
|
16
|
+
state: SessionState
|
|
17
|
+
logger: Logger
|
|
18
|
+
config: PluginConfig
|
|
19
|
+
prompts: PromptStore
|
|
20
|
+
/** Hidden transient model call (OpenCode V2 `session.generate`). */
|
|
21
|
+
generate: (input: {
|
|
22
|
+
sessionID: string
|
|
23
|
+
prompt: string
|
|
24
|
+
/** Per-request id embedded in the prompt marker for usage attribution. */
|
|
25
|
+
callId?: string
|
|
26
|
+
}) => Promise<GenerateSummaryResult>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BoundaryReference {
|
|
30
|
+
kind: "message" | "compressed-block"
|
|
31
|
+
rawIndex: number
|
|
32
|
+
messageId?: string
|
|
33
|
+
blockId?: number
|
|
34
|
+
anchorMessageId?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SearchContext {
|
|
38
|
+
rawMessages: WithParts[]
|
|
39
|
+
rawMessagesById: Map<string, WithParts>
|
|
40
|
+
rawIndexById: Map<string, number>
|
|
41
|
+
summaryByBlockId: Map<number, CompressionBlock>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SelectionResolution {
|
|
45
|
+
startReference: BoundaryReference
|
|
46
|
+
endReference: BoundaryReference
|
|
47
|
+
messageIds: string[]
|
|
48
|
+
messageTokenById: Map<string, number>
|
|
49
|
+
toolIds: string[]
|
|
50
|
+
requiredBlockIds: number[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AppliedCompressionResult {
|
|
54
|
+
compressedTokens: number
|
|
55
|
+
messageIds: string[]
|
|
56
|
+
newlyCompressedMessageIds: string[]
|
|
57
|
+
newlyCompressedToolIds: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CompressionStateInput {
|
|
61
|
+
topic: string
|
|
62
|
+
batchTopic: string
|
|
63
|
+
startId: string
|
|
64
|
+
endId: string
|
|
65
|
+
mode: CompressionMode
|
|
66
|
+
runId: number
|
|
67
|
+
compressMessageId: string
|
|
68
|
+
compressCallId?: string
|
|
69
|
+
summaryTokens: number
|
|
70
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage accounting for the hidden compression (summary) request.
|
|
3
|
+
*
|
|
4
|
+
* Provider-reported usage is preferred: OpenCode V2 generates the summary
|
|
5
|
+
* through `session.generate`, which does not surface token usage to the
|
|
6
|
+
* caller, so the numbers are captured by wrapping the resolved language
|
|
7
|
+
* model (`ctx.aisdk.hook("language")`) and reading the `finish` usage.
|
|
8
|
+
* When that path is unavailable, local tokenizer estimates are recorded
|
|
9
|
+
* instead so the plugin can still report an approximate cost.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type CompressionUsageSource = "provider" | "estimated"
|
|
13
|
+
|
|
14
|
+
/** Usage of a single hidden compression request. */
|
|
15
|
+
export interface CompressionUsage {
|
|
16
|
+
inputTokens: number
|
|
17
|
+
outputTokens: number
|
|
18
|
+
cacheReadTokens: number
|
|
19
|
+
cacheWriteTokens: number
|
|
20
|
+
reasoningTokens: number
|
|
21
|
+
source: CompressionUsageSource
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Aggregated usage of all hidden compression requests in a session. */
|
|
25
|
+
export interface CompressionUsageTotals {
|
|
26
|
+
calls: number
|
|
27
|
+
providerCalls: number
|
|
28
|
+
estimatedCalls: number
|
|
29
|
+
inputTokens: number
|
|
30
|
+
outputTokens: number
|
|
31
|
+
cacheReadTokens: number
|
|
32
|
+
cacheWriteTokens: number
|
|
33
|
+
reasoningTokens: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function emptyCompressionUsage(): CompressionUsageTotals {
|
|
37
|
+
return {
|
|
38
|
+
calls: 0,
|
|
39
|
+
providerCalls: 0,
|
|
40
|
+
estimatedCalls: 0,
|
|
41
|
+
inputTokens: 0,
|
|
42
|
+
outputTokens: 0,
|
|
43
|
+
cacheReadTokens: 0,
|
|
44
|
+
cacheWriteTokens: 0,
|
|
45
|
+
reasoningTokens: 0,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toCount(value: unknown, fallback = 0): number {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
51
|
+
return fallback
|
|
52
|
+
}
|
|
53
|
+
return Math.round(value)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize AI SDK v3 usage (`{ inputTokens: { total, noCache, cacheRead,
|
|
58
|
+
* cacheWrite }, outputTokens: { total, text, reasoning } }`) into the plugin
|
|
59
|
+
* shape. Returns undefined when there is nothing usable to record.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeProviderUsage(raw: unknown): CompressionUsage | undefined {
|
|
62
|
+
if (!raw || typeof raw !== "object") {
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const value = raw as { inputTokens?: unknown; outputTokens?: unknown }
|
|
67
|
+
const input =
|
|
68
|
+
value.inputTokens && typeof value.inputTokens === "object"
|
|
69
|
+
? (value.inputTokens as Record<string, unknown>)
|
|
70
|
+
: {}
|
|
71
|
+
const output =
|
|
72
|
+
value.outputTokens && typeof value.outputTokens === "object"
|
|
73
|
+
? (value.outputTokens as Record<string, unknown>)
|
|
74
|
+
: {}
|
|
75
|
+
|
|
76
|
+
const cacheRead = toCount(input.cacheRead)
|
|
77
|
+
const cacheWrite = toCount(input.cacheWrite)
|
|
78
|
+
const uncached = toCount(input.noCache)
|
|
79
|
+
const inputFromParts = uncached + cacheRead + cacheWrite
|
|
80
|
+
const inputTotal = Math.max(toCount(input.total, inputFromParts), inputFromParts)
|
|
81
|
+
|
|
82
|
+
const reasoning = toCount(output.reasoning)
|
|
83
|
+
const outputTotal = Math.max(toCount(output.total, toCount(output.text) + reasoning), reasoning)
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
inputTotal === 0 &&
|
|
87
|
+
outputTotal === 0 &&
|
|
88
|
+
cacheRead === 0 &&
|
|
89
|
+
cacheWrite === 0 &&
|
|
90
|
+
reasoning === 0
|
|
91
|
+
) {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
inputTokens: inputTotal,
|
|
97
|
+
outputTokens: outputTotal,
|
|
98
|
+
cacheReadTokens: cacheRead,
|
|
99
|
+
cacheWriteTokens: cacheWrite,
|
|
100
|
+
reasoningTokens: reasoning,
|
|
101
|
+
source: "provider",
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Local fallback when the provider did not report usage. */
|
|
106
|
+
export function estimatedCompressionUsage(
|
|
107
|
+
inputTokens: number,
|
|
108
|
+
outputTokens: number,
|
|
109
|
+
): CompressionUsage {
|
|
110
|
+
return {
|
|
111
|
+
inputTokens: toCount(inputTokens),
|
|
112
|
+
outputTokens: toCount(outputTokens),
|
|
113
|
+
cacheReadTokens: 0,
|
|
114
|
+
cacheWriteTokens: 0,
|
|
115
|
+
reasoningTokens: 0,
|
|
116
|
+
source: "estimated",
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function addCompressionUsage(
|
|
121
|
+
totals: CompressionUsageTotals,
|
|
122
|
+
usage: CompressionUsage,
|
|
123
|
+
): CompressionUsageTotals {
|
|
124
|
+
return {
|
|
125
|
+
calls: totals.calls + 1,
|
|
126
|
+
providerCalls: totals.providerCalls + (usage.source === "provider" ? 1 : 0),
|
|
127
|
+
estimatedCalls: totals.estimatedCalls + (usage.source === "estimated" ? 1 : 0),
|
|
128
|
+
inputTokens: totals.inputTokens + usage.inputTokens,
|
|
129
|
+
outputTokens: totals.outputTokens + usage.outputTokens,
|
|
130
|
+
cacheReadTokens: totals.cacheReadTokens + usage.cacheReadTokens,
|
|
131
|
+
cacheWriteTokens: totals.cacheWriteTokens + usage.cacheWriteTokens,
|
|
132
|
+
reasoningTokens: totals.reasoningTokens + usage.reasoningTokens,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function addCompressionUsageTotals(
|
|
137
|
+
totals: CompressionUsageTotals,
|
|
138
|
+
other: CompressionUsageTotals,
|
|
139
|
+
): CompressionUsageTotals {
|
|
140
|
+
return {
|
|
141
|
+
calls: totals.calls + other.calls,
|
|
142
|
+
providerCalls: totals.providerCalls + other.providerCalls,
|
|
143
|
+
estimatedCalls: totals.estimatedCalls + other.estimatedCalls,
|
|
144
|
+
inputTokens: totals.inputTokens + other.inputTokens,
|
|
145
|
+
outputTokens: totals.outputTokens + other.outputTokens,
|
|
146
|
+
cacheReadTokens: totals.cacheReadTokens + other.cacheReadTokens,
|
|
147
|
+
cacheWriteTokens: totals.cacheWriteTokens + other.cacheWriteTokens,
|
|
148
|
+
reasoningTokens: totals.reasoningTokens + other.reasoningTokens,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Backward-compatible load of persisted totals. */
|
|
153
|
+
export function normalizeCompressionUsageTotals(value: unknown): CompressionUsageTotals {
|
|
154
|
+
if (!value || typeof value !== "object") {
|
|
155
|
+
return emptyCompressionUsage()
|
|
156
|
+
}
|
|
157
|
+
const raw = value as Record<string, unknown>
|
|
158
|
+
return {
|
|
159
|
+
calls: toCount(raw.calls),
|
|
160
|
+
providerCalls: toCount(raw.providerCalls),
|
|
161
|
+
estimatedCalls: toCount(raw.estimatedCalls),
|
|
162
|
+
inputTokens: toCount(raw.inputTokens),
|
|
163
|
+
outputTokens: toCount(raw.outputTokens),
|
|
164
|
+
cacheReadTokens: toCount(raw.cacheReadTokens),
|
|
165
|
+
cacheWriteTokens: toCount(raw.cacheWriteTokens),
|
|
166
|
+
reasoningTokens: toCount(raw.reasoningTokens),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PluginConfig } from "./config"
|
|
2
|
+
import type { SessionState } from "./state"
|
|
3
|
+
|
|
4
|
+
export const compressPermission = (
|
|
5
|
+
state: SessionState,
|
|
6
|
+
config: PluginConfig,
|
|
7
|
+
): "ask" | "allow" | "deny" => {
|
|
8
|
+
return state.compressPermission ?? config.compress.permission
|
|
9
|
+
}
|