openhermes 2.6.1 → 2.8.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 +1 -1
- package/autorecall.mjs +1 -1
- package/bootstrap.mjs +45 -32
- package/curator.mjs +37 -10
- package/harness/rules/state-drift.md +1 -1
- package/index.mjs +3 -1
- package/lib/handoff.mjs +0 -5
- package/lib/hardening.mjs +21 -3
- package/lib/memory-tools-plugin.mjs +12 -9
- package/lib/ohc/compress/state.mjs +1 -1
- package/lib/ohc/config.mjs +3 -4
- package/lib/ohc/message-ids.mjs +10 -0
- package/lib/ohc/notify.mjs +31 -50
- package/lib/ohc/protected-patterns.mjs +3 -2
- package/lib/ohc/pruner.mjs +27 -29
- package/lib/ohc/reaper.mjs +1 -1
- package/lib/ohc/state.mjs +6 -7
- package/lib/ohc/updater.mjs +13 -14
- package/lib/paths.mjs +2 -3
- package/lib/schema-validator.mjs +7 -5
- package/lib/search.mjs +1 -1
- package/package.json +1 -1
- package/schemas/backlog.schema.json +1 -1
- package/schemas/instinct.schema.json +2 -2
- package/schemas/mistake.schema.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ No Python. No Docker. No cron. No database. Just Node.js and your existing OpenC
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## Agent Handoff Protocol
|
|
39
|
+
## Agent Handoff Protocol
|
|
40
40
|
|
|
41
41
|
The reason most agent sessions descend into chaos: every agent thinks it can do everything. OpenHermes fixes that with a **structured handoff system** baked into every subagent prompt.
|
|
42
42
|
|
package/autorecall.mjs
CHANGED
|
@@ -186,7 +186,7 @@ async function loadMemoryAndWriteCache(projectKey, directory) {
|
|
|
186
186
|
const backlogIndex = readJson(path.join(getMemoryRoot(), "backlog", "index.json"), [])
|
|
187
187
|
if (Array.isArray(backlogIndex)) {
|
|
188
188
|
memory.pendingSkillCandidates = backlogIndex.filter(e =>
|
|
189
|
-
e.status === "open" && (e.
|
|
189
|
+
e.status === "open" && Array.isArray(e.tags) && e.tags.includes("skill-candidate")
|
|
190
190
|
)
|
|
191
191
|
}
|
|
192
192
|
|
package/bootstrap.mjs
CHANGED
|
@@ -67,11 +67,16 @@ export function resolveHarnessRoot({
|
|
|
67
67
|
return path.resolve(currentDir, "harness")
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
let _harnessDir
|
|
71
|
+
export function setHarnessRootForTest(dir) { _harnessDir = dir }
|
|
72
|
+
export function getHarnessDir() {
|
|
73
|
+
if (!_harnessDir) _harnessDir = resolveHarnessRoot()
|
|
74
|
+
return _harnessDir
|
|
75
|
+
}
|
|
76
|
+
function getRulesDir() { return path.join(getHarnessDir(), "rules") }
|
|
77
|
+
function getSkillsDir() { return path.join(getHarnessDir(), "skills") }
|
|
78
|
+
function getConstitutionFile() { return path.join(getHarnessDir(), "codex", "CONSTITUTION.md") }
|
|
79
|
+
function getRuntimeFile() { return path.join(getHarnessDir(), "instructions", "RUNTIME.md") }
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
function scanDirNames(dir) {
|
|
@@ -127,17 +132,17 @@ export function loadLocalSoulOverride(overridePath) {
|
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
function buildBootstrapContent() {
|
|
130
|
-
let constitution = fs.readFileSync(
|
|
135
|
+
let constitution = fs.readFileSync(getConstitutionFile(), "utf8")
|
|
131
136
|
const localOverride = loadLocalSoulOverride()
|
|
132
137
|
if (localOverride) {
|
|
133
138
|
constitution += `\n\n## Local Overrides (survives reinstalls)\n\n${localOverride}`
|
|
134
139
|
}
|
|
135
|
-
const runtime = fs.readFileSync(
|
|
136
|
-
const capMap = buildCapabilityMap(
|
|
140
|
+
const runtime = fs.readFileSync(getRuntimeFile(), "utf8")
|
|
141
|
+
const capMap = buildCapabilityMap(getHarnessDir())
|
|
137
142
|
|
|
138
143
|
const router = `## AGENTS.md
|
|
139
144
|
|
|
140
|
-
OpenHermes thin constitutional router. Full harness →
|
|
145
|
+
OpenHermes thin constitutional router. Full harness → \`openhermes/harness/\`.
|
|
141
146
|
|
|
142
147
|
## Constitution
|
|
143
148
|
|
|
@@ -145,7 +150,7 @@ Pragmatic. Concise. Task-oriented. Subagent-first. Inspect, then act. Scope to t
|
|
|
145
150
|
|
|
146
151
|
## Safety
|
|
147
152
|
|
|
148
|
-
Snapshot before mutation. Never delete unrelated files. Never assume \`%USERPROFILE
|
|
153
|
+
Snapshot before mutation. Never delete unrelated files. Never assume \`%USERPROFILE%\\.config\\opencode\` is a git repo. Verify or roll back. **NEVER delete \`auth.json\`** (\`%USERPROFILE%\\.local\\share\\opencode\\auth.json\`).
|
|
149
154
|
|
|
150
155
|
${capMap}
|
|
151
156
|
|
|
@@ -179,11 +184,11 @@ Main context = coordination + verification only. Substantive work → subagent.
|
|
|
179
184
|
| Rust build fix | \`build-rust\` |
|
|
180
185
|
| Any non-trivial multi-step | appropriate specialist |
|
|
181
186
|
|
|
182
|
-
Never delegate trivial single-step ops. Subagent returns diff + summary + verification; inspect return only. Full ref:
|
|
187
|
+
Never delegate trivial single-step ops. Subagent returns diff + summary + verification; inspect return only. Full ref: \`openhermes/harness/rules/delegation.md\`.
|
|
183
188
|
|
|
184
189
|
## Handoff Protocol
|
|
185
190
|
|
|
186
|
-
Every agent knows its role, permissions, and when to delegate. Before delegating, assess task complexity (easy → direct, medium → single subagent, hard → sequential multi-agent, very-large → fan-out). Use structured handoff format documented in
|
|
191
|
+
Every agent knows its role, permissions, and when to delegate. Before delegating, assess task complexity (easy → direct, medium → single subagent, hard → sequential multi-agent, very-large → fan-out). Use structured handoff format documented in \`openhermes/harness/rules/handoff.md\`.
|
|
187
192
|
|
|
188
193
|
- **Act**: Task matches your role and permissions → do it directly
|
|
189
194
|
- **Delegate**: Task outside your role → pass to correct agent via \`task\` tool
|
|
@@ -198,7 +203,7 @@ Every agent knows its role, permissions, and when to delegate. Before delegating
|
|
|
198
203
|
- **Before close**: Query same-type mistakes (7 days). Match → \`code-reviewer\` or \`security-reviewer\`.
|
|
199
204
|
- **On failure**: \`ohc_search\` for similar incidents. Search memory before asking user.
|
|
200
205
|
- **Precision ladder**: \`ohc_latest\` → \`ohc_search\` → \`ohc_get\` → \`ohc_list\` (last resort). Full index reads only for explicit audit/repair tasks.
|
|
201
|
-
- **Anti-spam**: No obvious facts, no one-off prefs, no temp state, no low-risk mistakes. Supersede, don't duplicate. Full rules:
|
|
206
|
+
- **Anti-spam**: No obvious facts, no one-off prefs, no temp state, no low-risk mistakes. Supersede, don't duplicate. Full rules: \`openhermes/harness/rules/retrieval.md\`, \`openhermes/harness/rules/memory-management.md\`.
|
|
202
207
|
|
|
203
208
|
## Self-Edit Authority
|
|
204
209
|
|
|
@@ -206,44 +211,44 @@ Every agent knows its role, permissions, and when to delegate. Before delegating
|
|
|
206
211
|
|------|-------------|----------------|
|
|
207
212
|
| Memory entries, mistakes, checkpoints, receipts | openhermes docs/schemas/templates/non-core rules patches | AGENTS.md core, model routing, permissions, config, protected settings |
|
|
208
213
|
|
|
209
|
-
Full tiers:
|
|
214
|
+
Full tiers: \`openhermes/harness/rules/self-heal.md\`.
|
|
210
215
|
|
|
211
216
|
## Precedence
|
|
212
217
|
|
|
213
|
-
1. User instruction. 2. Safety/legal/destructive guard. 3. Constitution (
|
|
218
|
+
1. User instruction. 2. Safety/legal/destructive guard. 3. Constitution (\`openhermes/harness/codex/\`). 4. Project constraints. 5. Project decisions. 6. Verified guards. 7. Checkpoints. 8. Instincts. 9. Freeform notes. Full: \`openhermes/harness/rules/precedence.md\`.
|
|
214
219
|
|
|
215
220
|
## Hygiene
|
|
216
221
|
|
|
217
222
|
- Checkpoint on meaningful boundaries. Compress closed segments immediately.
|
|
218
223
|
- After subagent return: verify → compress that block.
|
|
219
224
|
- Compress proactively.
|
|
220
|
-
- Skill candidates → \`/learn\` only if repeated pattern + \`ohc_search\` confirms no dup. See
|
|
221
|
-
- Audit triggers: openhermes/config change, repeated failures, session start when last audit >7 days. See
|
|
225
|
+
- Skill candidates → \`/learn\` only if repeated pattern + \`ohc_search\` confirms no dup. See \`openhermes/harness/rules/skills-management.md\`.
|
|
226
|
+
- Audit triggers: openhermes/config change, repeated failures, session start when last audit >7 days. See \`openhermes/harness/rules/audit.md\`.
|
|
222
227
|
|
|
223
228
|
## Escalation
|
|
224
229
|
|
|
225
|
-
T0: observe → log mistake → smallest fix. T1: add prevention rule → verify. T2: diagnosis/specialist → backlog. T3: constrained safe mode. Full:
|
|
230
|
+
T0: observe → log mistake → smallest fix. T1: add prevention rule → verify. T2: diagnosis/specialist → backlog. T3: constrained safe mode. Full: \`openhermes/harness/rules/self-heal.md\`.
|
|
226
231
|
|
|
227
232
|
## State
|
|
228
233
|
|
|
229
|
-
- **Config root**: \`%USERPROFILE
|
|
230
|
-
- **Auth**: \`%USERPROFILE
|
|
231
|
-
- **Forensic ledger**: \`%USERPROFILE
|
|
234
|
+
- **Config root**: \`%USERPROFILE%\\.config\\opencode\`
|
|
235
|
+
- **Auth**: \`%USERPROFILE%\\.local\\share\\opencode\\auth.json\` (NEVER delete)
|
|
236
|
+
- **Forensic ledger**: \`%USERPROFILE%\\.local\\share\\opencode\\opencode.db\``
|
|
232
237
|
|
|
233
238
|
return [
|
|
234
|
-
`<OPENHERMES_BOOTSTRAP>\nOpenHermes v${getOwnVersion()} active. Harness:
|
|
239
|
+
`<OPENHERMES_BOOTSTRAP>\nOpenHermes v${getOwnVersion()} active. Harness: \`openhermes/harness/\`. Memory at \`~/.local/share/opencode/openhermes/memory/\`. Rules at \`openhermes/harness/rules/\`. Skills discoverable via \`skill\` tool — use \`skill\` tool to list/load them.`,
|
|
235
240
|
`<OPENHERMES_CONSTITUTION>\n${constitution}\n</OPENHERMES_CONSTITUTION>`,
|
|
236
241
|
`<OPENHERMES_RUNTIME>\n${runtime}\n</OPENHERMES_RUNTIME>`,
|
|
237
242
|
`<OPENHERMES_ROUTER>\n${router}\n</OPENHERMES_ROUTER>`
|
|
238
243
|
].join("\n\n")
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
const OWN_VERSION = (() => {
|
|
242
247
|
try {
|
|
243
|
-
|
|
244
|
-
return pkg.version || "1.0.0"
|
|
248
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version || "1.0.0"
|
|
245
249
|
} catch { return "1.0.0" }
|
|
246
|
-
}
|
|
250
|
+
})()
|
|
251
|
+
function getOwnVersion() { return OWN_VERSION }
|
|
247
252
|
|
|
248
253
|
export const BootstrapPlugin = async ({ client, directory }) => {
|
|
249
254
|
let _bootstrapCache
|
|
@@ -263,14 +268,22 @@ export const BootstrapPlugin = async ({ client, directory }) => {
|
|
|
263
268
|
config: async (config) => {
|
|
264
269
|
config.skills = config.skills || {}
|
|
265
270
|
config.skills.paths = config.skills.paths || []
|
|
266
|
-
if (!config.skills.paths.includes(
|
|
267
|
-
config.skills.paths.push(
|
|
271
|
+
if (!config.skills.paths.includes(getSkillsDir())) {
|
|
272
|
+
config.skills.paths.push(getSkillsDir())
|
|
268
273
|
}
|
|
269
274
|
|
|
270
|
-
const PROMPTS_DIR = path.join(
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
275
|
+
const PROMPTS_DIR = path.join(getHarnessDir(), "prompts")
|
|
276
|
+
const COMMANDS_DIR = path.join(getHarnessDir(), "commands")
|
|
277
|
+
const ct = (file) => {
|
|
278
|
+
const fp = path.join(COMMANDS_DIR, file)
|
|
279
|
+
try { return fs.readFileSync(fp, "utf8").trimEnd() + "\n\n$ARGUMENTS" }
|
|
280
|
+
catch { return "$ARGUMENTS" }
|
|
281
|
+
}
|
|
282
|
+
const p = (name) => {
|
|
283
|
+
const fp = path.join(PROMPTS_DIR, name)
|
|
284
|
+
try { return fs.readFileSync(fp, "utf8").trimEnd() }
|
|
285
|
+
catch { return "" }
|
|
286
|
+
}
|
|
274
287
|
|
|
275
288
|
const existingCommands = config.command ?? {}
|
|
276
289
|
const existingAgents = { ...(config.agent ?? {}) }
|
package/curator.mjs
CHANGED
|
@@ -14,6 +14,28 @@ const lastCheckpoint = { ts: 0 }
|
|
|
14
14
|
const writtenThisSession = []
|
|
15
15
|
const CURATOR_LOGS = /^(1|true|yes)$/i.test(process.env.OPENCODE_CURATOR_LOGS || "")
|
|
16
16
|
|
|
17
|
+
const MEMORY_CLASSES = ["checkpoints", "mistakes", "audits", "verification_receipts", "constraints", "decisions", "instincts", "backlog"]
|
|
18
|
+
|
|
19
|
+
function ensureConsistency(root) {
|
|
20
|
+
const memoryRoot = path.join(root, "memory")
|
|
21
|
+
const runtimeRoot = path.join(root, "runtime")
|
|
22
|
+
|
|
23
|
+
for (const cls of MEMORY_CLASSES) {
|
|
24
|
+
const dir = path.join(memoryRoot, cls)
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
26
|
+
const indexPath = path.join(dir, "index.json")
|
|
27
|
+
if (!fs.existsSync(indexPath)) {
|
|
28
|
+
atomicWriteJson(indexPath, [])
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fs.mkdirSync(runtimeRoot, { recursive: true })
|
|
33
|
+
const loopStatePath = path.join(runtimeRoot, "loop-state.json")
|
|
34
|
+
if (!fs.existsSync(loopStatePath)) {
|
|
35
|
+
atomicWriteJson(loopStatePath, { status: "idle", phase: "session.created" })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
function curatorLog(message) {
|
|
18
40
|
if (!CURATOR_LOGS) return
|
|
19
41
|
process.stderr.write(`${message}\n`)
|
|
@@ -70,7 +92,7 @@ function loadSchema(classId) {
|
|
|
70
92
|
function validateRecordAgainstSchema(record) {
|
|
71
93
|
const schema = loadSchema(record.class)
|
|
72
94
|
if (!schema) {
|
|
73
|
-
curatorLog(`[curator] no schema
|
|
95
|
+
curatorLog(`[curator] validation fallback: no schema for "${record.class}"`)
|
|
74
96
|
const required = record.class === "checkpoint"
|
|
75
97
|
? ["id", "class", "summary", "mission", "current_state", "next_actions", "blockers", "risk_notes", "provenance", "created_at", "status"]
|
|
76
98
|
: ["id", "class", "summary", "provenance", "created_at", "status"]
|
|
@@ -80,19 +102,19 @@ function validateRecordAgainstSchema(record) {
|
|
|
80
102
|
return false
|
|
81
103
|
}
|
|
82
104
|
if (record.class === "checkpoint" && record.provenance && !record.provenance.session_id) {
|
|
83
|
-
curatorLog(`[curator] validation failed:
|
|
105
|
+
curatorLog(`[curator] validation failed: missing session_id`)
|
|
84
106
|
return false
|
|
85
107
|
}
|
|
86
108
|
return true
|
|
87
109
|
}
|
|
88
110
|
const unsupported = findUnsupportedSchemaKeywords(schema)
|
|
89
111
|
if (unsupported.length) {
|
|
90
|
-
curatorLog(`[curator]
|
|
112
|
+
curatorLog(`[curator] validation failed: unsupported fields ${unsupported.join(", ")}`)
|
|
91
113
|
return false
|
|
92
114
|
}
|
|
93
115
|
const errors = validateSchema(schema, record, "$")
|
|
94
116
|
if (errors.length) {
|
|
95
|
-
curatorLog(`[curator]
|
|
117
|
+
curatorLog(`[curator] validation failed: ${errors.join("; ")}`)
|
|
96
118
|
return false
|
|
97
119
|
}
|
|
98
120
|
if (record.class === "checkpoint") {
|
|
@@ -164,7 +186,6 @@ async function writeCheckpoint(root, project, directory, trigger, summary, optio
|
|
|
164
186
|
const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
|
|
165
187
|
if (!validateRecordAgainstSchema(safeRecord)) return null
|
|
166
188
|
const dir = path.join(root, "memory", "checkpoints")
|
|
167
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
168
189
|
atomicWriteJson(path.join(dir, `${id}.json`), safeRecord)
|
|
169
190
|
indexEntry(root, "checkpoints", safeRecord)
|
|
170
191
|
updateLoopState(root, {
|
|
@@ -174,6 +195,7 @@ async function writeCheckpoint(root, project, directory, trigger, summary, optio
|
|
|
174
195
|
updated_at: ts,
|
|
175
196
|
status: trigger === "experimental.session.compacting" ? "active" : "idle",
|
|
176
197
|
})
|
|
198
|
+
if (writtenThisSession.length >= 100) writtenThisSession.shift()
|
|
177
199
|
writtenThisSession.push(id)
|
|
178
200
|
curatorLog(`[curator] checkpoint written: ${id} (trigger: ${trigger})`)
|
|
179
201
|
return id
|
|
@@ -261,7 +283,6 @@ function writeVerificationReceipt(root, project, directory, checkpointId) {
|
|
|
261
283
|
environment_fingerprint: environmentFingerprint,
|
|
262
284
|
}
|
|
263
285
|
const dir = path.join(root, "memory", "verification_receipts")
|
|
264
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
265
286
|
const safeRecord = sanitizeRecord(record, { maxStringLength: 4000 })
|
|
266
287
|
atomicWriteJson(path.join(dir, `${id}.json`), safeRecord)
|
|
267
288
|
indexEntry(root, "verification_receipts", safeRecord)
|
|
@@ -369,7 +390,6 @@ async function handlePermissionReplied(directory, project, event) {
|
|
|
369
390
|
}
|
|
370
391
|
}
|
|
371
392
|
const dir = path.join(root, "memory", "audits")
|
|
372
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
373
393
|
atomicWriteJson(path.join(dir, `${id}.json`), safeRecord)
|
|
374
394
|
indexEntry(root, "audits", safeRecord)
|
|
375
395
|
curatorLog(`[curator] permission audit logged: ${event.tool} -> ${event.action}`)
|
|
@@ -382,9 +402,16 @@ export const CuratorPlugin = async ({ project, directory }) => {
|
|
|
382
402
|
return {
|
|
383
403
|
event: async ({ event }) => {
|
|
384
404
|
// A1: v1 migration owned by autorecall.mjs — removed to avoid race
|
|
385
|
-
if (event.type === "session.created") {
|
|
386
|
-
lastCheckpoint.ts = 0
|
|
387
|
-
writtenThisSession.length = 0
|
|
405
|
+
if (event.type === "session.created") {
|
|
406
|
+
lastCheckpoint.ts = 0
|
|
407
|
+
writtenThisSession.length = 0
|
|
408
|
+
try {
|
|
409
|
+
const root = getDataRoot()
|
|
410
|
+
ensureConsistency(root)
|
|
411
|
+
curatorLog("[curator] consistency repair: all memory/runtime dirs verified")
|
|
412
|
+
} catch (err) {
|
|
413
|
+
curatorLog(`[curator] consistency repair failure: ${safeLogMessage(err.message)}`)
|
|
414
|
+
}
|
|
388
415
|
}
|
|
389
416
|
if (event.type === "session.idle") {
|
|
390
417
|
await handleSessionIdle(directory, project)
|
|
@@ -15,7 +15,7 @@ This creates "phantom" compressed data that references stale environments.
|
|
|
15
15
|
{
|
|
16
16
|
"fingerprint": {
|
|
17
17
|
"cwd": "C:/path/to/project",
|
|
18
|
-
"harness_root": "
|
|
18
|
+
"harness_root": "%USERPROFILE%\\.config\\opencode",
|
|
19
19
|
"project_root": "C:/path/to/project",
|
|
20
20
|
"project": "my-project",
|
|
21
21
|
"session_id": "session-123",
|
package/index.mjs
CHANGED
|
@@ -25,7 +25,9 @@ export default async (input) => {
|
|
|
25
25
|
OhcPlugin(input),
|
|
26
26
|
UpdaterPlugin(input),
|
|
27
27
|
])
|
|
28
|
-
const [
|
|
28
|
+
const pluginNames = ["Bootstrap", "Autorecall", "Curator", "SkillBuilder", "MemoryTools", "AmbientMemory", "Ohc", "Updater"]
|
|
29
|
+
results.forEach((r, i) => { if (r.status === "rejected") console.error(`[openhermes] ${pluginNames[i]} plugin failed:`, r.reason) })
|
|
30
|
+
const [bootstrap, autorecall, curator, skillBuilder, memoryTools, ambient, ohc, updater] = results.map(r => r.status === "fulfilled" ? r.value : {})
|
|
29
31
|
|
|
30
32
|
const merged = {}
|
|
31
33
|
|
package/lib/handoff.mjs
CHANGED
|
@@ -2,11 +2,6 @@
|
|
|
2
2
|
// Convention-based, no runtime deps. Agents import and call.
|
|
3
3
|
|
|
4
4
|
let _idSeq = 0
|
|
5
|
-
const COMPLEXITY_RULES = [
|
|
6
|
-
{ maxFiles: 2, patterns: 0, label: "easy" },
|
|
7
|
-
{ maxFiles: 10, patterns: 0, label: "medium" },
|
|
8
|
-
{ maxFiles: 60, patterns: 0, label: "hard" },
|
|
9
|
-
]
|
|
10
5
|
|
|
11
6
|
function nextId() {
|
|
12
7
|
_idSeq++
|
package/lib/hardening.mjs
CHANGED
|
@@ -3,10 +3,11 @@ import fs from "node:fs"
|
|
|
3
3
|
import os from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
|
|
6
|
-
const SECRET_KEY_PATTERN = /(token|secret|password|passwd|passphrase|api[-_ ]?key|access[-_ ]?key|refresh[-_ ]?token|authorization|cookie|bearer)/i
|
|
6
|
+
const SECRET_KEY_PATTERN = /(token|secret|password|passwd|passphrase|pwd|credential|auth|api[-_ ]?key|access[-_ ]?key|refresh[-_ ]?token|authorization|cookie|bearer)/i
|
|
7
7
|
const TEXT_REDACTIONS = [
|
|
8
8
|
[/\bsk-[A-Za-z0-9]{16,}\b/g, "[REDACTED]"],
|
|
9
9
|
[/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[REDACTED]"],
|
|
10
|
+
[/\bgithub_pat_[A-Za-z0-9_]+\b/g, "[REDACTED]"],
|
|
10
11
|
[/\bxox[baprs]-[A-Za-z0-9-]+\b/g, "[REDACTED]"],
|
|
11
12
|
[/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi, "Bearer [REDACTED]"],
|
|
12
13
|
[/\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g, "[REDACTED]"],
|
|
@@ -111,7 +112,11 @@ function readJson(fp, fallback) {
|
|
|
111
112
|
|
|
112
113
|
function readJsonl(fp) {
|
|
113
114
|
try {
|
|
114
|
-
|
|
115
|
+
const results = []
|
|
116
|
+
for (const l of fs.readFileSync(fp, "utf8").trim().split("\n").filter(Boolean)) {
|
|
117
|
+
try { results.push(JSON.parse(l)) } catch {}
|
|
118
|
+
}
|
|
119
|
+
return results
|
|
115
120
|
} catch { return [] }
|
|
116
121
|
}
|
|
117
122
|
|
|
@@ -125,4 +130,17 @@ function buildEnvironmentFingerprint(root, directory, project) {
|
|
|
125
130
|
})
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
function toDisplayPath(absPath) {
|
|
134
|
+
if (!absPath) return ''
|
|
135
|
+
const normalized = absPath.replace(/\\/g, '/')
|
|
136
|
+
const idx = normalized.lastIndexOf('/openhermes/')
|
|
137
|
+
if (idx !== -1) return 'openhermes' + normalized.slice(idx + 11)
|
|
138
|
+
const parts = normalized.split('/').filter(Boolean)
|
|
139
|
+
return parts.slice(-2).join('/')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function branded(prefix, message) {
|
|
143
|
+
return `OpenHermes${prefix ? ` ${prefix}` : ''}: ${message}`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { atomicWriteJson, branded, buildEnvironmentFingerprint, fingerprintEnvironment, fingerprintFile, isPlainObject, isTruthy, readJson, readJsonl, redactSensitiveText, sanitizeRecord, toDisplayPath, truncateText }
|
|
@@ -37,6 +37,11 @@ function buildEntry(cls, r) {
|
|
|
37
37
|
const e = { id: r.id, summary: r.summary, status: r.status, updated_at: r.updated_at ?? r.created_at, path: path.join("openhermes", "memory", PLURALS[cls], `${r.id}.json`), scope: r.scope ?? null, project: r.project ?? null }
|
|
38
38
|
if (cls === "audit") { e.target = r.target; e.overall_score = r.overall_score }
|
|
39
39
|
if (cls === "backlog") { e.priority = r.priority; e.trigger = r.trigger }
|
|
40
|
+
if (cls === "decision") { e.choice = r.choice; e.reason = r.reason }
|
|
41
|
+
if (cls === "constraint") { e.description = r.description }
|
|
42
|
+
if (cls === "instinct") { e.source = r.source }
|
|
43
|
+
if (cls === "checkpoint") { e.mission = r.mission }
|
|
44
|
+
if (cls === "verification_receipt") { e.artifact = r.artifact; e.result = r.result }
|
|
40
45
|
return e
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -58,6 +63,7 @@ function sortRecent(entries) {
|
|
|
58
63
|
|
|
59
64
|
function filterActive(entries) { return entries.filter(e => !hasExpired(e)) }
|
|
60
65
|
|
|
66
|
+
// CAUTION: read-modify-write — safe within single session; concurrent sessions may race
|
|
61
67
|
function writeObject(cls, record) {
|
|
62
68
|
const dir = classDir(cls)
|
|
63
69
|
fs.mkdirSync(dir, { recursive: true })
|
|
@@ -80,7 +86,9 @@ function upsertMistake(record) {
|
|
|
80
86
|
const idx = entries.findIndex(e => e?.id === record.id)
|
|
81
87
|
if (idx >= 0) entries[idx] = record; else entries.push(record)
|
|
82
88
|
const text = entries.map(e => stableStringify(e)).join("\n")
|
|
83
|
-
|
|
89
|
+
const tmp = fp + ".tmp"
|
|
90
|
+
fs.writeFileSync(tmp, text ? `${text}\n` : "", "utf8")
|
|
91
|
+
fs.renameSync(tmp, fp)
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
function queryList(cls, limit = 10) {
|
|
@@ -174,10 +182,6 @@ function handleAdd(cls, id, dataStr) {
|
|
|
174
182
|
if (!prov.file_refs.some(r => typeof r === "string" && r.trim())) prov.file_refs.push("auto-filled")
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
|
-
if (required.includes("trigger") && !record.trigger) record.trigger = "manual"
|
|
178
|
-
if (required.includes("success_count") && record.success_count == null) record.success_count = 0
|
|
179
|
-
if (required.includes("failure_count") && record.failure_count == null) record.failure_count = 0
|
|
180
|
-
if (required.includes("promotion_state") && !record.promotion_state) record.promotion_state = "project"
|
|
181
185
|
|
|
182
186
|
const unsupported = findUnsupportedSchemaKeywords(schema)
|
|
183
187
|
if (unsupported.length) return `unsupported schema: ${unsupported.join(", ")}`
|
|
@@ -211,7 +215,7 @@ function handleLatest(cls) {
|
|
|
211
215
|
const list = queryList(cls, 100)
|
|
212
216
|
const active = filterActive(list)
|
|
213
217
|
if (!active[0]?.id) return "no active records"
|
|
214
|
-
const record = queryGet(cls, active[0].id)
|
|
218
|
+
const record = cls === "mistake" ? active[0] : queryGet(cls, active[0].id)
|
|
215
219
|
if (!record) return "no active records"
|
|
216
220
|
return stableStringify(record)
|
|
217
221
|
}
|
|
@@ -246,13 +250,12 @@ function handleSearch(query, scope, classes, project, limit) {
|
|
|
246
250
|
.sort((a, b) => b.score - a.score)
|
|
247
251
|
.slice(0, lim)
|
|
248
252
|
|
|
249
|
-
// Fetch full records for top results to get richer fields
|
|
250
253
|
const results = scored.map(e => {
|
|
251
254
|
if (e._cls === "mistake") return e
|
|
252
255
|
const full = queryGet(e._cls, e.id)
|
|
253
|
-
return full
|
|
256
|
+
return full ? { ...full, score: e.score } : e
|
|
254
257
|
})
|
|
255
|
-
|
|
258
|
+
|
|
256
259
|
const lines = results.map(e => ` ${e.id} (${e.score}pt): ${truncateText(e.summary || "", 80)}`)
|
|
257
260
|
return `${results.length} result${results.length === 1 ? "" : "s"} for '${q}'\n` + lines.join("\n")
|
|
258
261
|
}
|
|
@@ -13,7 +13,7 @@ export function allocateRunId(state) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function wrapBlockSummary(blockId, summary) {
|
|
16
|
-
return `[
|
|
16
|
+
return `[Compressed bk${blockId}]\n\n${summary}\n<ohc-ref>bk${blockId}</ohc-ref>`
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function applyCompressionState(state, input, selection, anchorMessageId, blockId, storedSummary, consumedBlockIds) {
|
package/lib/ohc/config.mjs
CHANGED
|
@@ -16,8 +16,7 @@ const DEFAULTS = {
|
|
|
16
16
|
turnProtection: { enabled: false, turns: 4 },
|
|
17
17
|
protectedFilePatterns: [],
|
|
18
18
|
compress: {
|
|
19
|
-
|
|
20
|
-
minContextLimit: 50000,
|
|
19
|
+
|
|
21
20
|
nudgeFrequency: 5,
|
|
22
21
|
iterationNudgeThreshold: 15,
|
|
23
22
|
nudgeForce: "soft",
|
|
@@ -99,8 +98,6 @@ function mergeTurnProtection(base, override) {
|
|
|
99
98
|
function mergeCompress(base, override) {
|
|
100
99
|
if (!override || typeof override !== "object") return base
|
|
101
100
|
return {
|
|
102
|
-
maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,
|
|
103
|
-
minContextLimit: override.minContextLimit ?? base.minContextLimit,
|
|
104
101
|
nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
|
|
105
102
|
iterationNudgeThreshold: override.iterationNudgeThreshold ?? base.iterationNudgeThreshold,
|
|
106
103
|
nudgeForce: override.nudgeForce ?? base.nudgeForce,
|
|
@@ -175,6 +172,8 @@ export function loadConfig(ctx) {
|
|
|
175
172
|
config.max = typeof config.max === "number" && config.max > 0 ? config.max : DEFAULTS.max
|
|
176
173
|
config.min = typeof config.min === "number" ? Math.max(10000, Math.min(config.min, config.max - 10000)) : DEFAULTS.min
|
|
177
174
|
|
|
175
|
+
try { writeDefaults() } catch { /* best-effort */ }
|
|
176
|
+
|
|
178
177
|
return config
|
|
179
178
|
}
|
|
180
179
|
|
package/lib/ohc/message-ids.mjs
CHANGED
|
@@ -114,6 +114,16 @@ function allocateNextRef(state) {
|
|
|
114
114
|
return formatMessageRef(1)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export function cleanupMessageRefs(state, removedRawIds) {
|
|
118
|
+
for (const rawId of removedRawIds) {
|
|
119
|
+
const ref = state.messageIds.byRawId.get(rawId)
|
|
120
|
+
if (ref) {
|
|
121
|
+
state.messageIds.byRawId.delete(rawId)
|
|
122
|
+
state.messageIds.byRef.delete(ref)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
export function injectMessageIds(state, messages) {
|
|
118
128
|
for (const msg of messages) {
|
|
119
129
|
if (isIgnoredUserMessage(msg)) continue
|
package/lib/ohc/notify.mjs
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
1
|
function formatTokenCount(tokens) {
|
|
2
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`.replace(".0M", "M")
|
|
2
3
|
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}K`.replace(".0K", "K")
|
|
3
4
|
return String(tokens)
|
|
4
5
|
}
|
|
5
6
|
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (total === 0) return `\u2502${"\u2591".repeat(width)}\u2502 0% active`
|
|
10
|
-
const activeRatio = currentMessageCount / total
|
|
11
|
-
const activeW = Math.round(activeRatio * width)
|
|
12
|
-
const prunedW = width - activeW
|
|
13
|
-
const bar = "\u2588".repeat(Math.min(activeW, width)) + "\u2591".repeat(Math.min(prunedW, width))
|
|
14
|
-
return `\u2502${bar.slice(0, width)}\u2502 ${Math.round(activeRatio * 100)}% active`
|
|
7
|
+
function truncate(str, max) {
|
|
8
|
+
if (!str) return ""
|
|
9
|
+
return str.length > max ? str.slice(0, max) + "\u2026" : str
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
function
|
|
18
|
-
const
|
|
19
|
-
|
|
12
|
+
function buildCompressionDetailed(count, savedTotal, blockCount, summary) {
|
|
13
|
+
const parts = [
|
|
14
|
+
`${formatTokenCount(savedTotal)} saved \u00b7 ${count} msg${count === 1 ? "" : "s"} removed \u00b7 #${blockCount} block${blockCount === 1 ? "" : "s"}`,
|
|
15
|
+
]
|
|
16
|
+
if (summary) parts.push(truncate(summary, 48))
|
|
17
|
+
return parts.join("\n")
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
let msg = `\u25A3 OHC | ~${formatTokenCount(savedTotal)} saved total`
|
|
25
|
-
if (totalMessagesRemoved + currentMessageCount > 0) {
|
|
26
|
-
msg += `\n\n${buildProgressBar(totalMessagesRemoved, currentMessageCount)}`
|
|
27
|
-
}
|
|
28
|
-
msg += `\n\n\u25A3 ${label} #${blockCount}`
|
|
29
|
-
msg += `\n\u2192 ${count} message${count === 1 ? "" : "s"} removed`
|
|
30
|
-
if (summary) msg += `\n\u2192 Summary: ${summary}`
|
|
31
|
-
return msg
|
|
20
|
+
function buildCompressionMinimal(count, savedTotal, blockCount) {
|
|
21
|
+
return `${formatTokenCount(savedTotal)} saved \u00b7 ${blockCount} block${blockCount === 1 ? "" : "s"} \u00b7 ${count} msg${count === 1 ? "" : "s"} removed`
|
|
32
22
|
}
|
|
33
23
|
|
|
34
|
-
function
|
|
35
|
-
|
|
24
|
+
function buildMemoryMessage(action, cls, id, summary) {
|
|
25
|
+
const idPart = id ? truncate(id, 28) : ""
|
|
26
|
+
return [action, cls, idPart].filter(Boolean).join(" \u00b7 ")
|
|
36
27
|
}
|
|
37
28
|
|
|
38
29
|
export async function sendCompressNotification(client, sessionId, config, count, summary, tokensRemoved, ss, currentMessageCount) {
|
|
@@ -40,21 +31,20 @@ export async function sendCompressNotification(client, sessionId, config, count,
|
|
|
40
31
|
|
|
41
32
|
const savedTotal = ss?.totalTokensSaved || 0
|
|
42
33
|
const blockCount = ss?.blockCount || 0
|
|
43
|
-
const totalMessagesRemoved = ss?.totalMessagesRemoved || 0
|
|
44
34
|
|
|
45
|
-
const notifType = config.notification ?? "
|
|
46
|
-
const notifMode = config.notificationMode ?? "
|
|
35
|
+
const notifType = config.notification ?? "chat"
|
|
36
|
+
const notifMode = config.notificationMode ?? "detailed"
|
|
47
37
|
|
|
48
38
|
if (notifType === "off") return false
|
|
49
39
|
|
|
50
40
|
if (notifType === "toast") {
|
|
51
41
|
const message = notifMode === "minimal"
|
|
52
|
-
?
|
|
53
|
-
:
|
|
42
|
+
? buildCompressionMinimal(count, savedTotal, blockCount)
|
|
43
|
+
: buildCompressionDetailed(count, savedTotal, blockCount, summary)
|
|
54
44
|
try {
|
|
55
45
|
await client.tui.showToast({
|
|
56
46
|
body: {
|
|
57
|
-
title: "OHC
|
|
47
|
+
title: "OHC Compression",
|
|
58
48
|
message,
|
|
59
49
|
variant: "info",
|
|
60
50
|
duration: 5000,
|
|
@@ -64,12 +54,15 @@ export async function sendCompressNotification(client, sessionId, config, count,
|
|
|
64
54
|
return true
|
|
65
55
|
}
|
|
66
56
|
|
|
57
|
+
const message = notifMode === "minimal"
|
|
58
|
+
? buildCompressionMinimal(count, savedTotal, blockCount)
|
|
59
|
+
: buildCompressionDetailed(count, savedTotal, blockCount, summary)
|
|
67
60
|
try {
|
|
68
61
|
await client.session.prompt({
|
|
69
62
|
path: { id: sessionId },
|
|
70
63
|
body: {
|
|
71
64
|
noReply: true,
|
|
72
|
-
parts: [{ type: "text", text:
|
|
65
|
+
parts: [{ type: "text", text: `OHC: ${message}`, ignored: true }],
|
|
73
66
|
},
|
|
74
67
|
})
|
|
75
68
|
} catch {}
|
|
@@ -78,16 +71,16 @@ export async function sendCompressNotification(client, sessionId, config, count,
|
|
|
78
71
|
|
|
79
72
|
export async function sendStrategyNotification(client, sessionId, config, strategy, count, detail) {
|
|
80
73
|
if (count === 0) return false
|
|
81
|
-
const notifType = config.notification ?? "
|
|
74
|
+
const notifType = config.notification ?? "chat"
|
|
82
75
|
if (notifType === "off") return false
|
|
83
76
|
|
|
84
|
-
const message =
|
|
77
|
+
const message = detail ? `${strategy} \u2014 ${count} pruned \u00b7 ${truncate(detail, 36)}` : `${strategy} \u2014 ${count} pruned`
|
|
85
78
|
|
|
86
79
|
if (notifType === "toast") {
|
|
87
80
|
try {
|
|
88
81
|
await client.tui.showToast({
|
|
89
82
|
body: {
|
|
90
|
-
title:
|
|
83
|
+
title: "OHC Strategy",
|
|
91
84
|
message,
|
|
92
85
|
variant: "info",
|
|
93
86
|
duration: 3000,
|
|
@@ -102,7 +95,7 @@ export async function sendStrategyNotification(client, sessionId, config, strate
|
|
|
102
95
|
path: { id: sessionId },
|
|
103
96
|
body: {
|
|
104
97
|
noReply: true,
|
|
105
|
-
parts: [{ type: "text", text: message
|
|
98
|
+
parts: [{ type: "text", text: `OHC: ${message}`, ignored: true }],
|
|
106
99
|
},
|
|
107
100
|
})
|
|
108
101
|
} catch {}
|
|
@@ -111,27 +104,15 @@ export async function sendStrategyNotification(client, sessionId, config, strate
|
|
|
111
104
|
|
|
112
105
|
export async function sendMemoryNotification(client, sessionId, config, action, cls, id, summary) {
|
|
113
106
|
const notifType = config.notification ?? "toast"
|
|
114
|
-
const notifMode = config.notificationMode ?? "minimal"
|
|
115
|
-
|
|
116
107
|
if (notifType === "off") return false
|
|
117
108
|
|
|
118
|
-
const
|
|
119
|
-
const buildDetailed = (action, cls, id, summary) => {
|
|
120
|
-
let msg = `\u25A3 Memory ${action}`
|
|
121
|
-
if (cls) msg += `\n\u2192 Class: ${cls}`
|
|
122
|
-
if (id) msg += `\n\u2192 ID: ${id}`
|
|
123
|
-
if (summary) msg += `\n\u2192 ${summary}`
|
|
124
|
-
return msg
|
|
125
|
-
}
|
|
109
|
+
const message = buildMemoryMessage(action, cls, id, summary)
|
|
126
110
|
|
|
127
111
|
if (notifType === "toast") {
|
|
128
|
-
const message = notifMode === "minimal"
|
|
129
|
-
? buildHeader(action, summary)
|
|
130
|
-
: buildDetailed(action, cls, id, summary)
|
|
131
112
|
try {
|
|
132
113
|
await client.tui.showToast({
|
|
133
114
|
body: {
|
|
134
|
-
title: "Memory",
|
|
115
|
+
title: "OHC Memory",
|
|
135
116
|
message,
|
|
136
117
|
variant: "info",
|
|
137
118
|
duration: 4000,
|
|
@@ -146,7 +127,7 @@ export async function sendMemoryNotification(client, sessionId, config, action,
|
|
|
146
127
|
path: { id: sessionId },
|
|
147
128
|
body: {
|
|
148
129
|
noReply: true,
|
|
149
|
-
parts: [{ type: "text", text:
|
|
130
|
+
parts: [{ type: "text", text: `OHC: ${message}`, ignored: true }],
|
|
150
131
|
},
|
|
151
132
|
})
|
|
152
133
|
} catch {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const DEFAULT_PROTECTED_TOOLS = new Set([
|
|
2
2
|
"task", "skill", "todowrite", "todoread",
|
|
3
3
|
"compress", "batch", "plan_enter", "plan_exit",
|
|
4
|
-
"write", "edit",
|
|
4
|
+
"write", "edit", "bash", "webfetch",
|
|
5
5
|
])
|
|
6
6
|
|
|
7
7
|
export function isToolNameProtected(toolName, extraProtected = []) {
|
|
@@ -34,7 +34,8 @@ export function isFilePathProtected(filePaths, protectedPatterns = []) {
|
|
|
34
34
|
function globMatch(filePath, pattern) {
|
|
35
35
|
const regexStr = pattern
|
|
36
36
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
37
|
-
.replace(
|
|
37
|
+
.replace(/\*\*/g, ".*")
|
|
38
|
+
.replace(/\*/g, "[^/]*")
|
|
38
39
|
.replace(/\?/g, ".")
|
|
39
40
|
try {
|
|
40
41
|
return new RegExp(`^${regexStr}$`, "i").test(filePath)
|
package/lib/ohc/pruner.mjs
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
} from "./state.mjs"
|
|
9
9
|
import { sendCompressNotification } from "./notify.mjs"
|
|
10
10
|
import { deduplicate, purgeErrors } from "./strategies/index.mjs"
|
|
11
|
-
import { applyPruneTools, filterCompressedBlocks } from "./prune-apply.mjs"
|
|
12
|
-
import { assignMessageRefs, injectMessageIds } from "./message-ids.mjs"
|
|
11
|
+
import { applyPruneTools, filterCompressedBlocks, applyFullToolRemoval } from "./prune-apply.mjs"
|
|
12
|
+
import { assignMessageRefs, injectMessageIds, cleanupMessageRefs } from "./message-ids.mjs"
|
|
13
13
|
import { syncCompressionBlocks } from "./block-sync.mjs"
|
|
14
14
|
import { buildSearchContext, resolveBoundaryIds, resolveSelection, resolveAnchorMessageId, validateNonOverlapping } from "./compress/search.mjs"
|
|
15
15
|
import { allocateBlockId, allocateRunId, wrapBlockSummary, applyCompressionState } from "./compress/state.mjs"
|
|
@@ -38,16 +38,16 @@ function getSessionId(messages) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function buildNudge(pct, max) {
|
|
41
|
-
if (pct > 0.95) return `
|
|
42
|
-
if (pct > 0.85) return `
|
|
43
|
-
if (pct > 0.70) return `
|
|
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
44
|
return null
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function summarizeRemoved(selected, summary) {
|
|
48
48
|
const n = selected.length
|
|
49
|
-
if (summary) return `[
|
|
50
|
-
return `[Auto-pruned
|
|
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
51
|
}
|
|
52
52
|
|
|
53
53
|
function createSummaryMessage(text) {
|
|
@@ -99,7 +99,7 @@ function estimateSummaryTokens(messages) {
|
|
|
99
99
|
for (const m of messages) {
|
|
100
100
|
if (m.info?.role === "system" && Array.isArray(m.parts)) {
|
|
101
101
|
for (const p of m.parts) {
|
|
102
|
-
if (p.type === "text" && p.text?.startsWith("[Compressed
|
|
102
|
+
if (p.type === "text" && (p.text?.startsWith("[OHC: Compressed]") || p.text?.startsWith("[OHC: Auto-pruned]"))) {
|
|
103
103
|
total += Math.ceil(p.text.length / 4)
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -209,7 +209,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
209
209
|
const total = ss.compressionTiming.totalDurationMs || 0
|
|
210
210
|
const lastSec = (last / 1000).toFixed(1)
|
|
211
211
|
const totalSec = (total / 1000).toFixed(1)
|
|
212
|
-
return
|
|
212
|
+
return ` (last: ${lastSec}s, total: ${totalSec}s)`
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
function computeRoleBreakdown(messages) {
|
|
@@ -248,11 +248,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
248
248
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
249
249
|
if (systemInjected || !output.system?.length) return
|
|
250
250
|
systemInjected = true
|
|
251
|
-
|
|
252
|
-
const protectedToolsList = (config.compress?.protectedTools || []).length
|
|
253
|
-
? ` Protected tools (${(config.compress.protectedTools).join(", ")}) are preserved.`
|
|
254
|
-
: ``
|
|
255
|
-
output.system[output.system.length - 1] += `\n\n## OHC Context Management\n- OHC manages all compression. Set \`compaction.auto: false\` in opencode.json.\n- Default budget: ${max.toLocaleString()} tokens. Floor: ${min.toLocaleString()}.${summaryBufNote}${protectedToolsList}\n\n### Compress Tool\nUse \`compress\` to free context space. Two modes:\n- **Legacy**: \`{ summary }\` — oldest messages first\n- **Range**: \`{ topic, content: [{startId, endId, summary}] }\` — target specific ranges\n - \`startId\` / \`endId\`: \`ohcNNNN\` (message) or \`bkNN\` (block)\n - Each message in context has an \`<ohc-ref>\` tag with its ID\n - Ranges must be non-overlapping in one call\n - Summary replaces the entire range`
|
|
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`
|
|
256
252
|
},
|
|
257
253
|
|
|
258
254
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
@@ -276,6 +272,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
276
272
|
deduplicate(ss, config, output.messages)
|
|
277
273
|
purgeErrors(ss, config, output.messages)
|
|
278
274
|
applyPruneTools(ss, output.messages)
|
|
275
|
+
applyFullToolRemoval(ss, output.messages)
|
|
279
276
|
|
|
280
277
|
const now = Date.now()
|
|
281
278
|
const recentlyPruned = ss.lastAutoPruneAt && (now - ss.lastAutoPruneAt) < AUTOPRUNE_COOLDOWN
|
|
@@ -283,6 +280,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
283
280
|
if (ss.prunedIds.size > 0 && !recentlyPruned) {
|
|
284
281
|
const currentIds = new Set(output.messages.map(m => m.info?.id).filter(Boolean))
|
|
285
282
|
if ([...ss.prunedIds].every(id => !currentIds.has(id))) {
|
|
283
|
+
cleanupMessageRefs(ss, ss.prunedIds)
|
|
286
284
|
ss.prunedIds.clear()
|
|
287
285
|
ss.summary = null
|
|
288
286
|
ss.anchorMessageId = null
|
|
@@ -358,7 +356,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
358
356
|
const iterationThreshold = config.compress?.iterationNudgeThreshold ?? 15
|
|
359
357
|
const iterCount = countIterationsSinceLastUser(output.messages)
|
|
360
358
|
if (iterCount >= iterationThreshold && iterCount % 5 === 0) {
|
|
361
|
-
const iterNudge = `
|
|
359
|
+
const iterNudge = `OHC: ${iterCount} AI turns since your last input. Summarize with \`compress\`.`
|
|
362
360
|
if (nudgeText) nudgeText += "\n\n" + iterNudge
|
|
363
361
|
else nudgeText = iterNudge
|
|
364
362
|
}
|
|
@@ -431,12 +429,12 @@ export const OhcPlugin = async (ctx) => {
|
|
|
431
429
|
const strategyPruned = ss?.prune?.tools?.size || 0
|
|
432
430
|
const timing = buildTimingStr(ss)
|
|
433
431
|
const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
|
|
434
|
-
const blockLine = activeBlockIds.length ? `
|
|
432
|
+
const blockLine = activeBlockIds.length ? ` bk${activeBlockIds.join(", bk")}.` : ""
|
|
435
433
|
const summaryBufferTotal = config.compress?.summaryBuffer
|
|
436
434
|
? estimateSummaryTokens(msgs)
|
|
437
435
|
: 0
|
|
438
436
|
const effectiveMax = max + summaryBufferTotal
|
|
439
|
-
const text = `
|
|
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}`
|
|
440
438
|
await ctx.client.session.prompt({
|
|
441
439
|
path: { id: input.sessionID },
|
|
442
440
|
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
@@ -452,8 +450,8 @@ export const OhcPlugin = async (ctx) => {
|
|
|
452
450
|
const autoPruned = ss?.prunedIds?.size || 0
|
|
453
451
|
const timing = buildTimingStr(ss)
|
|
454
452
|
const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
|
|
455
|
-
const blockLine = activeBlockIds.length ? `
|
|
456
|
-
const text = `
|
|
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}`
|
|
457
455
|
await ctx.client.session.prompt({
|
|
458
456
|
path: { id: input.sessionID },
|
|
459
457
|
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
@@ -468,14 +466,14 @@ export const OhcPlugin = async (ctx) => {
|
|
|
468
466
|
if (ss) ss.manualMode = "active"
|
|
469
467
|
await ctx.client.session.prompt({
|
|
470
468
|
path: { id: input.sessionID },
|
|
471
|
-
body: { noReply: true, parts: [{ type: "text", text: "
|
|
469
|
+
body: { noReply: true, parts: [{ type: "text", text: "OHC Manual: active \u2014 agent will not autonomously compress", ignored: true }] },
|
|
472
470
|
})
|
|
473
471
|
} else {
|
|
474
472
|
const ss = getOrCreateState(input.sessionID)
|
|
475
473
|
if (ss) ss.manualMode = false
|
|
476
474
|
await ctx.client.session.prompt({
|
|
477
475
|
path: { id: input.sessionID },
|
|
478
|
-
body: { noReply: true, parts: [{ type: "text", text: "
|
|
476
|
+
body: { noReply: true, parts: [{ type: "text", text: "OHC Manual: off \u2014 agent can compress autonomously", ignored: true }] },
|
|
479
477
|
})
|
|
480
478
|
}
|
|
481
479
|
throw new Error("__OHC_MANUAL_HANDLED__")
|
|
@@ -499,7 +497,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
499
497
|
output.parts.length = 0
|
|
500
498
|
output.parts.push({
|
|
501
499
|
type: "text",
|
|
502
|
-
text: `
|
|
500
|
+
text: `OHC: Compressed ${result.removed} msgs. Summary: ${focus}`,
|
|
503
501
|
})
|
|
504
502
|
} catch {
|
|
505
503
|
output.parts.length = 0
|
|
@@ -533,8 +531,8 @@ export const OhcPlugin = async (ctx) => {
|
|
|
533
531
|
}
|
|
534
532
|
|
|
535
533
|
const activeBlockIds = [...(ss?.prune?.messages?.activeBlockIds || [])].filter(id => Number.isInteger(id)).sort((a, b) => a - b)
|
|
536
|
-
const blockLine = activeBlockIds.length ? `
|
|
537
|
-
const text = `
|
|
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")}`
|
|
538
536
|
await ctx.client.session.prompt({
|
|
539
537
|
path: { id: input.sessionID },
|
|
540
538
|
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
@@ -559,13 +557,13 @@ export const OhcPlugin = async (ctx) => {
|
|
|
559
557
|
}
|
|
560
558
|
}
|
|
561
559
|
applyPruneTools(ss, output.messages)
|
|
562
|
-
const text = `
|
|
560
|
+
const text = `OHC Sweep: ${sweptCount} tool call${sweptCount === 1 ? "" : "s"} pruned`
|
|
563
561
|
output.parts.length = 0
|
|
564
562
|
output.parts.push({ type: "text", text })
|
|
565
563
|
return
|
|
566
564
|
}
|
|
567
565
|
|
|
568
|
-
const text = "OHC
|
|
566
|
+
const text = "OHC: /ohc status | stats | context | sweep [n] | manual [on|off] | compress [focus]"
|
|
569
567
|
await ctx.client.session.prompt({
|
|
570
568
|
path: { id: input.sessionID },
|
|
571
569
|
body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
|
|
@@ -575,7 +573,7 @@ export const OhcPlugin = async (ctx) => {
|
|
|
575
573
|
|
|
576
574
|
tool: {
|
|
577
575
|
compress: tool({
|
|
578
|
-
description: "Compress conversation content to free context space.
|
|
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.",
|
|
579
577
|
args: {
|
|
580
578
|
topic: tool.schema.string().optional().describe("Range mode: Short label (3-5 words) for the overall batch — e.g. 'Auth System Exploration'"),
|
|
581
579
|
content: tool.schema.array(tool.schema.object({
|
|
@@ -595,14 +593,14 @@ export const OhcPlugin = async (ctx) => {
|
|
|
595
593
|
toolCtx.metadata({ title: "Compress Range" })
|
|
596
594
|
const resultSs = getOrCreateState(sessionId)
|
|
597
595
|
await sendCompressNotification(ctx.client, sessionId, config, result.messageIds.length, result.summaryRef, result.compressedTokens, resultSs, result.afterCount || 0)
|
|
598
|
-
return `Compressed ${result.messageIds.length}
|
|
596
|
+
return `OHC: Compressed ${result.messageIds.length} msgs across ${args.content.length} range(s). Summary: "${truncateText(result.summaryRef, 200)}"`
|
|
599
597
|
}
|
|
600
598
|
|
|
601
599
|
const result = await applyCompress(ctx, sessionId, args.summary, max, min, args.targetTokens)
|
|
602
600
|
toolCtx.metadata({ title: "Compress" })
|
|
603
601
|
const toolSs = getOrCreateState(sessionId)
|
|
604
602
|
await sendCompressNotification(ctx.client, sessionId, config, result.removed, truncateText(args.summary, 200), result.tokensRemoved, toolSs, result.afterCount)
|
|
605
|
-
return `
|
|
603
|
+
return `OHC: Compressed ${result.removed} msgs. Summary: "${truncateText(args.summary, 200)}"`
|
|
606
604
|
},
|
|
607
605
|
}),
|
|
608
606
|
},
|
package/lib/ohc/reaper.mjs
CHANGED
|
@@ -7,7 +7,7 @@ function partTokens(part) {
|
|
|
7
7
|
t += (typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output ?? "")).length / 4
|
|
8
8
|
return Math.ceil(t)
|
|
9
9
|
}
|
|
10
|
-
return Math.ceil(JSON.stringify(part).length / 4)
|
|
10
|
+
try { return Math.ceil(JSON.stringify(part).length / 4) } catch { return 0 }
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function msgTokens(msg) {
|
package/lib/ohc/state.mjs
CHANGED
|
@@ -150,6 +150,7 @@ export function serializeState(state) {
|
|
|
150
150
|
},
|
|
151
151
|
compressionTiming: {
|
|
152
152
|
starts: Object.fromEntries(state.compressionTiming?.starts || new Map()),
|
|
153
|
+
pendingByCallId: pruneMapToObj(state.compressionTiming?.pendingByCallId || new Map()),
|
|
153
154
|
lastDurationMs: state.compressionTiming?.lastDurationMs || 0,
|
|
154
155
|
totalDurationMs: state.compressionTiming?.totalDurationMs || 0,
|
|
155
156
|
},
|
|
@@ -216,12 +217,10 @@ export function buildToolIdList(state, messages) {
|
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
export function syncToolCache(state, messages) {
|
|
219
|
-
let
|
|
220
|
+
let userTurn = 0
|
|
220
221
|
for (const msg of messages) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (lastUser) maxTurn++
|
|
224
|
-
}
|
|
222
|
+
const hasUserText = msg.info?.role === "user" && msg.parts?.some(p => p.type === "text" && p.text?.trim())
|
|
223
|
+
if (hasUserText) userTurn++
|
|
225
224
|
if (!Array.isArray(msg.parts)) continue
|
|
226
225
|
for (const part of msg.parts) {
|
|
227
226
|
if (part.type !== "tool" || !part.callID) continue
|
|
@@ -234,13 +233,13 @@ export function syncToolCache(state, messages) {
|
|
|
234
233
|
tool: part.tool || "unknown",
|
|
235
234
|
parameters: part.state?.input || {},
|
|
236
235
|
status: part.state?.status || "pending",
|
|
237
|
-
turn:
|
|
236
|
+
turn: userTurn,
|
|
238
237
|
tokenCount: estimateToolTokens(part),
|
|
239
238
|
lastSeen: Date.now(),
|
|
240
239
|
})
|
|
241
240
|
}
|
|
242
241
|
}
|
|
243
|
-
state.currentTurn = Math.max(state.currentTurn,
|
|
242
|
+
state.currentTurn = Math.max(state.currentTurn, userTurn)
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
function estimateToolTokens(part) {
|
package/lib/ohc/updater.mjs
CHANGED
|
@@ -87,38 +87,37 @@ async function handleUpdateMe(ctx, input, output) {
|
|
|
87
87
|
output.parts.length = 0
|
|
88
88
|
output.parts.push({
|
|
89
89
|
type: "text",
|
|
90
|
-
text: `[Update-Me] No OpenHermes cache found
|
|
91
|
-
Restart OpenCode to redownload from ${info.method === "git" ? "git HEAD" : "npm registry"}.`,
|
|
90
|
+
text: `[Update-Me] No OpenHermes plugin cache found.\nRestart OpenCode to redownload from ${info.method === "git" ? "git HEAD" : "npm registry"}.`,
|
|
92
91
|
})
|
|
93
92
|
return
|
|
94
93
|
}
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
let clearedCount = 0
|
|
96
|
+
let failedCount = 0
|
|
97
|
+
let firstError = null
|
|
98
98
|
for (const dir of cacheDirs) {
|
|
99
99
|
try {
|
|
100
100
|
fs.rmSync(dir.path, { recursive: true, force: true })
|
|
101
|
-
|
|
101
|
+
clearedCount++
|
|
102
102
|
} catch (e) {
|
|
103
|
-
|
|
103
|
+
failedCount++
|
|
104
|
+
if (!firstError) firstError = e.message
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
output.parts.length = 0
|
|
108
109
|
let msg = `[Update-Me] OpenHermes update (${info.method})\n`
|
|
109
110
|
|
|
110
|
-
if (
|
|
111
|
-
msg += `\
|
|
112
|
-
for (const d of deleted) msg += ` ✓ ${d}\n`
|
|
111
|
+
if (clearedCount > 0) {
|
|
112
|
+
msg += `\n ✓ Cleared OpenHermes plugin cache (${clearedCount} entr${clearedCount === 1 ? "y" : "ies"}).`
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
if (
|
|
116
|
-
msg += `\n⚠
|
|
117
|
-
|
|
118
|
-
msg += `Try deleting manually:\n ${CACHE_ROOTS.join("\n ")}\n`
|
|
115
|
+
if (failedCount > 0) {
|
|
116
|
+
msg += `\n ⚠ ${failedCount} entr${failedCount === 1 ? "y" : "ies"} could not be removed (may be locked).`
|
|
117
|
+
if (firstError) msg += `\n ${firstError}`
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
msg += `\nRestart OpenCode to load the latest OpenHermes from ${info.method === "git" ? "git HEAD" : "npm registry"}.`
|
|
120
|
+
msg += `\n\nRestart OpenCode to load the latest OpenHermes from ${info.method === "git" ? "git HEAD" : "npm registry"}.`
|
|
122
121
|
|
|
123
122
|
output.parts.push({ type: "text", text: msg })
|
|
124
123
|
}
|
package/lib/paths.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import path from "node:path"
|
|
|
2
2
|
import os from "node:os"
|
|
3
3
|
import fs from "node:fs"
|
|
4
4
|
import { fileURLToPath } from "node:url"
|
|
5
|
+
import { isTruthy } from "./hardening.mjs"
|
|
5
6
|
|
|
6
7
|
const HOME = process.env.USERPROFILE || os.homedir()
|
|
7
8
|
const DATA_ROOT = path.join(HOME, ".local", "share", "opencode", "openhermes")
|
|
@@ -45,6 +46,4 @@ export function getSchemaRoot() {
|
|
|
45
46
|
return path.resolve(PKG_DIR, "..", "schemas")
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
return /^(1|true|yes|on)$/i.test(String(value || ""))
|
|
50
|
-
}
|
|
49
|
+
|
package/lib/schema-validator.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import { isPlainObject } from "./hardening.mjs"
|
|
2
|
+
|
|
1
3
|
const SUPPORTED_SCHEMA_KEYS = new Set(["type", "const", "enum", "format", "minimum", "maximum", "required", "properties", "items"])
|
|
2
4
|
const SCHEMA_METADATA_KEYS = new Set(["$schema", "title", "description", "default"])
|
|
3
5
|
|
|
4
|
-
function isPlainObject(value) {
|
|
5
|
-
return !!value && typeof value === "object" && !Array.isArray(value)
|
|
6
|
-
}
|
|
7
|
-
|
|
8
6
|
function matchesType(type, value) {
|
|
9
7
|
if (type === "null") return value === null
|
|
10
8
|
if (type === "array") return Array.isArray(value)
|
|
@@ -26,7 +24,7 @@ function validateNode(schema, value, at, errors) {
|
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
if (typeof value === "string" && schema.format === "date-time" && Number.isNaN(Date.parse(value))) errors.push(`${at} must be a valid date-time string`)
|
|
29
|
-
if (typeof value === "number"
|
|
27
|
+
if (typeof value === "number") {
|
|
30
28
|
if (schema.minimum !== undefined && value < schema.minimum) errors.push(`${at} must be >= ${schema.minimum}`)
|
|
31
29
|
if (schema.maximum !== undefined && value > schema.maximum) errors.push(`${at} must be <= ${schema.maximum}`)
|
|
32
30
|
}
|
|
@@ -64,6 +62,10 @@ function visitSchemaNode(schema, at, unsupported) {
|
|
|
64
62
|
visitSchemaNode(value, `${at}.items`, unsupported)
|
|
65
63
|
continue
|
|
66
64
|
}
|
|
65
|
+
if (key === "$defs" || key === "definitions") {
|
|
66
|
+
if (isPlainObject(value)) for (const defName of Object.keys(value)) visitSchemaNode(value[defName], `${at}.${key}.${defName}`, unsupported)
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
67
69
|
if (!SUPPORTED_SCHEMA_KEYS.has(key) && !SCHEMA_METADATA_KEYS.has(key)) unsupported.push(`${at} uses unsupported schema keyword "${key}"`)
|
|
68
70
|
}
|
|
69
71
|
}
|
package/lib/search.mjs
CHANGED
|
@@ -3,7 +3,7 @@ export function scoreRelevance(r, query, project) {
|
|
|
3
3
|
const tokens = q.split(/\s+/).filter(t => t.length > 2)
|
|
4
4
|
let score = 0
|
|
5
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)
|
|
6
|
+
const primaryFields = [r.summary, r.description, r.mission, r.current_state, r.failure, r.root_cause, r.fix, r.prevention, r.id, r.choice, r.reason, r.source, r.artifact, r.result].filter(Boolean)
|
|
7
7
|
const secondaryFields = [r.command, r.project, r.scope].filter(Boolean)
|
|
8
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
9
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openhermes",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.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",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"archived_at": { "type": ["string", "null"], "format": "date-time", "description": "ISO-8601 or null" },
|
|
38
38
|
"title": { "type": "string", "description": "Human-readable title for this backlog item" },
|
|
39
39
|
"priority": { "type": "string", "enum": ["low", "medium", "high", "critical", "P0", "P1", "P2", "P3", "P4"], "description": "Priority level (low/medium/high/critical or P0-P4)" },
|
|
40
|
-
"trigger": { "type": "string", "enum": ["audit", "mistake", "drift", "user", "manual"], "description": "What triggered creation of this item" },
|
|
40
|
+
"trigger": { "type": "string", "enum": ["audit", "mistake", "drift", "user", "manual"], "default": "manual", "description": "What triggered creation of this item" },
|
|
41
41
|
"evidence_refs": { "type": "array", "items": { "type": "string" }, "description": "References to evidence (audit IDs, mistake IDs, file paths)" },
|
|
42
42
|
"done_when": { "type": "array", "items": { "type": "string" }, "description": "Acceptance criteria — concrete conditions for closure" },
|
|
43
43
|
"description": { "type": "string", "description": "Optional description" },
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"review_at": { "type": ["string", "null"], "format": "date-time", "description": "ISO-8601 or null" },
|
|
36
36
|
"decay_at": { "type": ["string", "null"], "format": "date-time", "description": "ISO-8601 or null" },
|
|
37
37
|
"archived_at": { "type": ["string", "null"], "format": "date-time", "description": "ISO-8601 or null" },
|
|
38
|
-
"trigger": { "type": "string", "description": "Condition or pattern that triggers this instinct" },
|
|
38
|
+
"trigger": { "type": "string", "default": "manual", "description": "Condition or pattern that triggers this instinct" },
|
|
39
39
|
"action": { "type": "string", "description": "Recommended action when triggered" },
|
|
40
40
|
"success_count": { "type": "integer", "minimum": 0, "default": 0, "description": "Number of successful applications" },
|
|
41
41
|
"failure_count": { "type": "integer", "minimum": 0, "default": 0, "description": "Number of failed applications" },
|
|
42
|
-
"promotion_state": { "type": "string", "enum": ["project", "candidate_global", "global"], "description": "Current promotion level" },
|
|
42
|
+
"promotion_state": { "type": "string", "enum": ["project", "candidate_global", "global"], "default": "project", "description": "Current promotion level" },
|
|
43
43
|
"description": { "type": "string", "description": "Optional description" },
|
|
44
44
|
"environment_fingerprint": {
|
|
45
45
|
"type": "object",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"id": { "type": "string", "description": "Unique identifier, recommended pattern: mistake-YYYYMMDD-short-slug" },
|
|
9
9
|
"class": { "type": "string", "const": "mistake" },
|
|
10
10
|
"project": { "type": ["string", "null"], "description": "Project identifier; null for global/openhermes scope" },
|
|
11
|
-
"scope": { "type": "string", "enum": ["project", "global", "harness"], "description": "Scope of applicability" },
|
|
11
|
+
"scope": { "type": "string", "enum": ["project", "global", "session", "harness"], "description": "Scope of applicability" },
|
|
12
12
|
"summary": { "type": "string", "description": "One-line description of the mistake" },
|
|
13
13
|
"tags": { "type": "array", "items": { "type": "string" }, "description": "Searchable tags" },
|
|
14
14
|
"source": { "type": "string", "enum": ["user", "agent", "audit", "migration", "repair"], "description": "Origin of this record" },
|