openhermes 2.8.0 → 4.0.0
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/CONTEXT.md +18 -0
- package/ETHOS.md +15 -0
- package/README.md +135 -292
- package/bootstrap.mjs +174 -512
- package/harness/agents/openhermes.md +87 -0
- package/harness/codex/CONSTITUTION.md +70 -148
- package/harness/codex/ROUTING.md +126 -0
- package/harness/commands/oh-doctor.md +26 -0
- package/harness/instructions/CONVENTIONS.md +206 -206
- package/harness/instructions/RUNTIME.md +54 -31
- package/harness/skills/oh-builder/SKILL.md +98 -0
- package/harness/skills/oh-caveman/SKILL.md +33 -0
- package/harness/skills/oh-expert/SKILL.md +121 -0
- package/harness/skills/oh-freeze/SKILL.md +28 -0
- package/harness/skills/oh-gauntlet/SKILL.md +119 -0
- package/harness/skills/oh-grill/SKILL.md +77 -0
- package/harness/skills/oh-guard/SKILL.md +33 -0
- package/harness/skills/oh-handoff/SKILL.md +33 -0
- package/harness/skills/oh-health/SKILL.md +90 -0
- package/harness/skills/oh-init/SKILL.md +78 -0
- package/harness/skills/oh-investigate/SKILL.md +35 -0
- package/harness/skills/oh-issue/SKILL.md +36 -0
- package/harness/skills/oh-learn/SKILL.md +28 -0
- package/harness/skills/oh-manifest/SKILL.md +84 -0
- package/harness/skills/oh-plan-review/SKILL.md +128 -0
- package/harness/skills/oh-planner/SKILL.md +157 -0
- package/harness/skills/oh-prd/SKILL.md +35 -0
- package/harness/skills/oh-retro/SKILL.md +33 -0
- package/harness/skills/oh-review/SKILL.md +110 -0
- package/harness/skills/oh-security/SKILL.md +110 -0
- package/harness/skills/oh-ship/SKILL.md +39 -0
- package/harness/skills/oh-skill-craft/SKILL.md +107 -0
- package/harness/skills/oh-skills-link/SKILL.md +29 -0
- package/harness/skills/oh-skills-list/SKILL.md +31 -0
- package/harness/skills/oh-triage/SKILL.md +36 -0
- package/index.mjs +3 -60
- package/lib/harness-resolver.mjs +77 -0
- package/lib/logger.mjs +62 -0
- package/package.json +49 -53
- package/test/plugins-behavioral.test.mjs +64 -0
- package/test/plugins.test.mjs +62 -0
- package/autorecall.mjs +0 -237
- package/curator.mjs +0 -482
- package/harness/commands/build-fix.md +0 -60
- package/harness/commands/checkpoint.md +0 -68
- package/harness/commands/code-review.md +0 -71
- package/harness/commands/doctor.md +0 -42
- package/harness/commands/eval.md +0 -89
- package/harness/commands/go-build.md +0 -87
- package/harness/commands/go-review.md +0 -71
- package/harness/commands/harness-audit.md +0 -90
- package/harness/commands/learn.md +0 -37
- package/harness/commands/loop-start.md +0 -38
- package/harness/commands/loop-status.md +0 -30
- package/harness/commands/memory-search.md +0 -37
- package/harness/commands/model-route.md +0 -32
- package/harness/commands/ohc.md +0 -13
- package/harness/commands/orchestrate.md +0 -88
- package/harness/commands/plan.md +0 -53
- package/harness/commands/quality-gate.md +0 -35
- package/harness/commands/refactor-clean.md +0 -102
- package/harness/commands/rust-build.md +0 -78
- package/harness/commands/rust-review.md +0 -65
- package/harness/commands/security.md +0 -93
- package/harness/commands/setup-pm.md +0 -65
- package/harness/commands/skill-create.md +0 -99
- package/harness/commands/test-coverage.md +0 -80
- package/harness/commands/update-codemaps.md +0 -81
- package/harness/commands/update-docs.md +0 -67
- package/harness/commands/verify.md +0 -68
- package/harness/prompts/architect.txt +0 -189
- package/harness/prompts/build-cpp.md +0 -98
- package/harness/prompts/build-error-resolver.md +0 -44
- package/harness/prompts/build-go.md +0 -340
- package/harness/prompts/build-java.md +0 -140
- package/harness/prompts/build-kotlin.md +0 -137
- package/harness/prompts/build-rust.md +0 -108
- package/harness/prompts/code-reviewer.md +0 -40
- package/harness/prompts/doc-updater.md +0 -206
- package/harness/prompts/docs-lookup.md +0 -71
- package/harness/prompts/e2e-runner.txt +0 -317
- package/harness/prompts/explore.md +0 -42
- package/harness/prompts/harness-optimizer.md +0 -42
- package/harness/prompts/loop-operator.md +0 -53
- package/harness/prompts/planner.md +0 -37
- package/harness/prompts/refactor-cleaner.md +0 -256
- package/harness/prompts/review-cpp.md +0 -81
- package/harness/prompts/review-database.md +0 -261
- package/harness/prompts/review-go.md +0 -257
- package/harness/prompts/review-java.md +0 -113
- package/harness/prompts/review-kotlin.md +0 -143
- package/harness/prompts/review-python.md +0 -101
- package/harness/prompts/review-rust.md +0 -77
- package/harness/prompts/security-reviewer.md +0 -42
- package/harness/prompts/tdd-guide.md +0 -228
- package/harness/rules/audit.md +0 -84
- package/harness/rules/checkpointing.md +0 -75
- package/harness/rules/context-loading.md +0 -33
- package/harness/rules/credential-exposure.md +0 -0
- package/harness/rules/delegation.md +0 -80
- package/harness/rules/handoff.md +0 -267
- package/harness/rules/memory-management.md +0 -28
- package/harness/rules/precedence.md +0 -52
- package/harness/rules/promotion.md +0 -46
- package/harness/rules/ranking.md +0 -64
- package/harness/rules/retrieval.md +0 -94
- package/harness/rules/runtime-guards.md +0 -196
- package/harness/rules/self-heal.md +0 -79
- package/harness/rules/session-start.md +0 -34
- package/harness/rules/skills-management.md +0 -165
- package/harness/rules/state-drift.md +0 -192
- package/harness/rules/verification.md +0 -88
- package/harness/scripts/sync-commands.mjs +0 -259
- package/harness/skills/.bundled_manifest +0 -17
- package/harness/skills/.usage.json +0 -6
- package/harness/skills/api-design/SKILL.md +0 -523
- package/harness/skills/backend-patterns/SKILL.md +0 -598
- package/harness/skills/coding-standards/SKILL.md +0 -549
- package/harness/skills/e2e-testing/SKILL.md +0 -326
- package/harness/skills/frontend-patterns/SKILL.md +0 -642
- package/harness/skills/frontend-slides/SKILL.md +0 -184
- package/harness/skills/security-review/SKILL.md +0 -495
- package/harness/skills/strategic-compact/SKILL.md +0 -131
- package/harness/skills/tdd-workflow/SKILL.md +0 -463
- package/harness/skills/verification-loop/SKILL.md +0 -126
- package/lib/ambient-memory.mjs +0 -167
- package/lib/handoff.mjs +0 -171
- package/lib/hardening.mjs +0 -146
- package/lib/memory-tools-plugin.mjs +0 -368
- package/lib/ohc/block-sync.mjs +0 -69
- package/lib/ohc/compress/search.mjs +0 -152
- package/lib/ohc/compress/state.mjs +0 -76
- package/lib/ohc/config.mjs +0 -185
- package/lib/ohc/message-ids.mjs +0 -178
- package/lib/ohc/notify.mjs +0 -135
- package/lib/ohc/protected-patterns.mjs +0 -55
- package/lib/ohc/prune-apply.mjs +0 -134
- package/lib/ohc/pruner.mjs +0 -608
- package/lib/ohc/reaper.mjs +0 -70
- package/lib/ohc/state.mjs +0 -265
- package/lib/ohc/strategies/deduplication.mjs +0 -72
- package/lib/ohc/strategies/index.mjs +0 -2
- package/lib/ohc/strategies/purge-errors.mjs +0 -43
- package/lib/ohc/token-utils.mjs +0 -26
- package/lib/ohc/updater.mjs +0 -132
- package/lib/paths.mjs +0 -49
- package/lib/schema-validator.mjs +0 -79
- package/lib/search.mjs +0 -48
- package/schemas/audit.schema.json +0 -82
- package/schemas/backlog.schema.json +0 -63
- package/schemas/checkpoint.schema.json +0 -65
- package/schemas/constraint.schema.json +0 -62
- package/schemas/decision.schema.json +0 -63
- package/schemas/instinct.schema.json +0 -63
- package/schemas/loop-state.schema.json +0 -33
- package/schemas/mistake.schema.json +0 -64
- package/schemas/verification_receipt.schema.json +0 -88
- package/skill-builder.mjs +0 -88
package/lib/ohc/pruner.mjs
DELETED
|
@@ -1,608 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import { loadConfig } from "./config.mjs"
|
|
3
|
-
import { selectMessagesToReap, totalTokens, msgTokens } from "./reaper.mjs"
|
|
4
|
-
import {
|
|
5
|
-
loadOhcState, saveOhcState, createSessionState,
|
|
6
|
-
serializeState, deserializeState,
|
|
7
|
-
buildToolIdList, syncToolCache, countTurns,
|
|
8
|
-
} from "./state.mjs"
|
|
9
|
-
import { sendCompressNotification } from "./notify.mjs"
|
|
10
|
-
import { deduplicate, purgeErrors } from "./strategies/index.mjs"
|
|
11
|
-
import { applyPruneTools, filterCompressedBlocks, applyFullToolRemoval } from "./prune-apply.mjs"
|
|
12
|
-
import { assignMessageRefs, injectMessageIds, cleanupMessageRefs } from "./message-ids.mjs"
|
|
13
|
-
import { syncCompressionBlocks } from "./block-sync.mjs"
|
|
14
|
-
import { buildSearchContext, resolveBoundaryIds, resolveSelection, resolveAnchorMessageId, validateNonOverlapping } from "./compress/search.mjs"
|
|
15
|
-
import { allocateBlockId, allocateRunId, wrapBlockSummary, applyCompressionState } from "./compress/state.mjs"
|
|
16
|
-
|
|
17
|
-
const AUTOPRUNE_COOLDOWN = 10000
|
|
18
|
-
const stateCache = new Map()
|
|
19
|
-
|
|
20
|
-
function getOrCreateState(sessionId) {
|
|
21
|
-
if (!sessionId) return null
|
|
22
|
-
let s = stateCache.get(sessionId)
|
|
23
|
-
if (!s) {
|
|
24
|
-
const persisted = loadOhcState(sessionId)
|
|
25
|
-
s = deserializeState(persisted)
|
|
26
|
-
s.sessionId = sessionId
|
|
27
|
-
stateCache.set(sessionId, s)
|
|
28
|
-
}
|
|
29
|
-
return s
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function getSessionId(messages) {
|
|
33
|
-
if (!messages?.length) return null
|
|
34
|
-
for (const m of messages) {
|
|
35
|
-
if (m.info?.sessionID) return m.info.sessionID
|
|
36
|
-
}
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function buildNudge(pct, max) {
|
|
41
|
-
if (pct > 0.95) return `OHC: Context is nearly full \u2014 use \`compress\` now.`
|
|
42
|
-
if (pct > 0.85) return `OHC: Context usage is high. Run \`compress\` to free space.`
|
|
43
|
-
if (pct > 0.70) return `OHC: Context is growing. Consider \`compress\` to keep room.`
|
|
44
|
-
return null
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function summarizeRemoved(selected, summary) {
|
|
48
|
-
const n = selected.length
|
|
49
|
-
if (summary) return `[OHC: Compressed] ${summary} \u2014 ${n} message${n === 1 ? "" : "s"} removed`
|
|
50
|
-
return `[OHC: Auto-pruned] ${n} message${n === 1 ? "" : "s"} removed`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function createSummaryMessage(text) {
|
|
54
|
-
return { parts: [{ type: "text", text }], info: { role: "system" } }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function applyCompress(ctx, sessionId, summary, max, min, targetTokens) {
|
|
58
|
-
const ss = getOrCreateState(sessionId)
|
|
59
|
-
if (ss) {
|
|
60
|
-
ss.prunedIds.clear()
|
|
61
|
-
ss.summary = null
|
|
62
|
-
ss.anchorMessageId = null
|
|
63
|
-
ss._pruneCycleDone = false
|
|
64
|
-
ss.lastAutoPruneAt = null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const res = await ctx.client.session.messages({ path: { id: sessionId } })
|
|
68
|
-
const msgs = res?.data || res || []
|
|
69
|
-
if (!Array.isArray(msgs)) return { removed: 0, message: "no messages" }
|
|
70
|
-
|
|
71
|
-
const selected = selectMessagesToReap(msgs, max, min, "compress", targetTokens)
|
|
72
|
-
if (selected.length === 0) return { removed: 0, message: "already within target" }
|
|
73
|
-
|
|
74
|
-
if (ss) {
|
|
75
|
-
for (const r of selected) ss.prunedIds.add(r.id)
|
|
76
|
-
ss.summary = summarizeRemoved(selected, summary)
|
|
77
|
-
ss.anchorMessageId = selected[0].id
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const tokensRemoved = selected.reduce((s, r) => s + r.tokens, 0)
|
|
81
|
-
const beforeTotal = totalTokens(msgs)
|
|
82
|
-
const afterTotal = beforeTotal - tokensRemoved
|
|
83
|
-
if (ss) {
|
|
84
|
-
ss.blockCount++
|
|
85
|
-
ss.totalTokensSaved += tokensRemoved
|
|
86
|
-
ss.totalMessagesRemoved += selected.length
|
|
87
|
-
saveOhcState(sessionId, serializeState(ss))
|
|
88
|
-
}
|
|
89
|
-
return { removed: selected.length, afterTotal, tokensRemoved, beforeTotal, beforeCount: msgs.length, afterCount: msgs.length - selected.length }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function truncateText(s, n) {
|
|
93
|
-
if (!s || s.length <= n) return s || ""
|
|
94
|
-
return s.slice(0, n) + "..."
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function estimateSummaryTokens(messages) {
|
|
98
|
-
let total = 0
|
|
99
|
-
for (const m of messages) {
|
|
100
|
-
if (m.info?.role === "system" && Array.isArray(m.parts)) {
|
|
101
|
-
for (const p of m.parts) {
|
|
102
|
-
if (p.type === "text" && (p.text?.startsWith("[OHC: Compressed]") || p.text?.startsWith("[OHC: Auto-pruned]"))) {
|
|
103
|
-
total += Math.ceil(p.text.length / 4)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return total
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function executeRangeCompress(ctx, sessionId, callId, topic, content) {
|
|
112
|
-
const ss = getOrCreateState(sessionId)
|
|
113
|
-
if (!ss) throw new Error("No session state")
|
|
114
|
-
ss.prunedIds.clear()
|
|
115
|
-
ss.summary = null
|
|
116
|
-
ss.anchorMessageId = null
|
|
117
|
-
ss._pruneCycleDone = false
|
|
118
|
-
ss.lastAutoPruneAt = null
|
|
119
|
-
|
|
120
|
-
const res = await ctx.client.session.messages({ path: { id: sessionId } })
|
|
121
|
-
const rawMessages = res?.data || res || []
|
|
122
|
-
if (!Array.isArray(rawMessages) || rawMessages.length === 0) throw new Error("No messages")
|
|
123
|
-
|
|
124
|
-
const searchContext = buildSearchContext(ss, rawMessages)
|
|
125
|
-
|
|
126
|
-
const plans = content.map((entry, idx) => {
|
|
127
|
-
const { startReference, endReference } = resolveBoundaryIds(searchContext, ss, entry.startId.trim(), entry.endId.trim())
|
|
128
|
-
const selection = resolveSelection(searchContext, startReference, endReference)
|
|
129
|
-
const anchorMessageId = resolveAnchorMessageId(startReference)
|
|
130
|
-
return { index: idx, entry, selection, anchorMessageId }
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
validateNonOverlapping(plans)
|
|
134
|
-
|
|
135
|
-
const runId = allocateRunId(ss)
|
|
136
|
-
const notifications = []
|
|
137
|
-
let totalActualTokensRemoved = 0
|
|
138
|
-
const allMessageIds = []
|
|
139
|
-
|
|
140
|
-
for (const plan of plans) {
|
|
141
|
-
const blockId = allocateBlockId(ss)
|
|
142
|
-
const storedSummary = wrapBlockSummary(blockId, plan.entry.summary)
|
|
143
|
-
const summaryTokens = Math.ceil(storedSummary.length / 4)
|
|
144
|
-
|
|
145
|
-
const actualTokensRemoved = plan.selection.messageIds.reduce((sum, mid) => {
|
|
146
|
-
const msg = searchContext.rawMessagesById.get(mid)
|
|
147
|
-
return msg ? sum + msgTokens(msg) : sum
|
|
148
|
-
}, 0)
|
|
149
|
-
totalActualTokensRemoved += actualTokensRemoved
|
|
150
|
-
allMessageIds.push(...plan.selection.messageIds)
|
|
151
|
-
|
|
152
|
-
applyCompressionState(
|
|
153
|
-
ss,
|
|
154
|
-
{
|
|
155
|
-
topic,
|
|
156
|
-
batchTopic: topic,
|
|
157
|
-
startId: plan.entry.startId,
|
|
158
|
-
endId: plan.entry.endId,
|
|
159
|
-
mode: "range",
|
|
160
|
-
runId,
|
|
161
|
-
compressMessageId: plan.selection.messageIds[0],
|
|
162
|
-
compressCallId: callId,
|
|
163
|
-
summaryTokens,
|
|
164
|
-
compressedTokens: actualTokensRemoved,
|
|
165
|
-
},
|
|
166
|
-
plan.selection,
|
|
167
|
-
plan.anchorMessageId,
|
|
168
|
-
blockId,
|
|
169
|
-
storedSummary,
|
|
170
|
-
plan.selection.requiredBlockIds,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
ss.blockCount++
|
|
174
|
-
ss.totalTokensSaved += actualTokensRemoved
|
|
175
|
-
ss.totalMessagesRemoved += plan.selection.messageIds.length
|
|
176
|
-
|
|
177
|
-
notifications.push({
|
|
178
|
-
blockId,
|
|
179
|
-
runId,
|
|
180
|
-
summary: plan.entry.summary,
|
|
181
|
-
messageIds: plan.selection.messageIds,
|
|
182
|
-
})
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
saveOhcState(sessionId, serializeState(ss))
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
messageIds: allMessageIds,
|
|
189
|
-
compressedTokens: totalActualTokensRemoved,
|
|
190
|
-
summaryRef: content[0]?.summary || topic,
|
|
191
|
-
blockCount: plans.length,
|
|
192
|
-
afterCount: rawMessages.length - allMessageIds.length,
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export const OhcPlugin = async (ctx) => {
|
|
197
|
-
const config = loadConfig(ctx)
|
|
198
|
-
if (!config.enabled) return {}
|
|
199
|
-
|
|
200
|
-
const max = config.max
|
|
201
|
-
const min = config.min
|
|
202
|
-
if (max <= min + 10000) return {}
|
|
203
|
-
|
|
204
|
-
let systemInjected = false
|
|
205
|
-
|
|
206
|
-
function buildTimingStr(ss) {
|
|
207
|
-
if (!ss?.compressionTiming?.totalDurationMs) return ""
|
|
208
|
-
const last = ss.compressionTiming.lastDurationMs || 0
|
|
209
|
-
const total = ss.compressionTiming.totalDurationMs || 0
|
|
210
|
-
const lastSec = (last / 1000).toFixed(1)
|
|
211
|
-
const totalSec = (total / 1000).toFixed(1)
|
|
212
|
-
return ` (last: ${lastSec}s, total: ${totalSec}s)`
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function computeRoleBreakdown(messages) {
|
|
216
|
-
const roles = {}
|
|
217
|
-
for (const m of messages) {
|
|
218
|
-
const role = m.info?.role || "unknown"
|
|
219
|
-
if (!roles[role]) roles[role] = { count: 0, tokens: 0 }
|
|
220
|
-
roles[role].count++
|
|
221
|
-
if (Array.isArray(m.parts)) {
|
|
222
|
-
for (const p of m.parts) {
|
|
223
|
-
if (p.type === "text") roles[role].tokens += Math.ceil((p.text || "").length / 4)
|
|
224
|
-
else if (p.type === "tool") {
|
|
225
|
-
if (p.state?.input) roles[role].tokens += JSON.stringify(p.state.input).length / 4
|
|
226
|
-
if (p.state?.output)
|
|
227
|
-
roles[role].tokens += (typeof p.state.output === "string" ? p.state.output : JSON.stringify(p.state.output ?? "")).length / 4
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return roles
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function countIterationsSinceLastUser(messages) {
|
|
236
|
-
let lastUserIdx = -1
|
|
237
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
238
|
-
if (messages[i].info?.role === "user") {
|
|
239
|
-
lastUserIdx = i
|
|
240
|
-
break
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (lastUserIdx === -1) return 0
|
|
244
|
-
return messages.length - 1 - lastUserIdx
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return {
|
|
248
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
249
|
-
if (systemInjected || !output.system?.length) return
|
|
250
|
-
systemInjected = true
|
|
251
|
-
output.system[output.system.length - 1] += `\n\n## Context Management\nOpenHermes manages context compression automatically. Set \`compaction.auto: false\` in opencode.json to disable.\n\n### Compress Tool\nUse \`compress\` to free context space. Two modes:\n- **Summary mode**: \`{ summary }\` — compresses oldest messages first\n- **Range mode**: \`{ topic, content: [{startId, endId, summary}] }\` — targets specific conversation ranges\n - \`startId\` / \`endId\`: \`ohcNNNN\` (message ref) or \`bkNN\` (block ref)\n - Ranges must not overlap within one call`
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
255
|
-
if (!output?.messages?.length) return
|
|
256
|
-
|
|
257
|
-
const sessionId = getSessionId(output.messages)
|
|
258
|
-
if (!sessionId) return
|
|
259
|
-
|
|
260
|
-
const ss = getOrCreateState(sessionId)
|
|
261
|
-
if (!ss) return
|
|
262
|
-
|
|
263
|
-
syncToolCache(ss, output.messages)
|
|
264
|
-
buildToolIdList(ss, output.messages)
|
|
265
|
-
ss.currentTurn = countTurns(ss, output.messages)
|
|
266
|
-
|
|
267
|
-
assignMessageRefs(ss, output.messages)
|
|
268
|
-
injectMessageIds(ss, output.messages)
|
|
269
|
-
syncCompressionBlocks(ss, output.messages)
|
|
270
|
-
filterCompressedBlocks(ss, output.messages)
|
|
271
|
-
|
|
272
|
-
deduplicate(ss, config, output.messages)
|
|
273
|
-
purgeErrors(ss, config, output.messages)
|
|
274
|
-
applyPruneTools(ss, output.messages)
|
|
275
|
-
applyFullToolRemoval(ss, output.messages)
|
|
276
|
-
|
|
277
|
-
const now = Date.now()
|
|
278
|
-
const recentlyPruned = ss.lastAutoPruneAt && (now - ss.lastAutoPruneAt) < AUTOPRUNE_COOLDOWN
|
|
279
|
-
|
|
280
|
-
if (ss.prunedIds.size > 0 && !recentlyPruned) {
|
|
281
|
-
const currentIds = new Set(output.messages.map(m => m.info?.id).filter(Boolean))
|
|
282
|
-
if ([...ss.prunedIds].every(id => !currentIds.has(id))) {
|
|
283
|
-
cleanupMessageRefs(ss, ss.prunedIds)
|
|
284
|
-
ss.prunedIds.clear()
|
|
285
|
-
ss.summary = null
|
|
286
|
-
ss.anchorMessageId = null
|
|
287
|
-
ss._pruneCycleDone = false
|
|
288
|
-
saveOhcState(sessionId, serializeState(ss))
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const currentTotal = totalTokens(output.messages)
|
|
293
|
-
if (currentTotal > max && !recentlyPruned && !ss._pruneCycleDone) {
|
|
294
|
-
const selected = selectMessagesToReap(output.messages, max, min)
|
|
295
|
-
if (selected.length > 0) {
|
|
296
|
-
for (const r of selected) ss.prunedIds.add(r.id)
|
|
297
|
-
if (!ss.summary) ss.summary = summarizeRemoved(selected, null)
|
|
298
|
-
if (!ss.anchorMessageId) ss.anchorMessageId = selected[0].id
|
|
299
|
-
const tokensRemoved = selected.reduce((s, r) => s + r.tokens, 0)
|
|
300
|
-
ss.blockCount++
|
|
301
|
-
ss.totalTokensSaved += tokensRemoved
|
|
302
|
-
ss.totalMessagesRemoved += selected.length
|
|
303
|
-
saveOhcState(sessionId, serializeState(ss))
|
|
304
|
-
ss.lastAutoPruneAt = now
|
|
305
|
-
ss._pruneCycleDone = true
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (ss._pruneCycleDone && ss.lastAutoPruneAt && (now - ss.lastAutoPruneAt) > AUTOPRUNE_COOLDOWN) {
|
|
310
|
-
ss._pruneCycleDone = false
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (ss.prunedIds.size > 0) {
|
|
314
|
-
const prunedIds = ss.prunedIds
|
|
315
|
-
const summary = ss.summary
|
|
316
|
-
const anchorId = ss.anchorMessageId
|
|
317
|
-
const result = []
|
|
318
|
-
let injected = false
|
|
319
|
-
|
|
320
|
-
for (const msg of output.messages) {
|
|
321
|
-
const msgId = msg.info?.id
|
|
322
|
-
|
|
323
|
-
if (anchorId && msgId === anchorId && !injected && summary) {
|
|
324
|
-
result.push(createSummaryMessage(summary))
|
|
325
|
-
injected = true
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (msgId !== undefined && prunedIds.has(msgId)) continue
|
|
329
|
-
|
|
330
|
-
result.push(msg)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (!injected && summary && result.length > 1) {
|
|
334
|
-
result.splice(1, 0, createSummaryMessage(summary))
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
output.messages.length = 0
|
|
338
|
-
output.messages.push(...result)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const afterTotal = totalTokens(output.messages)
|
|
342
|
-
|
|
343
|
-
const summaryBufferTotal = config.compress?.summaryBuffer
|
|
344
|
-
? estimateSummaryTokens(output.messages)
|
|
345
|
-
: 0
|
|
346
|
-
const effectiveMax = max + summaryBufferTotal
|
|
347
|
-
const pct = afterTotal / effectiveMax
|
|
348
|
-
const nudge = buildNudge(pct, effectiveMax)
|
|
349
|
-
|
|
350
|
-
let nudgeText = null
|
|
351
|
-
if (nudge && pct > ss.lastNudgePct + 0.05) {
|
|
352
|
-
nudgeText = nudge
|
|
353
|
-
ss.lastNudgePct = pct
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const iterationThreshold = config.compress?.iterationNudgeThreshold ?? 15
|
|
357
|
-
const iterCount = countIterationsSinceLastUser(output.messages)
|
|
358
|
-
if (iterCount >= iterationThreshold && iterCount % 5 === 0) {
|
|
359
|
-
const iterNudge = `OHC: ${iterCount} AI turns since your last input. Summarize with \`compress\`.`
|
|
360
|
-
if (nudgeText) nudgeText += "\n\n" + iterNudge
|
|
361
|
-
else nudgeText = iterNudge
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const blockRefs = [...(ss.prune?.messages?.activeBlockIds || [])]
|
|
365
|
-
.filter(id => Number.isInteger(id) && id > 0)
|
|
366
|
-
.sort((a, b) => a - b)
|
|
367
|
-
.map(id => `bk${id}`)
|
|
368
|
-
let blockGuidance = null
|
|
369
|
-
if (blockRefs.length > 0) {
|
|
370
|
-
blockGuidance = `<ohc-reminder>\nActive compressed blocks: ${blockRefs.join(", ")}\nUse \`bkNN\` IDs as boundaries when compressing ranges that include previously compressed blocks.\n</ohc-reminder>`
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (nudgeText || blockGuidance) {
|
|
374
|
-
const appendText = [nudgeText, blockGuidance].filter(Boolean).join("\n\n")
|
|
375
|
-
for (let i = output.messages.length - 1; i >= 0; i--) {
|
|
376
|
-
const m = output.messages[i]
|
|
377
|
-
if (m.info?.role === "assistant" && m.parts?.length) {
|
|
378
|
-
const textPart = m.parts.find(p => p.type === "text")
|
|
379
|
-
if (textPart) { textPart.text += "\n\n" + appendText; break }
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
event: async (input) => {
|
|
386
|
-
if (input.event?.type !== "message.part.updated") return
|
|
387
|
-
const part = input.event.properties?.part
|
|
388
|
-
if (part?.type !== "tool" || part.tool !== "compress") return
|
|
389
|
-
|
|
390
|
-
const sessionId = input.event.properties?.sessionID
|
|
391
|
-
if (!sessionId) return
|
|
392
|
-
const ss = getOrCreateState(sessionId)
|
|
393
|
-
if (!ss) return
|
|
394
|
-
|
|
395
|
-
if (part.state?.status === "pending") {
|
|
396
|
-
if (typeof part.callID !== "string") return
|
|
397
|
-
ss.compressionTiming.starts.set(part.callID, Date.now())
|
|
398
|
-
return
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (part.state?.status === "completed") {
|
|
402
|
-
if (typeof part.callID !== "string") return
|
|
403
|
-
const start = ss.compressionTiming.starts.get(part.callID)
|
|
404
|
-
if (!start) return
|
|
405
|
-
ss.compressionTiming.starts.delete(part.callID)
|
|
406
|
-
const durationMs = Date.now() - start
|
|
407
|
-
ss.compressionTiming.lastDurationMs = durationMs
|
|
408
|
-
ss.compressionTiming.totalDurationMs = (ss.compressionTiming.totalDurationMs || 0) + durationMs
|
|
409
|
-
saveOhcState(sessionId, serializeState(ss))
|
|
410
|
-
}
|
|
411
|
-
},
|
|
412
|
-
|
|
413
|
-
"command.execute.before": async (input, output) => {
|
|
414
|
-
if (input.command !== "ohc") return
|
|
415
|
-
const sub = (input.arguments || "").trim().toLowerCase()
|
|
416
|
-
const args = (input.arguments || "").trim()
|
|
417
|
-
|
|
418
|
-
if (sub === "status") {
|
|
419
|
-
let msgs = [], t = 0
|
|
420
|
-
try {
|
|
421
|
-
if (ctx?.client?.session?.messages) {
|
|
422
|
-
const res = await ctx.client.session.messages({ path: { id: input.sessionID } })
|
|
423
|
-
msgs = res?.data || res || []
|
|
424
|
-
t = totalTokens(msgs)
|
|
425
|
-
}
|
|
426
|
-
} catch {}
|
|
427
|
-
const ss = getOrCreateState(input.sessionID)
|
|
428
|
-
const prunedCount = ss?.prunedIds.size || 0
|
|
429
|
-
const strategyPruned = ss?.prune?.tools?.size || 0
|
|
430
|
-
const timing = buildTimingStr(ss)
|
|
431
|
-
const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
|
|
432
|
-
const blockLine = activeBlockIds.length ? ` bk${activeBlockIds.join(", bk")}.` : ""
|
|
433
|
-
const summaryBufferTotal = config.compress?.summaryBuffer
|
|
434
|
-
? estimateSummaryTokens(msgs)
|
|
435
|
-
: 0
|
|
436
|
-
const effectiveMax = max + summaryBufferTotal
|
|
437
|
-
const text = `OHC: ${msgs.length} visible (${prunedCount} auto, ${strategyPruned} strategy) \u00b7 ~${Math.round(t / 1000)}K tok (${Math.round((t / effectiveMax) * 100)}%)${blockLine}${timing}`
|
|
438
|
-
await ctx.client.session.prompt({
|
|
439
|
-
path: { id: input.sessionID },
|
|
440
|
-
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
441
|
-
})
|
|
442
|
-
throw new Error("__OHC_STATUS_HANDLED__")
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (sub === "stats") {
|
|
446
|
-
const ss = getOrCreateState(input.sessionID)
|
|
447
|
-
const totalSaved = ss?.totalTokensSaved || 0
|
|
448
|
-
const blocks = ss?.blockCount || 0
|
|
449
|
-
const dedupPruned = ss?.prune?.tools?.size || 0
|
|
450
|
-
const autoPruned = ss?.prunedIds?.size || 0
|
|
451
|
-
const timing = buildTimingStr(ss)
|
|
452
|
-
const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
|
|
453
|
-
const blockLine = activeBlockIds.length ? ` \u00b7 bk${activeBlockIds.join(", bk")}` : ""
|
|
454
|
-
const text = `OHC: ${blocks} blocks${blockLine} \u00b7 ~${Math.round(totalSaved / 1000)}K saved \u00b7 auto: ${autoPruned}, strategy: ${dedupPruned}${timing}`
|
|
455
|
-
await ctx.client.session.prompt({
|
|
456
|
-
path: { id: input.sessionID },
|
|
457
|
-
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
458
|
-
})
|
|
459
|
-
throw new Error("__OHC_STATS_HANDLED__")
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (sub === "manual") {
|
|
463
|
-
const onOff = args.replace(/^manual\s*/i, "").trim().toLowerCase()
|
|
464
|
-
if (onOff === "on" || onOff === "1" || onOff === "true") {
|
|
465
|
-
const ss = getOrCreateState(input.sessionID)
|
|
466
|
-
if (ss) ss.manualMode = "active"
|
|
467
|
-
await ctx.client.session.prompt({
|
|
468
|
-
path: { id: input.sessionID },
|
|
469
|
-
body: { noReply: true, parts: [{ type: "text", text: "OHC Manual: active \u2014 agent will not autonomously compress", ignored: true }] },
|
|
470
|
-
})
|
|
471
|
-
} else {
|
|
472
|
-
const ss = getOrCreateState(input.sessionID)
|
|
473
|
-
if (ss) ss.manualMode = false
|
|
474
|
-
await ctx.client.session.prompt({
|
|
475
|
-
path: { id: input.sessionID },
|
|
476
|
-
body: { noReply: true, parts: [{ type: "text", text: "OHC Manual: off \u2014 agent can compress autonomously", ignored: true }] },
|
|
477
|
-
})
|
|
478
|
-
}
|
|
479
|
-
throw new Error("__OHC_MANUAL_HANDLED__")
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (sub.startsWith("compress")) {
|
|
483
|
-
const rest = args.replace(/^compress\s*/i, "").trim()
|
|
484
|
-
const numMatch = rest.match(/^(\d+)\s*(.*)/)
|
|
485
|
-
let targetTokens
|
|
486
|
-
let focus
|
|
487
|
-
if (numMatch) {
|
|
488
|
-
targetTokens = parseInt(numMatch[1], 10)
|
|
489
|
-
focus = numMatch[2].trim() || "Manual compression"
|
|
490
|
-
} else {
|
|
491
|
-
focus = rest || "Manual compression"
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
const result = await applyCompress(ctx, input.sessionID, focus, max, min, targetTokens)
|
|
495
|
-
const cmdSs = getOrCreateState(input.sessionID)
|
|
496
|
-
await sendCompressNotification(ctx.client, input.sessionID, config, result.removed, focus, result.tokensRemoved, cmdSs, result.afterCount)
|
|
497
|
-
output.parts.length = 0
|
|
498
|
-
output.parts.push({
|
|
499
|
-
type: "text",
|
|
500
|
-
text: `OHC: Compressed ${result.removed} msgs. Summary: ${focus}`,
|
|
501
|
-
})
|
|
502
|
-
} catch {
|
|
503
|
-
output.parts.length = 0
|
|
504
|
-
output.parts.push({ type: "text", text: `/ohc compress ${focus}` })
|
|
505
|
-
}
|
|
506
|
-
return
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
if (sub === "context") {
|
|
510
|
-
let msgs = [], t = 0
|
|
511
|
-
try {
|
|
512
|
-
if (ctx?.client?.session?.messages) {
|
|
513
|
-
const res = await ctx.client.session.messages({ path: { id: input.sessionID } })
|
|
514
|
-
msgs = res?.data || res || []
|
|
515
|
-
t = totalTokens(msgs)
|
|
516
|
-
}
|
|
517
|
-
} catch {}
|
|
518
|
-
const ss = getOrCreateState(input.sessionID)
|
|
519
|
-
const autoPruned = ss?.prunedIds?.size || 0
|
|
520
|
-
const stratPruned = ss?.prune?.tools?.size || 0
|
|
521
|
-
const totalSaved = ss?.totalTokensSaved || 0
|
|
522
|
-
const blocks = ss?.blockCount || 0
|
|
523
|
-
const visibleTokens = t
|
|
524
|
-
const totalTokensWithPruned = visibleTokens + totalSaved
|
|
525
|
-
|
|
526
|
-
const roles = computeRoleBreakdown(msgs)
|
|
527
|
-
const roleLines = []
|
|
528
|
-
for (const [role, info] of Object.entries(roles)) {
|
|
529
|
-
const pct = t > 0 ? Math.round((info.tokens / t) * 100) : 0
|
|
530
|
-
roleLines.push(`${role}: ${info.count} msgs, ~${Math.round(info.tokens / 1000)}K (${pct}%)`)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
|
|
534
|
-
const blockLine = activeBlockIds.length ? ` \u00b7 bk${activeBlockIds.join(", bk")}` : ""
|
|
535
|
-
const text = `OHC: ${msgs.length} visible (${autoPruned + stratPruned} pruned) \u00b7 ~${Math.round(visibleTokens / 1000)}K/~${Math.round(totalTokensWithPruned / 1000)}K \u00b7 ${blocks} blocks, ~${Math.round(totalSaved / 1000)}K saved${blockLine}\n\nRole breakdown:\n${roleLines.map(r => ` ${r}`).join("\n")}`
|
|
536
|
-
await ctx.client.session.prompt({
|
|
537
|
-
path: { id: input.sessionID },
|
|
538
|
-
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
539
|
-
})
|
|
540
|
-
throw new Error("__OHC_CONTEXT_HANDLED__")
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
if (sub === "sweep") {
|
|
544
|
-
const rest = args.replace(/^sweep\s*/i, "").trim()
|
|
545
|
-
let count = rest ? parseInt(rest, 10) : 10
|
|
546
|
-
if (isNaN(count) || count < 1) count = 10
|
|
547
|
-
const ss = getOrCreateState(input.sessionID)
|
|
548
|
-
const allToolIds = ss?.toolIdList || []
|
|
549
|
-
const unpruned = allToolIds.filter(id => !ss.prune.tools.has(id))
|
|
550
|
-
const toSweep = unpruned.slice(-count)
|
|
551
|
-
let sweptCount = 0
|
|
552
|
-
for (const id of toSweep) {
|
|
553
|
-
const entry = ss.toolParameters.get(id)
|
|
554
|
-
if (entry) {
|
|
555
|
-
ss.prune.tools.set(id, entry.tokenCount || 0)
|
|
556
|
-
sweptCount++
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
applyPruneTools(ss, output.messages)
|
|
560
|
-
const text = `OHC Sweep: ${sweptCount} tool call${sweptCount === 1 ? "" : "s"} pruned`
|
|
561
|
-
output.parts.length = 0
|
|
562
|
-
output.parts.push({ type: "text", text })
|
|
563
|
-
return
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const text = "OHC: /ohc status | stats | context | sweep [n] | manual [on|off] | compress [focus]"
|
|
567
|
-
await ctx.client.session.prompt({
|
|
568
|
-
path: { id: input.sessionID },
|
|
569
|
-
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
570
|
-
})
|
|
571
|
-
throw new Error("__OHC_HELP_HANDLED__")
|
|
572
|
-
},
|
|
573
|
-
|
|
574
|
-
tool: {
|
|
575
|
-
compress: tool({
|
|
576
|
-
description: "Compress conversation content to free context space. Two modes: range mode (specify content array with startId/endId/summary per entry) and summary mode (specify summary with optional targetTokens). Use range for precise targeting; fall back to summary for general oldest-first pruning. In range mode, each startId/endId pair uses message or block references found in conversation. Each entry's summary replaces the entire range. Provide a technical summary of what was removed, including file paths, function signatures, decisions, and constraints.",
|
|
577
|
-
args: {
|
|
578
|
-
topic: tool.schema.string().optional().describe("Range mode: Short label (3-5 words) for the overall batch — e.g. 'Auth System Exploration'"),
|
|
579
|
-
content: tool.schema.array(tool.schema.object({
|
|
580
|
-
startId: tool.schema.string().describe("Boundary at range start: ohcNNNN (message) or bkNN (block)"),
|
|
581
|
-
endId: tool.schema.string().describe("Boundary at range end: ohcNNNN (message) or bkNN (block)"),
|
|
582
|
-
summary: tool.schema.string().describe("Complete technical summary replacing all content in this range. Include user intent, decisions, constraints, file paths, and function signatures."),
|
|
583
|
-
})).optional().describe("Range mode: One or more non-overlapping ranges to compress"),
|
|
584
|
-
summary: tool.schema.string().optional().describe("Legacy mode: Technical summary of compressed content. Use when not specifying content array."),
|
|
585
|
-
targetTokens: tool.schema.number().optional().describe("Legacy mode: Estimated target after compression. Lower = more aggressive. Default uses soft config floor."),
|
|
586
|
-
},
|
|
587
|
-
async execute(args, toolCtx) {
|
|
588
|
-
const callId = toolCtx.callID || null
|
|
589
|
-
const sessionId = toolCtx.sessionID
|
|
590
|
-
|
|
591
|
-
if (Array.isArray(args.content) && args.content.length > 0) {
|
|
592
|
-
const result = await executeRangeCompress(ctx, sessionId, callId, args.topic || "Compression", args.content)
|
|
593
|
-
toolCtx.metadata({ title: "Compress Range" })
|
|
594
|
-
const resultSs = getOrCreateState(sessionId)
|
|
595
|
-
await sendCompressNotification(ctx.client, sessionId, config, result.messageIds.length, result.summaryRef, result.compressedTokens, resultSs, result.afterCount || 0)
|
|
596
|
-
return `OHC: Compressed ${result.messageIds.length} msgs across ${args.content.length} range(s). Summary: "${truncateText(result.summaryRef, 200)}"`
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const result = await applyCompress(ctx, sessionId, args.summary, max, min, args.targetTokens)
|
|
600
|
-
toolCtx.metadata({ title: "Compress" })
|
|
601
|
-
const toolSs = getOrCreateState(sessionId)
|
|
602
|
-
await sendCompressNotification(ctx.client, sessionId, config, result.removed, truncateText(args.summary, 200), result.tokensRemoved, toolSs, result.afterCount)
|
|
603
|
-
return `OHC: Compressed ${result.removed} msgs. Summary: "${truncateText(args.summary, 200)}"`
|
|
604
|
-
},
|
|
605
|
-
}),
|
|
606
|
-
},
|
|
607
|
-
}
|
|
608
|
-
}
|
package/lib/ohc/reaper.mjs
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
function partTokens(part) {
|
|
2
|
-
if (part.type === "text") return Math.ceil((part.text || "").length / 4)
|
|
3
|
-
if (part.type === "tool") {
|
|
4
|
-
let t = 0
|
|
5
|
-
if (part.state?.input) t += JSON.stringify(part.state.input).length / 4
|
|
6
|
-
if (part.state?.output)
|
|
7
|
-
t += (typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output ?? "")).length / 4
|
|
8
|
-
return Math.ceil(t)
|
|
9
|
-
}
|
|
10
|
-
try { return Math.ceil(JSON.stringify(part).length / 4) } catch { return 0 }
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function msgTokens(msg) {
|
|
14
|
-
return (Array.isArray(msg.parts) ? msg.parts : []).reduce((s, p) => s + partTokens(p), 0)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function totalTokens(messages) {
|
|
18
|
-
return (Array.isArray(messages) ? messages : []).reduce((s, m) => s + msgTokens(m), 0)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Select oldest messages to remove.
|
|
23
|
-
* Returns array of { id, msg, tokens } without mutating the input.
|
|
24
|
-
* Never removes index 0 (system prompt) or the last message (latest turn).
|
|
25
|
-
*
|
|
26
|
-
* mode "auto": remove just enough to bring total under maxLimit
|
|
27
|
-
* mode "compress": remove everything down to floor (deep prune)
|
|
28
|
-
*
|
|
29
|
-
* targetOverride: when set, replaces floor/minFloor — agent explicit request
|
|
30
|
-
* overrides the soft config defaults. Used when user asks "compress to X".
|
|
31
|
-
*
|
|
32
|
-
* turnProtectionTags: optional set of message IDs to protect from pruning.
|
|
33
|
-
* These are recent tool call message IDs that should be kept.
|
|
34
|
-
*/
|
|
35
|
-
export function selectMessagesToReap(messages, maxLimit, minFloor, mode = "auto", targetOverride, turnProtectionTags) {
|
|
36
|
-
if (!messages?.length || messages.length < 3) return []
|
|
37
|
-
|
|
38
|
-
const protectSet = turnProtectionTags?.length ? new Set(turnProtectionTags) : null
|
|
39
|
-
|
|
40
|
-
let total = totalTokens(messages)
|
|
41
|
-
const selected = []
|
|
42
|
-
|
|
43
|
-
if (mode === "compress") {
|
|
44
|
-
const floor = targetOverride ?? minFloor
|
|
45
|
-
const tokenCache = messages.map((msg, i) => ({ idx: i, tokens: msgTokens(msg) }))
|
|
46
|
-
let i = 1
|
|
47
|
-
while (i < messages.length - 1) {
|
|
48
|
-
if (protectSet?.has(String(messages[i].info?.id ?? i))) { i++; continue }
|
|
49
|
-
const t = tokenCache[i].tokens
|
|
50
|
-
if (total - t < floor) break
|
|
51
|
-
total -= t
|
|
52
|
-
selected.push({ id: String(messages[i].info?.id ?? i), msg: messages[i], tokens: t })
|
|
53
|
-
i++
|
|
54
|
-
}
|
|
55
|
-
} else {
|
|
56
|
-
const floor = targetOverride ?? minFloor
|
|
57
|
-
const tokenCache = messages.map((msg, i) => ({ idx: i, tokens: msgTokens(msg) }))
|
|
58
|
-
let i = 1
|
|
59
|
-
while (i < messages.length - 1 && total > maxLimit) {
|
|
60
|
-
if (protectSet?.has(String(messages[i].info?.id ?? i))) { i++; continue }
|
|
61
|
-
const t = tokenCache[i].tokens
|
|
62
|
-
if (total - t < floor) break
|
|
63
|
-
total -= t
|
|
64
|
-
selected.push({ id: String(messages[i].info?.id ?? i), msg: messages[i], tokens: t })
|
|
65
|
-
i++
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return selected
|
|
70
|
-
}
|