openhermes 1.13.1 → 2.5.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/README.md +125 -206
- package/autorecall.mjs +79 -12
- package/bootstrap.mjs +122 -25
- package/curator.mjs +4 -40
- package/harness/commands/harness-audit.md +1 -1
- package/harness/commands/learn.md +2 -2
- package/harness/commands/memory-search.md +2 -2
- package/harness/constitution/soul.md +16 -4
- package/harness/instructions/RUNTIME.md +6 -3
- package/harness/prompts/architect.txt +14 -0
- package/harness/prompts/build-cpp.md +15 -1
- package/harness/prompts/build-error-resolver.md +15 -9
- package/harness/prompts/build-go.md +14 -0
- package/harness/prompts/build-java.md +15 -1
- package/harness/prompts/build-kotlin.md +15 -1
- package/harness/prompts/build-rust.md +14 -0
- package/harness/prompts/code-reviewer.md +15 -9
- package/harness/prompts/doc-updater.md +13 -0
- package/harness/prompts/docs-lookup.md +11 -0
- package/harness/prompts/e2e-runner.txt +12 -0
- package/harness/prompts/explore.md +16 -4
- package/harness/prompts/harness-optimizer.md +12 -0
- package/harness/prompts/loop-operator.md +11 -0
- package/harness/prompts/planner.md +15 -9
- package/harness/prompts/refactor-cleaner.md +14 -0
- package/harness/prompts/review-cpp.md +14 -1
- package/harness/prompts/review-database.md +13 -0
- package/harness/prompts/review-go.md +13 -0
- package/harness/prompts/review-java.md +14 -1
- package/harness/prompts/review-kotlin.md +13 -0
- package/harness/prompts/review-python.md +14 -1
- package/harness/prompts/review-rust.md +13 -0
- package/harness/prompts/security-reviewer.md +15 -9
- package/harness/prompts/tdd-guide.md +14 -0
- package/harness/rules/audit.md +2 -2
- package/harness/rules/delegation.md +0 -2
- package/harness/rules/handoff.md +267 -0
- package/harness/rules/memory-management.md +4 -4
- package/harness/rules/precedence.md +1 -1
- package/harness/rules/retrieval.md +5 -5
- package/harness/rules/runtime-guards.md +1 -1
- package/harness/rules/self-heal.md +1 -1
- package/harness/rules/session-start.md +5 -5
- package/harness/rules/skills-management.md +2 -2
- package/harness/rules/verification.md +4 -4
- package/index.mjs +6 -2
- package/lib/ambient-memory.mjs +167 -0
- package/lib/handoff.mjs +176 -0
- package/lib/hardening.mjs +13 -8
- package/lib/memory-tools-plugin.mjs +107 -54
- package/lib/ohc/block-sync.mjs +69 -0
- package/lib/ohc/compress/search.mjs +152 -0
- package/lib/ohc/compress/state.mjs +76 -0
- package/lib/ohc/config.mjs +172 -16
- package/lib/ohc/message-ids.mjs +168 -0
- package/lib/ohc/notify.mjs +150 -0
- package/lib/ohc/protected-patterns.mjs +54 -0
- package/lib/ohc/prune-apply.mjs +134 -0
- package/lib/ohc/pruner.mjs +406 -55
- package/lib/ohc/reaper.mjs +12 -3
- package/lib/ohc/state.mjs +246 -15
- package/lib/ohc/strategies/deduplication.mjs +72 -0
- package/lib/ohc/strategies/index.mjs +2 -0
- package/lib/ohc/strategies/purge-errors.mjs +43 -0
- package/lib/ohc/token-utils.mjs +26 -0
- package/lib/ohc/updater.mjs +36 -13
- package/lib/paths.mjs +0 -3
- package/lib/search.mjs +48 -0
- package/package.json +1 -1
- package/schemas/audit.schema.json +22 -1
- package/schemas/backlog.schema.json +23 -2
- package/schemas/checkpoint.schema.json +23 -2
- package/schemas/constraint.schema.json +23 -2
- package/schemas/decision.schema.json +23 -2
- package/schemas/instinct.schema.json +23 -2
- package/schemas/mistake.schema.json +23 -2
- package/schemas/verification_receipt.schema.json +23 -2
- package/skill-builder.mjs +12 -23
package/lib/ohc/state.mjs
CHANGED
|
@@ -2,31 +2,262 @@ import fs from "node:fs"
|
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
|
|
5
|
-
const STATE_DIR = path.join(os.homedir(), ".local", "share", "opencode")
|
|
6
|
-
const
|
|
5
|
+
const STATE_DIR = path.join(os.homedir(), ".local", "share", "opencode", "ohc")
|
|
6
|
+
const LEGACY_FILE = path.join(os.homedir(), ".local", "share", "opencode", "ohc-state.json")
|
|
7
7
|
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} catch {
|
|
12
|
-
return {}
|
|
13
|
-
}
|
|
8
|
+
function sessionPath(sessionId) {
|
|
9
|
+
const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
10
|
+
return path.join(STATE_DIR, `${safe}.json`)
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
function
|
|
13
|
+
function ensureDir() {
|
|
17
14
|
fs.mkdirSync(STATE_DIR, { recursive: true })
|
|
18
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2), "utf8")
|
|
19
15
|
}
|
|
20
16
|
|
|
17
|
+
function migrateLegacy() {
|
|
18
|
+
try {
|
|
19
|
+
if (!fs.existsSync(LEGACY_FILE)) return
|
|
20
|
+
const raw = JSON.parse(fs.readFileSync(LEGACY_FILE, "utf8"))
|
|
21
|
+
if (typeof raw !== "object") return
|
|
22
|
+
ensureDir()
|
|
23
|
+
for (const [sid, data] of Object.entries(raw)) {
|
|
24
|
+
const sp = sessionPath(sid)
|
|
25
|
+
if (!fs.existsSync(sp)) {
|
|
26
|
+
fs.writeFileSync(sp, JSON.stringify({ ...data, migratedFrom: "legacy" }, null, 2), "utf8")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
fs.renameSync(LEGACY_FILE, LEGACY_FILE + ".bak")
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
migrateLegacy()
|
|
34
|
+
|
|
21
35
|
export function loadOhcState(sessionId) {
|
|
22
36
|
if (!sessionId) return null
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fs.readFileSync(sessionPath(sessionId), "utf8"))
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
export function saveOhcState(sessionId, data) {
|
|
28
45
|
if (!sessionId) return
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
ensureDir()
|
|
47
|
+
fs.writeFileSync(sessionPath(sessionId), JSON.stringify({ ...data, updatedAt: new Date().toISOString() }, null, 2), "utf8")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export function createSessionState() {
|
|
52
|
+
return {
|
|
53
|
+
sessionId: null,
|
|
54
|
+
isSubAgent: false,
|
|
55
|
+
manualMode: false,
|
|
56
|
+
pendingManualTrigger: null,
|
|
57
|
+
prune: {
|
|
58
|
+
tools: new Map(),
|
|
59
|
+
messages: {
|
|
60
|
+
byMessageId: new Map(),
|
|
61
|
+
blocksById: new Map(),
|
|
62
|
+
activeBlockIds: new Set(),
|
|
63
|
+
activeByAnchorMessageId: new Map(),
|
|
64
|
+
nextBlockId: 1,
|
|
65
|
+
nextRunId: 1,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
nudges: {
|
|
69
|
+
contextLimitAnchors: new Set(),
|
|
70
|
+
turnNudgeAnchors: new Set(),
|
|
71
|
+
iterationNudgeAnchors: new Set(),
|
|
72
|
+
},
|
|
73
|
+
stats: {
|
|
74
|
+
pruneTokenCounter: 0,
|
|
75
|
+
totalPruneTokens: 0,
|
|
76
|
+
},
|
|
77
|
+
toolParameters: new Map(),
|
|
78
|
+
toolIdList: [],
|
|
79
|
+
messageIds: { byRawId: new Map(), byRef: new Map(), nextRef: 1 },
|
|
80
|
+
lastCompaction: 0,
|
|
81
|
+
currentTurn: 0,
|
|
82
|
+
modelContextLimit: undefined,
|
|
83
|
+
systemPromptTokens: undefined,
|
|
84
|
+
protectedTurns: { enabled: false, turns: 0 },
|
|
85
|
+
compressionTiming: { starts: new Map(), pendingByCallId: new Map(), lastDurationMs: 0, totalDurationMs: 0 },
|
|
86
|
+
lastNudgePct: 0,
|
|
87
|
+
lastAutoPruneAt: null,
|
|
88
|
+
prunedIds: new Set(),
|
|
89
|
+
summary: null,
|
|
90
|
+
anchorMessageId: null,
|
|
91
|
+
totalTokensSaved: 0,
|
|
92
|
+
blockCount: 0,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pruneMapToObj(map) {
|
|
97
|
+
return Object.fromEntries(map)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pruneMapFromObj(obj) {
|
|
101
|
+
if (!obj || typeof obj !== "object") return new Map()
|
|
102
|
+
return new Map(Object.entries(obj))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function setToArr(s) {
|
|
106
|
+
return [...s]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function setFromArr(a) {
|
|
110
|
+
return new Set(Array.isArray(a) ? a : [])
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function serializeState(state) {
|
|
114
|
+
return {
|
|
115
|
+
sessionId: state.sessionId,
|
|
116
|
+
manualMode: state.manualMode,
|
|
117
|
+
lastCompaction: state.lastCompaction,
|
|
118
|
+
currentTurn: state.currentTurn,
|
|
119
|
+
modelContextLimit: state.modelContextLimit,
|
|
120
|
+
systemPromptTokens: state.systemPromptTokens,
|
|
121
|
+
stats: { ...state.stats },
|
|
122
|
+
nudges: {
|
|
123
|
+
contextLimitAnchors: setToArr(state.nudges.contextLimitAnchors),
|
|
124
|
+
turnNudgeAnchors: setToArr(state.nudges.turnNudgeAnchors),
|
|
125
|
+
iterationNudgeAnchors: setToArr(state.nudges.iterationNudgeAnchors),
|
|
126
|
+
},
|
|
127
|
+
prune: {
|
|
128
|
+
tools: pruneMapToObj(state.prune.tools),
|
|
129
|
+
messages: {
|
|
130
|
+
nextBlockId: state.prune?.messages?.nextBlockId || 1,
|
|
131
|
+
nextRunId: state.prune?.messages?.nextRunId || 1,
|
|
132
|
+
blocksById: pruneMapToObj(state.prune?.messages?.blocksById),
|
|
133
|
+
byMessageId: pruneMapToObj(state.prune?.messages?.byMessageId),
|
|
134
|
+
activeBlockIds: setToArr(state.prune?.messages?.activeBlockIds),
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
lastAutoPruneAt: state.lastAutoPruneAt,
|
|
138
|
+
totalTokensSaved: state.totalTokensSaved,
|
|
139
|
+
blockCount: state.blockCount,
|
|
140
|
+
summary: state.summary,
|
|
141
|
+
anchorMessageId: state.anchorMessageId,
|
|
142
|
+
prunedIds: setToArr(state.prunedIds),
|
|
143
|
+
isSubAgent: state.isSubAgent || false,
|
|
144
|
+
messageIds: {
|
|
145
|
+
byRawId: pruneMapToObj(state.messageIds?.byRawId),
|
|
146
|
+
byRef: pruneMapToObj(state.messageIds?.byRef),
|
|
147
|
+
nextRef: state.messageIds?.nextRef || 1,
|
|
148
|
+
},
|
|
149
|
+
compressionTiming: {
|
|
150
|
+
starts: Object.fromEntries(state.compressionTiming?.starts || new Map()),
|
|
151
|
+
lastDurationMs: state.compressionTiming?.lastDurationMs || 0,
|
|
152
|
+
totalDurationMs: state.compressionTiming?.totalDurationMs || 0,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function deserializeState(saved) {
|
|
158
|
+
const state = createSessionState()
|
|
159
|
+
if (!saved) return state
|
|
160
|
+
state.sessionId = saved.sessionId || null
|
|
161
|
+
state.manualMode = saved.manualMode || false
|
|
162
|
+
state.lastCompaction = saved.lastCompaction || 0
|
|
163
|
+
state.currentTurn = saved.currentTurn || 0
|
|
164
|
+
state.modelContextLimit = saved.modelContextLimit
|
|
165
|
+
state.systemPromptTokens = saved.systemPromptTokens
|
|
166
|
+
if (saved.stats) Object.assign(state.stats, saved.stats)
|
|
167
|
+
if (saved.nudges) {
|
|
168
|
+
state.nudges.contextLimitAnchors = setFromArr(saved.nudges.contextLimitAnchors)
|
|
169
|
+
state.nudges.turnNudgeAnchors = setFromArr(saved.nudges.turnNudgeAnchors)
|
|
170
|
+
state.nudges.iterationNudgeAnchors = setFromArr(saved.nudges.iterationNudgeAnchors)
|
|
171
|
+
}
|
|
172
|
+
if (saved.prune?.tools) state.prune.tools = pruneMapFromObj(saved.prune.tools)
|
|
173
|
+
if (saved.prune?.messages) {
|
|
174
|
+
const pm = saved.prune.messages
|
|
175
|
+
state.prune.messages.nextBlockId = pm.nextBlockId || 1
|
|
176
|
+
state.prune.messages.nextRunId = pm.nextRunId || 1
|
|
177
|
+
state.prune.messages.blocksById = pruneMapFromObj(pm.blocksById)
|
|
178
|
+
state.prune.messages.byMessageId = pruneMapFromObj(pm.byMessageId)
|
|
179
|
+
state.prune.messages.activeBlockIds = setFromArr(pm.activeBlockIds)
|
|
180
|
+
}
|
|
181
|
+
state.lastAutoPruneAt = saved.lastAutoPruneAt || null
|
|
182
|
+
state.totalTokensSaved = saved.totalTokensSaved || 0
|
|
183
|
+
state.blockCount = saved.blockCount || 0
|
|
184
|
+
state.summary = saved.summary || null
|
|
185
|
+
state.anchorMessageId = saved.anchorMessageId || null
|
|
186
|
+
state.prunedIds = setFromArr(saved.prunedIds)
|
|
187
|
+
if (saved.compressionTiming) {
|
|
188
|
+
state.compressionTiming.starts = pruneMapFromObj(saved.compressionTiming.starts)
|
|
189
|
+
state.compressionTiming.lastDurationMs = saved.compressionTiming.lastDurationMs || 0
|
|
190
|
+
state.compressionTiming.totalDurationMs = saved.compressionTiming.totalDurationMs || 0
|
|
191
|
+
}
|
|
192
|
+
if (saved.messageIds) {
|
|
193
|
+
state.messageIds.byRawId = pruneMapFromObj(saved.messageIds.byRawId)
|
|
194
|
+
state.messageIds.byRef = pruneMapFromObj(saved.messageIds.byRef)
|
|
195
|
+
state.messageIds.nextRef = saved.messageIds.nextRef || 1
|
|
196
|
+
}
|
|
197
|
+
if (saved.isSubAgent !== undefined) state.isSubAgent = saved.isSubAgent
|
|
198
|
+
return state
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function buildToolIdList(state, messages) {
|
|
202
|
+
const ids = []
|
|
203
|
+
for (const msg of messages) {
|
|
204
|
+
if (!Array.isArray(msg.parts)) continue
|
|
205
|
+
for (const part of msg.parts) {
|
|
206
|
+
if (part.type === "tool" && part.callID) {
|
|
207
|
+
ids.push(part.callID)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
state.toolIdList = ids
|
|
212
|
+
return ids
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function syncToolCache(state, messages) {
|
|
216
|
+
let maxTurn = 0
|
|
217
|
+
for (const msg of messages) {
|
|
218
|
+
if (msg.info?.role === "user") {
|
|
219
|
+
const lastUser = state.toolIdList.length > 0
|
|
220
|
+
if (lastUser) maxTurn++
|
|
221
|
+
}
|
|
222
|
+
if (!Array.isArray(msg.parts)) continue
|
|
223
|
+
for (const part of msg.parts) {
|
|
224
|
+
if (part.type !== "tool" || !part.callID) continue
|
|
225
|
+
const existing = state.toolParameters.get(part.callID)
|
|
226
|
+
if (existing) {
|
|
227
|
+
existing.status = part.state?.status || existing.status
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
state.toolParameters.set(part.callID, {
|
|
231
|
+
tool: part.tool || "unknown",
|
|
232
|
+
parameters: part.state?.input || {},
|
|
233
|
+
status: part.state?.status || "pending",
|
|
234
|
+
turn: maxTurn,
|
|
235
|
+
tokenCount: estimateToolTokens(part),
|
|
236
|
+
lastSeen: Date.now(),
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
state.currentTurn = Math.max(state.currentTurn, maxTurn)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function estimateToolTokens(part) {
|
|
244
|
+
if (!part.state) return 0
|
|
245
|
+
let t = 0
|
|
246
|
+
if (part.state.input) t += JSON.stringify(part.state.input).length / 4
|
|
247
|
+
if (part.state.output) {
|
|
248
|
+
t += (typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output ?? "")).length / 4
|
|
249
|
+
}
|
|
250
|
+
return Math.ceil(t)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function countTurns(state, messages) {
|
|
254
|
+
let userCount = 0
|
|
255
|
+
for (const msg of messages) {
|
|
256
|
+
if (msg.info?.role === "user") {
|
|
257
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
258
|
+
const hasText = parts.some(p => p.type === "text" && p.text?.trim())
|
|
259
|
+
if (hasText) userCount++
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return userCount
|
|
32
263
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isToolNameProtected, getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns.mjs"
|
|
2
|
+
import { getTotalToolTokens } from "../token-utils.mjs"
|
|
3
|
+
|
|
4
|
+
export function deduplicate(state, config, messages) {
|
|
5
|
+
if (state.manualMode && !config.manualMode?.automaticStrategies) return
|
|
6
|
+
|
|
7
|
+
if (!config.strategies?.deduplication?.enabled) return
|
|
8
|
+
|
|
9
|
+
const allIds = state.toolIdList
|
|
10
|
+
if (!allIds?.length) return
|
|
11
|
+
|
|
12
|
+
const unprunedIds = allIds.filter(id => !state.prune.tools.has(id))
|
|
13
|
+
if (!unprunedIds.length) return
|
|
14
|
+
|
|
15
|
+
const protectedTools = config.strategies.deduplication.protectedTools || []
|
|
16
|
+
|
|
17
|
+
const sigMap = new Map()
|
|
18
|
+
|
|
19
|
+
for (const id of unprunedIds) {
|
|
20
|
+
const meta = state.toolParameters.get(id)
|
|
21
|
+
if (!meta) continue
|
|
22
|
+
|
|
23
|
+
if (isToolNameProtected(meta.tool, protectedTools)) continue
|
|
24
|
+
|
|
25
|
+
const fps = getFilePathsFromParameters(meta.tool, meta.parameters)
|
|
26
|
+
if (isFilePathProtected(fps, config.protectedFilePatterns)) continue
|
|
27
|
+
|
|
28
|
+
const sig = createToolSignature(meta.tool, meta.parameters)
|
|
29
|
+
if (!sigMap.has(sig)) sigMap.set(sig, [])
|
|
30
|
+
sigMap.get(sig).push(id)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toPrune = []
|
|
34
|
+
for (const ids of sigMap.values()) {
|
|
35
|
+
if (ids.length > 1) {
|
|
36
|
+
toPrune.push(...ids.slice(0, -1))
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!toPrune.length) return
|
|
41
|
+
|
|
42
|
+
state.stats.totalPruneTokens += getTotalToolTokens(state, toPrune)
|
|
43
|
+
for (const id of toPrune) {
|
|
44
|
+
const entry = state.toolParameters.get(id)
|
|
45
|
+
state.prune.tools.set(id, entry?.tokenCount ?? 0)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createToolSignature(tool, params) {
|
|
50
|
+
if (!params) return tool
|
|
51
|
+
const norm = normalizeParams(params)
|
|
52
|
+
const sorted = sortKeys(norm)
|
|
53
|
+
return `${tool}::${JSON.stringify(sorted)}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeParams(p) {
|
|
57
|
+
if (typeof p !== "object" || p === null) return p
|
|
58
|
+
if (Array.isArray(p)) return p
|
|
59
|
+
const n = {}
|
|
60
|
+
for (const [k, v] of Object.entries(p)) {
|
|
61
|
+
if (v !== undefined && v !== null) n[k] = v
|
|
62
|
+
}
|
|
63
|
+
return n
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sortKeys(o) {
|
|
67
|
+
if (typeof o !== "object" || o === null) return o
|
|
68
|
+
if (Array.isArray(o)) return o.map(sortKeys)
|
|
69
|
+
const s = {}
|
|
70
|
+
for (const k of Object.keys(o).sort()) s[k] = sortKeys(o[k])
|
|
71
|
+
return s
|
|
72
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isToolNameProtected, getFilePathsFromParameters, isFilePathProtected } from "../protected-patterns.mjs"
|
|
2
|
+
import { getTotalToolTokens } from "../token-utils.mjs"
|
|
3
|
+
|
|
4
|
+
export function purgeErrors(state, config, messages) {
|
|
5
|
+
if (state.manualMode && !config.manualMode?.automaticStrategies) return
|
|
6
|
+
|
|
7
|
+
if (!config.strategies?.purgeErrors?.enabled) return
|
|
8
|
+
|
|
9
|
+
const allIds = state.toolIdList
|
|
10
|
+
if (!allIds?.length) return
|
|
11
|
+
|
|
12
|
+
const unprunedIds = allIds.filter(id => !state.prune.tools.has(id))
|
|
13
|
+
if (!unprunedIds.length) return
|
|
14
|
+
|
|
15
|
+
const protectedTools = config.strategies.purgeErrors.protectedTools || []
|
|
16
|
+
const threshold = Math.max(1, config.strategies.purgeErrors.turns ?? 4)
|
|
17
|
+
|
|
18
|
+
const toPrune = []
|
|
19
|
+
for (const id of unprunedIds) {
|
|
20
|
+
const meta = state.toolParameters.get(id)
|
|
21
|
+
if (!meta) continue
|
|
22
|
+
|
|
23
|
+
if (isToolNameProtected(meta.tool, protectedTools)) continue
|
|
24
|
+
|
|
25
|
+
const fps = getFilePathsFromParameters(meta.tool, meta.parameters)
|
|
26
|
+
if (isFilePathProtected(fps, config.protectedFilePatterns)) continue
|
|
27
|
+
|
|
28
|
+
if (meta.status !== "error") continue
|
|
29
|
+
|
|
30
|
+
const turnAge = state.currentTurn - meta.turn
|
|
31
|
+
if (turnAge >= threshold) {
|
|
32
|
+
toPrune.push(id)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!toPrune.length) return
|
|
37
|
+
|
|
38
|
+
state.stats.totalPruneTokens += getTotalToolTokens(state, toPrune)
|
|
39
|
+
for (const id of toPrune) {
|
|
40
|
+
const entry = state.toolParameters.get(id)
|
|
41
|
+
state.prune.tools.set(id, entry?.tokenCount ?? 0)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { totalTokens } from "./reaper.mjs"
|
|
2
|
+
|
|
3
|
+
export { totalTokens }
|
|
4
|
+
|
|
5
|
+
export function countTokens(value) {
|
|
6
|
+
if (typeof value === "string") return Math.ceil(value.length / 4)
|
|
7
|
+
if (typeof value === "object" && value !== null) return Math.ceil(JSON.stringify(value).length / 4)
|
|
8
|
+
return 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getTotalToolTokens(state, toolIds) {
|
|
12
|
+
let total = 0
|
|
13
|
+
for (const id of toolIds) {
|
|
14
|
+
const entry = state.toolParameters.get(id)
|
|
15
|
+
if (entry?.tokenCount) total += entry.tokenCount
|
|
16
|
+
}
|
|
17
|
+
return total
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function estimateToolTokenCost(part) {
|
|
21
|
+
if (part.type !== "tool") return 0
|
|
22
|
+
let t = 0
|
|
23
|
+
if (part.state?.input) t += countTokens(part.state.input)
|
|
24
|
+
if (part.state?.output) t += countTokens(part.state.output)
|
|
25
|
+
return t
|
|
26
|
+
}
|
package/lib/ohc/updater.mjs
CHANGED
|
@@ -3,7 +3,10 @@ import path from "node:path"
|
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
|
|
5
5
|
const CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "opencode.json")
|
|
6
|
-
const
|
|
6
|
+
const CACHE_ROOTS = [
|
|
7
|
+
path.join(os.homedir(), ".cache", "opencode", "packages"),
|
|
8
|
+
path.join(os.homedir(), ".cache", "opencode", "node_modules"),
|
|
9
|
+
]
|
|
7
10
|
|
|
8
11
|
function detectInstallMethod() {
|
|
9
12
|
let raw = {}
|
|
@@ -33,18 +36,37 @@ function detectInstallMethod() {
|
|
|
33
36
|
return { method: "unknown", entry: null, reason: "openhermes not found in plugin config" }
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
+
function walkCacheDirs(root, results) {
|
|
40
|
+
if (!fs.existsSync(root)) return
|
|
41
|
+
if (path.basename(root).startsWith("openhermes")) {
|
|
42
|
+
results.push({ name: path.basename(root), path: root })
|
|
43
|
+
}
|
|
44
|
+
let entries = []
|
|
39
45
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
47
|
+
} catch {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory()) continue
|
|
53
|
+
const full = path.join(root, entry.name)
|
|
54
|
+
if (entry.name.startsWith("openhermes")) {
|
|
55
|
+
results.push({ name: entry.name, path: full })
|
|
45
56
|
}
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
walkCacheDirs(full, results)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function findCacheDirs({ cacheRoots = CACHE_ROOTS } = {}) {
|
|
62
|
+
const results = []
|
|
63
|
+
for (const root of cacheRoots) walkCacheDirs(root, results)
|
|
64
|
+
const seen = new Set()
|
|
65
|
+
return results.filter(dir => {
|
|
66
|
+
if (seen.has(dir.path)) return false
|
|
67
|
+
seen.add(dir.path)
|
|
68
|
+
return true
|
|
69
|
+
})
|
|
48
70
|
}
|
|
49
71
|
|
|
50
72
|
async function handleUpdateMe(ctx, input, output) {
|
|
@@ -65,7 +87,8 @@ async function handleUpdateMe(ctx, input, output) {
|
|
|
65
87
|
output.parts.length = 0
|
|
66
88
|
output.parts.push({
|
|
67
89
|
type: "text",
|
|
68
|
-
text: `[Update-Me] No
|
|
90
|
+
text: `[Update-Me] No OpenHermes cache found under OpenCode package/node_modules caches.
|
|
91
|
+
Restart OpenCode to redownload from ${info.method === "git" ? "git HEAD" : "npm registry"}.`,
|
|
69
92
|
})
|
|
70
93
|
return
|
|
71
94
|
}
|
|
@@ -92,7 +115,7 @@ async function handleUpdateMe(ctx, input, output) {
|
|
|
92
115
|
if (failed.length > 0) {
|
|
93
116
|
msg += `\n⚠ Could not remove (file may be locked):\n`
|
|
94
117
|
for (const f of failed) msg += ` ✗ ${f.name} — ${f.error}\n`
|
|
95
|
-
msg += `Try deleting manually:\n ${
|
|
118
|
+
msg += `Try deleting manually:\n ${CACHE_ROOTS.join("\n ")}\n`
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
msg += `\nRestart OpenCode to load the latest OpenHermes from ${info.method === "git" ? "git HEAD" : "npm registry"}.`
|
package/lib/paths.mjs
CHANGED
package/lib/search.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function scoreRelevance(r, query, project) {
|
|
2
|
+
const q = query.toLowerCase()
|
|
3
|
+
const tokens = q.split(/\s+/).filter(t => t.length > 2)
|
|
4
|
+
let score = 0
|
|
5
|
+
|
|
6
|
+
const primaryFields = [r.summary, r.description, r.mission, r.current_state, r.failure, r.root_cause, r.fix, r.prevention, r.id].filter(Boolean)
|
|
7
|
+
const secondaryFields = [r.command, r.project, r.scope].filter(Boolean)
|
|
8
|
+
const listFields = [...(Array.isArray(r.tags) ? r.tags : []), ...(Array.isArray(r.next_actions) ? r.next_actions : []), ...(Array.isArray(r.refs) ? r.refs : [])].filter(Boolean)
|
|
9
|
+
|
|
10
|
+
for (const f of primaryFields) {
|
|
11
|
+
const str = String(f).toLowerCase()
|
|
12
|
+
let idx = 0; let count = 0
|
|
13
|
+
while ((idx = str.indexOf(q, idx)) !== -1) { count++; idx += q.length }
|
|
14
|
+
score += count * 15
|
|
15
|
+
if (str.startsWith(q)) score += 10
|
|
16
|
+
if (str.includes(q)) score += 4
|
|
17
|
+
for (const token of tokens) {
|
|
18
|
+
if (str.includes(token)) score += 4
|
|
19
|
+
if (str.startsWith(token)) score += 2
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const f of secondaryFields) {
|
|
24
|
+
const str = String(f).toLowerCase()
|
|
25
|
+
if (str.includes(q)) score += 8
|
|
26
|
+
for (const token of tokens) {
|
|
27
|
+
if (str.includes(token)) score += 3
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const f of listFields) {
|
|
32
|
+
const str = String(f).toLowerCase()
|
|
33
|
+
if (str.includes(q)) score += 5
|
|
34
|
+
for (const token of tokens) {
|
|
35
|
+
if (str.includes(token)) score += 2
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (r.project && r.project.toLowerCase() === (project || "").toLowerCase()) score += 25
|
|
40
|
+
if (r.project && project && r.project.toLowerCase().includes(project.toLowerCase())) score += 12
|
|
41
|
+
|
|
42
|
+
const age = Date.now() - Date.parse(r.updated_at || r.created_at || 0)
|
|
43
|
+
if (!Number.isNaN(age)) score += Math.max(0, 10 - age / 604800000)
|
|
44
|
+
if (r.status === "active") score += 4
|
|
45
|
+
if (r.status === "closed") score -= 3
|
|
46
|
+
|
|
47
|
+
return score
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhermes",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.5.0",
|
|
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",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"task_id": { "type": "string" },
|
|
21
21
|
"db_refs": { "type": "array", "items": { "type": "string" } },
|
|
22
22
|
"file_refs": { "type": "array", "items": { "type": "string" } },
|
|
23
|
-
"log_refs": { "type": "array", "items": { "type": "string" } }
|
|
23
|
+
"log_refs": { "type": "array", "items": { "type": "string" } },
|
|
24
|
+
"harness_root": { "type": "string" },
|
|
25
|
+
"project_root": { "type": "string" }
|
|
24
26
|
}
|
|
25
27
|
},
|
|
26
28
|
"created_at": { "type": "string", "format": "date-time", "description": "ISO-8601 timestamp" },
|
|
@@ -56,6 +58,25 @@
|
|
|
56
58
|
"provenance_ok": { "type": "boolean", "description": "All objects have valid provenance" },
|
|
57
59
|
"duplicates_ok": { "type": "boolean", "description": "No duplicate IDs found" }
|
|
58
60
|
}
|
|
61
|
+
},
|
|
62
|
+
"description": { "type": "string", "description": "Optional description" },
|
|
63
|
+
"environment_fingerprint": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"description": "System fingerprint at creation time",
|
|
66
|
+
"properties": {
|
|
67
|
+
"cwd": { "type": "string" },
|
|
68
|
+
"harness_root": { "type": "string" },
|
|
69
|
+
"project_root": { "type": "string" },
|
|
70
|
+
"project": { "type": "string" },
|
|
71
|
+
"session_id": { "type": "string" },
|
|
72
|
+
"os": { "type": "string" },
|
|
73
|
+
"release": { "type": "string" },
|
|
74
|
+
"arch": { "type": "string" },
|
|
75
|
+
"shell": { "type": "string" },
|
|
76
|
+
"provider": { "type": "string" },
|
|
77
|
+
"model": { "type": "string" },
|
|
78
|
+
"sha256": { "type": "string" }
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
}
|
|
61
82
|
}
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"task_id": { "type": "string" },
|
|
21
21
|
"db_refs": { "type": "array", "items": { "type": "string" } },
|
|
22
22
|
"file_refs": { "type": "array", "items": { "type": "string" } },
|
|
23
|
-
"log_refs": { "type": "array", "items": { "type": "string" } }
|
|
23
|
+
"log_refs": { "type": "array", "items": { "type": "string" } },
|
|
24
|
+
"harness_root": { "type": "string" },
|
|
25
|
+
"project_root": { "type": "string" }
|
|
24
26
|
}
|
|
25
27
|
},
|
|
26
28
|
"created_at": { "type": "string", "format": "date-time", "description": "ISO-8601 timestamp" },
|
|
@@ -37,6 +39,25 @@
|
|
|
37
39
|
"priority": { "type": "string", "enum": ["low", "medium", "high", "critical", "P0", "P1", "P2", "P3", "P4"], "description": "Priority level (low/medium/high/critical or P0-P4)" },
|
|
38
40
|
"trigger": { "type": "string", "enum": ["audit", "mistake", "drift", "user", "manual"], "description": "What triggered creation of this item" },
|
|
39
41
|
"evidence_refs": { "type": "array", "items": { "type": "string" }, "description": "References to evidence (audit IDs, mistake IDs, file paths)" },
|
|
40
|
-
"done_when": { "type": "array", "items": { "type": "string" }, "description": "Acceptance criteria — concrete conditions for closure" }
|
|
42
|
+
"done_when": { "type": "array", "items": { "type": "string" }, "description": "Acceptance criteria — concrete conditions for closure" },
|
|
43
|
+
"description": { "type": "string", "description": "Optional description" },
|
|
44
|
+
"environment_fingerprint": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"description": "System fingerprint at creation time",
|
|
47
|
+
"properties": {
|
|
48
|
+
"cwd": { "type": "string" },
|
|
49
|
+
"harness_root": { "type": "string" },
|
|
50
|
+
"project_root": { "type": "string" },
|
|
51
|
+
"project": { "type": "string" },
|
|
52
|
+
"session_id": { "type": "string" },
|
|
53
|
+
"os": { "type": "string" },
|
|
54
|
+
"release": { "type": "string" },
|
|
55
|
+
"arch": { "type": "string" },
|
|
56
|
+
"shell": { "type": "string" },
|
|
57
|
+
"provider": { "type": "string" },
|
|
58
|
+
"model": { "type": "string" },
|
|
59
|
+
"sha256": { "type": "string" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
41
62
|
}
|
|
42
63
|
}
|