openhermes 2.6.0 → 2.6.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/README.md CHANGED
@@ -259,17 +259,6 @@ openhermes/
259
259
 
260
260
  ---
261
261
 
262
- ## Environment Variables
263
-
264
- Two knobs. That's it.
265
-
266
- | Variable | Default | Effect |
267
- |----------|---------|--------|
268
- | `OPENCODE_ALLOW_PROJECT_HARNESS` | `false` | Enable project-local harness at `.opencode/openhermes/` |
269
- | `OPENCODE_CURATOR_LOGS` | `false` | Pipe curator diagnostics to stderr for debugging |
270
-
271
- ---
272
-
273
262
  ## Why OpenHermes ≠ Hermes Agent
274
263
 
275
264
  Same messenger emoji. Entirely different mediums.
@@ -33,7 +33,7 @@ export function applyCompressionState(state, input, selection, anchorMessageId,
33
33
  endId: input.endId,
34
34
  summary: storedSummary,
35
35
  summaryTokens: input.summaryTokens || 0,
36
- compressedTokens: 0,
36
+ compressedTokens: input.compressedTokens || 0,
37
37
  consumedBlockIds: Array.isArray(consumedBlockIds) ? consumedBlockIds : [],
38
38
  deactivatedByBlockId: undefined,
39
39
  deactivatedByUser: false,
@@ -3,11 +3,11 @@ function formatTokenCount(tokens) {
3
3
  return String(tokens)
4
4
  }
5
5
 
6
- function buildProgressBar(prunedCount, visibleCount, width) {
6
+ function buildProgressBar(totalMessagesRemoved, currentMessageCount, width) {
7
7
  width = width || 30
8
- const total = prunedCount + visibleCount
8
+ const total = totalMessagesRemoved + currentMessageCount
9
9
  if (total === 0) return `\u2502${"\u2591".repeat(width)}\u2502 0% active`
10
- const activeRatio = visibleCount / total
10
+ const activeRatio = currentMessageCount / total
11
11
  const activeW = Math.round(activeRatio * width)
12
12
  const prunedW = width - activeW
13
13
  const bar = "\u2588".repeat(Math.min(activeW, width)) + "\u2591".repeat(Math.min(prunedW, width))
@@ -19,11 +19,11 @@ function buildMinimal(count, tokensRemoved, savedTotal, blockCount) {
19
19
  return `\u25A3 OHC | ~${formatTokenCount(savedTotal)} saved total \u2014 ${label}`
20
20
  }
21
21
 
22
- function buildDetailed(count, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount, summary) {
22
+ function buildDetailed(count, tokensRemoved, savedTotal, blockCount, totalMessagesRemoved, currentMessageCount, summary) {
23
23
  const label = "Compression"
24
24
  let msg = `\u25A3 OHC | ~${formatTokenCount(savedTotal)} saved total`
25
- if (prunedCount + visibleCount > 0) {
26
- msg += `\n\n${buildProgressBar(prunedCount, visibleCount)}`
25
+ if (totalMessagesRemoved + currentMessageCount > 0) {
26
+ msg += `\n\n${buildProgressBar(totalMessagesRemoved, currentMessageCount)}`
27
27
  }
28
28
  msg += `\n\n\u25A3 ${label} #${blockCount}`
29
29
  msg += `\n\u2192 ${count} message${count === 1 ? "" : "s"} removed`
@@ -35,9 +35,13 @@ function buildStrategyNotification(strategy, count, detail) {
35
35
  return `\u25A3 OHC | ${strategy}: ${count} pruned${detail ? ` (${detail})` : ""}`
36
36
  }
37
37
 
38
- export async function sendCompressNotification(client, sessionId, config, count, summary, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount) {
38
+ export async function sendCompressNotification(client, sessionId, config, count, summary, tokensRemoved, ss, currentMessageCount) {
39
39
  if (count === 0) return false
40
40
 
41
+ const savedTotal = ss?.totalTokensSaved || 0
42
+ const blockCount = ss?.blockCount || 0
43
+ const totalMessagesRemoved = ss?.totalMessagesRemoved || 0
44
+
41
45
  const notifType = config.notification ?? "toast"
42
46
  const notifMode = config.notificationMode ?? "minimal"
43
47
 
@@ -46,7 +50,7 @@ export async function sendCompressNotification(client, sessionId, config, count,
46
50
  if (notifType === "toast") {
47
51
  const message = notifMode === "minimal"
48
52
  ? buildMinimal(count, tokensRemoved, savedTotal, blockCount)
49
- : buildDetailed(count, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount, summary)
53
+ : buildDetailed(count, tokensRemoved, savedTotal, blockCount, totalMessagesRemoved, currentMessageCount, summary)
50
54
  try {
51
55
  await client.tui.showToast({
52
56
  body: {
@@ -65,7 +69,7 @@ export async function sendCompressNotification(client, sessionId, config, count,
65
69
  path: { id: sessionId },
66
70
  body: {
67
71
  noReply: true,
68
- parts: [{ type: "text", text: buildDetailed(count, tokensRemoved, savedTotal, blockCount, prunedCount, visibleCount, summary), ignored: true }],
72
+ parts: [{ type: "text", text: buildDetailed(count, tokensRemoved, savedTotal, blockCount, totalMessagesRemoved, currentMessageCount, summary), ignored: true }],
69
73
  },
70
74
  })
71
75
  } catch {}
@@ -1,6 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import { loadConfig } from "./config.mjs"
3
- import { selectMessagesToReap, totalTokens } from "./reaper.mjs"
3
+ import { selectMessagesToReap, totalTokens, msgTokens } from "./reaper.mjs"
4
4
  import {
5
5
  loadOhcState, saveOhcState, createSessionState,
6
6
  serializeState, deserializeState,
@@ -75,7 +75,6 @@ async function applyCompress(ctx, sessionId, summary, max, min, targetTokens) {
75
75
  for (const r of selected) ss.prunedIds.add(r.id)
76
76
  ss.summary = summarizeRemoved(selected, summary)
77
77
  ss.anchorMessageId = selected[0].id
78
- saveOhcState(sessionId, serializeState(ss))
79
78
  }
80
79
 
81
80
  const tokensRemoved = selected.reduce((s, r) => s + r.tokens, 0)
@@ -84,6 +83,8 @@ async function applyCompress(ctx, sessionId, summary, max, min, targetTokens) {
84
83
  if (ss) {
85
84
  ss.blockCount++
86
85
  ss.totalTokensSaved += tokensRemoved
86
+ ss.totalMessagesRemoved += selected.length
87
+ saveOhcState(sessionId, serializeState(ss))
87
88
  }
88
89
  return { removed: selected.length, afterTotal, tokensRemoved, beforeTotal, beforeCount: msgs.length, afterCount: msgs.length - selected.length }
89
90
  }
@@ -133,12 +134,21 @@ async function executeRangeCompress(ctx, sessionId, callId, topic, content) {
133
134
 
134
135
  const runId = allocateRunId(ss)
135
136
  const notifications = []
137
+ let totalActualTokensRemoved = 0
138
+ const allMessageIds = []
136
139
 
137
140
  for (const plan of plans) {
138
141
  const blockId = allocateBlockId(ss)
139
142
  const storedSummary = wrapBlockSummary(blockId, plan.entry.summary)
140
143
  const summaryTokens = Math.ceil(storedSummary.length / 4)
141
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
+
142
152
  applyCompressionState(
143
153
  ss,
144
154
  {
@@ -151,6 +161,7 @@ async function executeRangeCompress(ctx, sessionId, callId, topic, content) {
151
161
  compressMessageId: plan.selection.messageIds[0],
152
162
  compressCallId: callId,
153
163
  summaryTokens,
164
+ compressedTokens: actualTokensRemoved,
154
165
  },
155
166
  plan.selection,
156
167
  plan.anchorMessageId,
@@ -160,7 +171,8 @@ async function executeRangeCompress(ctx, sessionId, callId, topic, content) {
160
171
  )
161
172
 
162
173
  ss.blockCount++
163
- ss.totalTokensSaved += summaryTokens
174
+ ss.totalTokensSaved += actualTokensRemoved
175
+ ss.totalMessagesRemoved += plan.selection.messageIds.length
164
176
 
165
177
  notifications.push({
166
178
  blockId,
@@ -170,11 +182,14 @@ async function executeRangeCompress(ctx, sessionId, callId, topic, content) {
170
182
  })
171
183
  }
172
184
 
185
+ saveOhcState(sessionId, serializeState(ss))
186
+
173
187
  return {
174
- messageIds: plans.flatMap(p => p.selection.messageIds),
175
- compressedTokens: 0,
188
+ messageIds: allMessageIds,
189
+ compressedTokens: totalActualTokensRemoved,
176
190
  summaryRef: content[0]?.summary || topic,
177
191
  blockCount: plans.length,
192
+ afterCount: rawMessages.length - allMessageIds.length,
178
193
  }
179
194
  }
180
195
 
@@ -283,10 +298,11 @@ export const OhcPlugin = async (ctx) => {
283
298
  for (const r of selected) ss.prunedIds.add(r.id)
284
299
  if (!ss.summary) ss.summary = summarizeRemoved(selected, null)
285
300
  if (!ss.anchorMessageId) ss.anchorMessageId = selected[0].id
286
- saveOhcState(sessionId, serializeState(ss))
287
301
  const tokensRemoved = selected.reduce((s, r) => s + r.tokens, 0)
288
302
  ss.blockCount++
289
303
  ss.totalTokensSaved += tokensRemoved
304
+ ss.totalMessagesRemoved += selected.length
305
+ saveOhcState(sessionId, serializeState(ss))
290
306
  ss.lastAutoPruneAt = now
291
307
  ss._pruneCycleDone = true
292
308
  }
@@ -416,7 +432,11 @@ export const OhcPlugin = async (ctx) => {
416
432
  const timing = buildTimingStr(ss)
417
433
  const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
418
434
  const blockLine = activeBlockIds.length ? ` Blocks: bk${activeBlockIds.join(", bk")}.` : ""
419
- const text = `[OHC Status] ${msgs.length} messages visible (${prunedCount} auto-pruned, ${strategyPruned} strategy-pruned)${blockLine}${timing}. ~${Math.round(t / 1000)}K / ${max.toLocaleString()} tokens (${Math.round((t / max) * 100)}%). Soft floor: ${min.toLocaleString()}.`
435
+ const summaryBufferTotal = config.compress?.summaryBuffer
436
+ ? estimateSummaryTokens(msgs)
437
+ : 0
438
+ const effectiveMax = max + summaryBufferTotal
439
+ const text = `[OHC Status] ${msgs.length} messages visible (${prunedCount} auto-pruned, ${strategyPruned} strategy-pruned)${blockLine}${timing}. ~${Math.round(t / 1000)}K / ${effectiveMax.toLocaleString()} tokens (${Math.round((t / effectiveMax) * 100)}%). Soft floor: ${min.toLocaleString()}.`
420
440
  await ctx.client.session.prompt({
421
441
  path: { id: input.sessionID },
422
442
  body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
@@ -475,7 +495,7 @@ export const OhcPlugin = async (ctx) => {
475
495
  try {
476
496
  const result = await applyCompress(ctx, input.sessionID, focus, max, min, targetTokens)
477
497
  const cmdSs = getOrCreateState(input.sessionID)
478
- await sendCompressNotification(ctx.client, input.sessionID, config, result.removed, focus, result.tokensRemoved, cmdSs?.totalTokensSaved || 0, cmdSs?.blockCount || 0, result.removed, result.afterCount)
498
+ await sendCompressNotification(ctx.client, input.sessionID, config, result.removed, focus, result.tokensRemoved, cmdSs, result.afterCount)
479
499
  output.parts.length = 0
480
500
  output.parts.push({
481
501
  type: "text",
@@ -574,14 +594,14 @@ export const OhcPlugin = async (ctx) => {
574
594
  const result = await executeRangeCompress(ctx, sessionId, callId, args.topic || "Compression", args.content)
575
595
  toolCtx.metadata({ title: "Compress Range" })
576
596
  const resultSs = getOrCreateState(sessionId)
577
- await sendCompressNotification(ctx.client, sessionId, config, result.messageIds.length, result.summaryRef, result.compressedTokens, resultSs?.totalTokensSaved || 0, resultSs?.blockCount || 0, result.messageIds.length, 0)
597
+ await sendCompressNotification(ctx.client, sessionId, config, result.messageIds.length, result.summaryRef, result.compressedTokens, resultSs, result.afterCount || 0)
578
598
  return `Compressed ${result.messageIds.length} messages across ${args.content.length} range(s). Summary: "${truncateText(result.summaryRef, 200)}"`
579
599
  }
580
600
 
581
601
  const result = await applyCompress(ctx, sessionId, args.summary, max, min, args.targetTokens)
582
602
  toolCtx.metadata({ title: "Compress" })
583
603
  const toolSs = getOrCreateState(sessionId)
584
- await sendCompressNotification(ctx.client, sessionId, config, result.removed, truncateText(args.summary, 200), result.tokensRemoved, toolSs?.totalTokensSaved || 0, toolSs?.blockCount || 0, result.removed, result.afterCount)
604
+ await sendCompressNotification(ctx.client, sessionId, config, result.removed, truncateText(args.summary, 200), result.tokensRemoved, toolSs, result.afterCount)
585
605
  return `Compressed: ${result.removed} messages removed. Summary: "${truncateText(args.summary, 200)}"`
586
606
  },
587
607
  }),
@@ -10,7 +10,7 @@ function partTokens(part) {
10
10
  return Math.ceil(JSON.stringify(part).length / 4)
11
11
  }
12
12
 
13
- function msgTokens(msg) {
13
+ export function msgTokens(msg) {
14
14
  return (Array.isArray(msg.parts) ? msg.parts : []).reduce((s, p) => s + partTokens(p), 0)
15
15
  }
16
16
 
package/lib/ohc/state.mjs CHANGED
@@ -89,6 +89,7 @@ export function createSessionState() {
89
89
  summary: null,
90
90
  anchorMessageId: null,
91
91
  totalTokensSaved: 0,
92
+ totalMessagesRemoved: 0,
92
93
  blockCount: 0,
93
94
  }
94
95
  }
@@ -136,6 +137,7 @@ export function serializeState(state) {
136
137
  },
137
138
  lastAutoPruneAt: state.lastAutoPruneAt,
138
139
  totalTokensSaved: state.totalTokensSaved,
140
+ totalMessagesRemoved: state.totalMessagesRemoved,
139
141
  blockCount: state.blockCount,
140
142
  summary: state.summary,
141
143
  anchorMessageId: state.anchorMessageId,
@@ -180,6 +182,7 @@ export function deserializeState(saved) {
180
182
  }
181
183
  state.lastAutoPruneAt = saved.lastAutoPruneAt || null
182
184
  state.totalTokensSaved = saved.totalTokensSaved || 0
185
+ state.totalMessagesRemoved = saved.totalMessagesRemoved || 0
183
186
  state.blockCount = saved.blockCount || 0
184
187
  state.summary = saved.summary || null
185
188
  state.anchorMessageId = saved.anchorMessageId || null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openhermes",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "OpenHermes plugin suite for OpenCode — autonomous checkpointing, native memory tools, subagent routing, slash commands, and skill-candidate detection.",
5
5
  "type": "module",
6
6
  "license": "MIT",