openhermes 2.6.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +18 -0
- package/ETHOS.md +15 -0
- package/README.md +135 -292
- package/bootstrap.mjs +174 -499
- package/harness/agents/openhermes.md +87 -0
- package/harness/codex/CONSTITUTION.md +70 -148
- package/harness/codex/ROUTING.md +126 -0
- package/harness/commands/oh-doctor.md +26 -0
- package/harness/instructions/CONVENTIONS.md +206 -206
- package/harness/instructions/RUNTIME.md +54 -31
- package/harness/skills/oh-builder/SKILL.md +98 -0
- package/harness/skills/oh-caveman/SKILL.md +33 -0
- package/harness/skills/oh-expert/SKILL.md +121 -0
- package/harness/skills/oh-freeze/SKILL.md +28 -0
- package/harness/skills/oh-gauntlet/SKILL.md +119 -0
- package/harness/skills/oh-grill/SKILL.md +77 -0
- package/harness/skills/oh-guard/SKILL.md +33 -0
- package/harness/skills/oh-handoff/SKILL.md +33 -0
- package/harness/skills/oh-health/SKILL.md +90 -0
- package/harness/skills/oh-init/SKILL.md +78 -0
- package/harness/skills/oh-investigate/SKILL.md +35 -0
- package/harness/skills/oh-issue/SKILL.md +36 -0
- package/harness/skills/oh-learn/SKILL.md +28 -0
- package/harness/skills/oh-manifest/SKILL.md +84 -0
- package/harness/skills/oh-plan-review/SKILL.md +128 -0
- package/harness/skills/oh-planner/SKILL.md +157 -0
- package/harness/skills/oh-prd/SKILL.md +35 -0
- package/harness/skills/oh-retro/SKILL.md +33 -0
- package/harness/skills/oh-review/SKILL.md +110 -0
- package/harness/skills/oh-security/SKILL.md +110 -0
- package/harness/skills/oh-ship/SKILL.md +39 -0
- package/harness/skills/oh-skill-craft/SKILL.md +107 -0
- package/harness/skills/oh-skills-link/SKILL.md +29 -0
- package/harness/skills/oh-skills-list/SKILL.md +31 -0
- package/harness/skills/oh-triage/SKILL.md +36 -0
- package/index.mjs +3 -58
- package/lib/harness-resolver.mjs +77 -0
- package/lib/logger.mjs +62 -0
- package/package.json +49 -53
- package/test/plugins-behavioral.test.mjs +64 -0
- package/test/plugins.test.mjs +62 -0
- package/autorecall.mjs +0 -237
- package/curator.mjs +0 -455
- package/harness/commands/build-fix.md +0 -60
- package/harness/commands/checkpoint.md +0 -68
- package/harness/commands/code-review.md +0 -71
- package/harness/commands/doctor.md +0 -42
- package/harness/commands/eval.md +0 -89
- package/harness/commands/go-build.md +0 -87
- package/harness/commands/go-review.md +0 -71
- package/harness/commands/harness-audit.md +0 -90
- package/harness/commands/learn.md +0 -37
- package/harness/commands/loop-start.md +0 -38
- package/harness/commands/loop-status.md +0 -30
- package/harness/commands/memory-search.md +0 -37
- package/harness/commands/model-route.md +0 -32
- package/harness/commands/ohc.md +0 -13
- package/harness/commands/orchestrate.md +0 -88
- package/harness/commands/plan.md +0 -53
- package/harness/commands/quality-gate.md +0 -35
- package/harness/commands/refactor-clean.md +0 -102
- package/harness/commands/rust-build.md +0 -78
- package/harness/commands/rust-review.md +0 -65
- package/harness/commands/security.md +0 -93
- package/harness/commands/setup-pm.md +0 -65
- package/harness/commands/skill-create.md +0 -99
- package/harness/commands/test-coverage.md +0 -80
- package/harness/commands/update-codemaps.md +0 -81
- package/harness/commands/update-docs.md +0 -67
- package/harness/commands/verify.md +0 -68
- package/harness/prompts/architect.txt +0 -189
- package/harness/prompts/build-cpp.md +0 -98
- package/harness/prompts/build-error-resolver.md +0 -44
- package/harness/prompts/build-go.md +0 -340
- package/harness/prompts/build-java.md +0 -140
- package/harness/prompts/build-kotlin.md +0 -137
- package/harness/prompts/build-rust.md +0 -108
- package/harness/prompts/code-reviewer.md +0 -40
- package/harness/prompts/doc-updater.md +0 -206
- package/harness/prompts/docs-lookup.md +0 -71
- package/harness/prompts/e2e-runner.txt +0 -317
- package/harness/prompts/explore.md +0 -42
- package/harness/prompts/harness-optimizer.md +0 -42
- package/harness/prompts/loop-operator.md +0 -53
- package/harness/prompts/planner.md +0 -37
- package/harness/prompts/refactor-cleaner.md +0 -256
- package/harness/prompts/review-cpp.md +0 -81
- package/harness/prompts/review-database.md +0 -261
- package/harness/prompts/review-go.md +0 -257
- package/harness/prompts/review-java.md +0 -113
- package/harness/prompts/review-kotlin.md +0 -143
- package/harness/prompts/review-python.md +0 -101
- package/harness/prompts/review-rust.md +0 -77
- package/harness/prompts/security-reviewer.md +0 -42
- package/harness/prompts/tdd-guide.md +0 -228
- package/harness/rules/audit.md +0 -84
- package/harness/rules/checkpointing.md +0 -75
- package/harness/rules/context-loading.md +0 -33
- package/harness/rules/credential-exposure.md +0 -0
- package/harness/rules/delegation.md +0 -80
- package/harness/rules/handoff.md +0 -267
- package/harness/rules/memory-management.md +0 -28
- package/harness/rules/precedence.md +0 -52
- package/harness/rules/promotion.md +0 -46
- package/harness/rules/ranking.md +0 -64
- package/harness/rules/retrieval.md +0 -94
- package/harness/rules/runtime-guards.md +0 -196
- package/harness/rules/self-heal.md +0 -79
- package/harness/rules/session-start.md +0 -34
- package/harness/rules/skills-management.md +0 -165
- package/harness/rules/state-drift.md +0 -192
- package/harness/rules/verification.md +0 -88
- package/harness/scripts/sync-commands.mjs +0 -259
- package/harness/skills/.bundled_manifest +0 -17
- package/harness/skills/.usage.json +0 -6
- package/harness/skills/api-design/SKILL.md +0 -523
- package/harness/skills/backend-patterns/SKILL.md +0 -598
- package/harness/skills/coding-standards/SKILL.md +0 -549
- package/harness/skills/e2e-testing/SKILL.md +0 -326
- package/harness/skills/frontend-patterns/SKILL.md +0 -642
- package/harness/skills/frontend-slides/SKILL.md +0 -184
- package/harness/skills/security-review/SKILL.md +0 -495
- package/harness/skills/strategic-compact/SKILL.md +0 -131
- package/harness/skills/tdd-workflow/SKILL.md +0 -463
- package/harness/skills/verification-loop/SKILL.md +0 -126
- package/lib/ambient-memory.mjs +0 -167
- package/lib/handoff.mjs +0 -176
- package/lib/hardening.mjs +0 -128
- package/lib/memory-tools-plugin.mjs +0 -365
- package/lib/ohc/block-sync.mjs +0 -69
- package/lib/ohc/compress/search.mjs +0 -152
- package/lib/ohc/compress/state.mjs +0 -76
- package/lib/ohc/config.mjs +0 -186
- package/lib/ohc/message-ids.mjs +0 -168
- package/lib/ohc/notify.mjs +0 -154
- package/lib/ohc/protected-patterns.mjs +0 -54
- package/lib/ohc/prune-apply.mjs +0 -134
- package/lib/ohc/pruner.mjs +0 -610
- package/lib/ohc/reaper.mjs +0 -70
- package/lib/ohc/state.mjs +0 -266
- package/lib/ohc/strategies/deduplication.mjs +0 -72
- package/lib/ohc/strategies/index.mjs +0 -2
- package/lib/ohc/strategies/purge-errors.mjs +0 -43
- package/lib/ohc/token-utils.mjs +0 -26
- package/lib/ohc/updater.mjs +0 -133
- package/lib/paths.mjs +0 -50
- package/lib/schema-validator.mjs +0 -77
- package/lib/search.mjs +0 -48
- package/schemas/audit.schema.json +0 -82
- package/schemas/backlog.schema.json +0 -63
- package/schemas/checkpoint.schema.json +0 -65
- package/schemas/constraint.schema.json +0 -62
- package/schemas/decision.schema.json +0 -63
- package/schemas/instinct.schema.json +0 -63
- package/schemas/loop-state.schema.json +0 -33
- package/schemas/mistake.schema.json +0 -64
- package/schemas/verification_receipt.schema.json +0 -88
- package/skill-builder.mjs +0 -88
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import { fileURLToPath } from "node:url"
|
|
3
|
-
import path from "node:path"
|
|
4
|
-
import fs from "node:fs"
|
|
5
|
-
import os from "node:os"
|
|
6
|
-
|
|
7
|
-
import { atomicWriteJson, fingerprintEnvironment, readJson, readJsonl, sanitizeRecord, truncateText } from "./hardening.mjs"
|
|
8
|
-
import { findUnsupportedSchemaKeywords, validateSchema } from "./schema-validator.mjs"
|
|
9
|
-
import { getMemoryRoot, getRuntimeRoot } from "./paths.mjs"
|
|
10
|
-
import { loadConfig } from "./ohc/config.mjs"
|
|
11
|
-
import { sendMemoryNotification } from "./ohc/notify.mjs"
|
|
12
|
-
import { scoreRelevance } from "./search.mjs"
|
|
13
|
-
|
|
14
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
15
|
-
const SCHEMAS_DIR = path.resolve(__dirname, "..", "schemas")
|
|
16
|
-
const MEMORY_DIR = getMemoryRoot()
|
|
17
|
-
|
|
18
|
-
const CLASSES = ["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]
|
|
19
|
-
const PLURALS = { audit: "audits", checkpoint: "checkpoints", mistake: "mistakes", instinct: "instincts", decision: "decisions", constraint: "constraints", backlog: "backlog", verification_receipt: "verification_receipts" }
|
|
20
|
-
|
|
21
|
-
function isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArray(v) }
|
|
22
|
-
|
|
23
|
-
function classDir(cls) { return path.join(MEMORY_DIR, PLURALS[cls]) }
|
|
24
|
-
|
|
25
|
-
function stableStringify(v, space = 0) {
|
|
26
|
-
function sort(o) {
|
|
27
|
-
if (Array.isArray(o)) return o.map(sort)
|
|
28
|
-
if (!isPlainObject(o)) return o
|
|
29
|
-
const r = {}
|
|
30
|
-
for (const k of Object.keys(o).sort()) r[k] = sort(o[k])
|
|
31
|
-
return r
|
|
32
|
-
}
|
|
33
|
-
return JSON.stringify(sort(v), null, space)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function buildEntry(cls, r) {
|
|
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
|
-
if (cls === "audit") { e.target = r.target; e.overall_score = r.overall_score }
|
|
39
|
-
if (cls === "backlog") { e.priority = r.priority; e.trigger = r.trigger }
|
|
40
|
-
return e
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function hasExpired(r) {
|
|
44
|
-
if (r?.status === "expired" || r?.status === "decayed") return true
|
|
45
|
-
if (r?.decay_at && Date.parse(r.decay_at) < Date.now()) return true
|
|
46
|
-
if (r?.expires_at && Date.parse(r.expires_at) < Date.now()) return true
|
|
47
|
-
return false
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function sortRecent(entries) {
|
|
51
|
-
function ts(e) { return e?.updated_at ?? e?.created_at ?? "" }
|
|
52
|
-
return [...entries].sort((a, b) => {
|
|
53
|
-
const at = Date.parse(ts(a)), bt = Date.parse(ts(b))
|
|
54
|
-
if (!Number.isNaN(at) && !Number.isNaN(bt) && at !== bt) return bt - at
|
|
55
|
-
return String(ts(b)).localeCompare(String(ts(a)))
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function filterActive(entries) { return entries.filter(e => !hasExpired(e)) }
|
|
60
|
-
|
|
61
|
-
function writeObject(cls, record) {
|
|
62
|
-
const dir = classDir(cls)
|
|
63
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
64
|
-
const fp = path.join(dir, `${record.id}.json`)
|
|
65
|
-
atomicWriteJson(fp, record)
|
|
66
|
-
const indexPath = path.join(dir, "index.json")
|
|
67
|
-
let index = readJson(indexPath, [])
|
|
68
|
-
if (!Array.isArray(index)) index = []
|
|
69
|
-
const idx = index.findIndex(e => e?.id === record.id)
|
|
70
|
-
const entry = buildEntry(cls, record)
|
|
71
|
-
if (idx >= 0) index[idx] = entry; else index.push(entry)
|
|
72
|
-
atomicWriteJson(indexPath, index)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function upsertMistake(record) {
|
|
76
|
-
const dir = classDir("mistake")
|
|
77
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
78
|
-
const fp = path.join(dir, "mistakes.jsonl")
|
|
79
|
-
let entries = readJsonl(fp)
|
|
80
|
-
const idx = entries.findIndex(e => e?.id === record.id)
|
|
81
|
-
if (idx >= 0) entries[idx] = record; else entries.push(record)
|
|
82
|
-
const text = entries.map(e => stableStringify(e)).join("\n")
|
|
83
|
-
fs.writeFileSync(fp, text ? `${text}\n` : "", "utf8")
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function queryList(cls, limit = 10) {
|
|
87
|
-
if (cls === "mistake") {
|
|
88
|
-
return sortRecent(filterActive(readJsonl(path.join(classDir(cls), "mistakes.jsonl")))).slice(0, limit)
|
|
89
|
-
}
|
|
90
|
-
const dir = classDir(cls)
|
|
91
|
-
const index = readJson(path.join(dir, "index.json"), [])
|
|
92
|
-
if (!Array.isArray(index)) return []
|
|
93
|
-
return sortRecent(filterActive(index)).slice(0, limit)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function queryGet(cls, id) {
|
|
97
|
-
if (cls === "mistake") return readJsonl(path.join(classDir(cls), "mistakes.jsonl")).find(e => e?.id === id) ?? null
|
|
98
|
-
return readJson(path.join(classDir(cls), `${id}.json`), null)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function enforceAuditEvidence(record) {
|
|
102
|
-
const prov = isPlainObject(record.provenance) ? record.provenance : {}
|
|
103
|
-
return ["db_refs", "file_refs", "log_refs"].some(k => Array.isArray(prov[k]) && prov[k].some(i => typeof i === "string" && i.trim()))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function setToolTitle(context, title) {
|
|
107
|
-
if (!context || typeof context.metadata !== "function") return
|
|
108
|
-
context.metadata({ title })
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function fillSchemaDefaults(record, schema) {
|
|
112
|
-
const required = Array.isArray(schema.required) ? schema.required : []
|
|
113
|
-
for (const field of required) {
|
|
114
|
-
const prop = schema.properties?.[field]
|
|
115
|
-
if (!prop) { if (!(field in record)) record[field] = null; continue }
|
|
116
|
-
|
|
117
|
-
const types = Array.isArray(prop.type) ? prop.type : [prop.type]
|
|
118
|
-
const t = types.find(t => t !== "null") || types[0]
|
|
119
|
-
|
|
120
|
-
if (t === "object" && prop.properties) {
|
|
121
|
-
if (record[field] == null) record[field] = {}
|
|
122
|
-
if (isPlainObject(record[field])) fillSchemaDefaults(record[field], prop)
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (field in record) continue
|
|
127
|
-
if (prop.default !== undefined) { record[field] = structuredClone(prop.default); continue }
|
|
128
|
-
if (prop.const !== undefined) { record[field] = prop.const; continue }
|
|
129
|
-
if (prop.enum) { record[field] = prop.enum[0]; continue }
|
|
130
|
-
if (t === "integer" || t === "number") {
|
|
131
|
-
record[field] = prop.minimum !== undefined ? prop.minimum : 0
|
|
132
|
-
continue
|
|
133
|
-
}
|
|
134
|
-
switch (t) {
|
|
135
|
-
case "string":
|
|
136
|
-
record[field] = prop.minLength > 1 ? "x".repeat(prop.minLength) : field
|
|
137
|
-
break
|
|
138
|
-
case "boolean": record[field] = false; break
|
|
139
|
-
case "array": record[field] = []; break
|
|
140
|
-
default: record[field] = field; break
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function sanitizeId(raw) {
|
|
146
|
-
return raw.replace(/[<>:"/\\|?*]/g, "_").replace(/\.\./g, "_").trim()
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function handleAdd(cls, id, dataStr) {
|
|
150
|
-
let parsed
|
|
151
|
-
try { parsed = JSON.parse(dataStr) } catch (e) { parsed = null }
|
|
152
|
-
if (!isPlainObject(parsed)) {
|
|
153
|
-
const txt = String(dataStr ?? parsed ?? "").slice(0, 200)
|
|
154
|
-
parsed = { summary: txt }
|
|
155
|
-
}
|
|
156
|
-
if (!id?.trim()) return "non-blank id is required"
|
|
157
|
-
id = sanitizeId(id.trim())
|
|
158
|
-
|
|
159
|
-
const now = new Date().toISOString()
|
|
160
|
-
const existing = queryGet(cls, id)
|
|
161
|
-
const existingIsActive = existing && !hasExpired(existing)
|
|
162
|
-
const record = { ...parsed, id, class: cls, source: parsed.source ?? "agent", status: parsed.status ?? "active", created_at: parsed.created_at ?? (existing?.created_at ?? now), updated_at: now }
|
|
163
|
-
|
|
164
|
-
const schema = readJson(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
|
|
165
|
-
if (schema) {
|
|
166
|
-
fillSchemaDefaults(record, schema)
|
|
167
|
-
|
|
168
|
-
const required = Array.isArray(schema.required) ? schema.required : []
|
|
169
|
-
if (required.includes("provenance")) {
|
|
170
|
-
if (!record.provenance?.session_id) record.provenance = { ...record.provenance, session_id: `auto-${now}` }
|
|
171
|
-
if (cls === "audit" && !enforceAuditEvidence(record)) {
|
|
172
|
-
const prov = record.provenance
|
|
173
|
-
if (!Array.isArray(prov.file_refs)) prov.file_refs = []
|
|
174
|
-
if (!prov.file_refs.some(r => typeof r === "string" && r.trim())) prov.file_refs.push("auto-filled")
|
|
175
|
-
}
|
|
176
|
-
}
|
|
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
|
-
|
|
182
|
-
const unsupported = findUnsupportedSchemaKeywords(schema)
|
|
183
|
-
if (unsupported.length) return `unsupported schema: ${unsupported.join(", ")}`
|
|
184
|
-
const errs = validateSchema(schema, record, "$")
|
|
185
|
-
if (cls === "audit" && !enforceAuditEvidence(record)) errs.push("$.provenance must include at least one non-empty evidence ref")
|
|
186
|
-
if (errs.length) return `validation: ${errs.join("; ")}`
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (cls === "mistake") upsertMistake(record)
|
|
190
|
-
else writeObject(cls, record)
|
|
191
|
-
|
|
192
|
-
const action = existingIsActive ? "updated" : "saved"
|
|
193
|
-
return `${action}: ${id.trim()}`
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function handleFetch(cls, id) {
|
|
197
|
-
if (!id?.trim()) return "non-blank id is required"
|
|
198
|
-
id = sanitizeId(id.trim())
|
|
199
|
-
const record = queryGet(cls, id)
|
|
200
|
-
if (!record) return `not found: ${id}`
|
|
201
|
-
return stableStringify(record)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function handleList(cls, limit = 10) {
|
|
205
|
-
const entries = queryList(cls, Math.min(limit, 100))
|
|
206
|
-
const lines = entries.map(e => ` ${e.id}: ${truncateText(e.summary || "", 80)}`)
|
|
207
|
-
return `${entries.length} ${cls} record${entries.length === 1 ? "" : "s"}\n` + lines.join("\n")
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function handleLatest(cls) {
|
|
211
|
-
const list = queryList(cls, 100)
|
|
212
|
-
const active = filterActive(list)
|
|
213
|
-
if (!active[0]?.id) return "no active records"
|
|
214
|
-
const record = queryGet(cls, active[0].id)
|
|
215
|
-
if (!record) return "no active records"
|
|
216
|
-
return stableStringify(record)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function handleSearch(query, scope, classes, project, limit) {
|
|
220
|
-
const q = (query || "").trim()
|
|
221
|
-
if (!q) return "non-blank query is required"
|
|
222
|
-
const clsList = Array.isArray(classes) && classes.length ? classes : CLASSES
|
|
223
|
-
const lim = Math.min(limit ?? 10, 50)
|
|
224
|
-
let candidates = []
|
|
225
|
-
for (const cls of clsList) {
|
|
226
|
-
if (cls === "mistake") {
|
|
227
|
-
for (const m of readJsonl(path.join(classDir(cls), "mistakes.jsonl"))) {
|
|
228
|
-
if (!hasExpired(m)) candidates.push({ ...m, _cls: cls })
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
const dir = classDir(cls)
|
|
232
|
-
const index = readJson(path.join(dir, "index.json"), [])
|
|
233
|
-
if (!Array.isArray(index)) continue
|
|
234
|
-
for (const entry of index) {
|
|
235
|
-
if (!hasExpired(entry)) candidates.push({ ...entry, _cls: cls })
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (scope === "global") candidates = candidates.filter(r => r.scope === "global" || !r.scope)
|
|
240
|
-
else if (scope === "local") candidates = candidates.filter(r => r.scope === "project" || r.scope === "session")
|
|
241
|
-
|
|
242
|
-
// Score using index entries (summary-based)
|
|
243
|
-
const scored = candidates
|
|
244
|
-
.map(e => ({ ...e, score: scoreRelevance(e, q, project || "") }))
|
|
245
|
-
.filter(e => e.score > 0)
|
|
246
|
-
.sort((a, b) => b.score - a.score)
|
|
247
|
-
.slice(0, lim)
|
|
248
|
-
|
|
249
|
-
// Fetch full records for top results to get richer fields
|
|
250
|
-
const results = scored.map(e => {
|
|
251
|
-
if (e._cls === "mistake") return e
|
|
252
|
-
const full = queryGet(e._cls, e.id)
|
|
253
|
-
return full || e
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
const lines = results.map(e => ` ${e.id} (${e.score}pt): ${truncateText(e.summary || "", 80)}`)
|
|
257
|
-
return `${results.length} result${results.length === 1 ? "" : "s"} for '${q}'\n` + lines.join("\n")
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function handleArchive(cls, id) {
|
|
261
|
-
if (!id?.trim()) return "non-blank id is required"
|
|
262
|
-
id = sanitizeId(id.trim())
|
|
263
|
-
const record = queryGet(cls, id)
|
|
264
|
-
if (!record) return `not found: ${id}`
|
|
265
|
-
record.status = "expired"
|
|
266
|
-
record.updated_at = new Date().toISOString()
|
|
267
|
-
if (cls === "mistake") upsertMistake(record)
|
|
268
|
-
else writeObject(cls, record)
|
|
269
|
-
return `archived: ${id.trim()}`
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export const MemoryToolsPlugin = async (ctx) => {
|
|
273
|
-
fs.mkdirSync(MEMORY_DIR, { recursive: true })
|
|
274
|
-
fs.mkdirSync(getRuntimeRoot(), { recursive: true })
|
|
275
|
-
|
|
276
|
-
const config = loadConfig()
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
tool: {
|
|
280
|
-
ohc_save: tool({
|
|
281
|
-
description: "Save a new memory record or update an existing one by class and id",
|
|
282
|
-
args: {
|
|
283
|
-
class: tool.schema.enum(CLASSES),
|
|
284
|
-
id: tool.schema.string(),
|
|
285
|
-
data: tool.schema.string(),
|
|
286
|
-
},
|
|
287
|
-
async execute(args, context) {
|
|
288
|
-
setToolTitle(context, `save ${args.class}: ${args.id}`)
|
|
289
|
-
const result = handleAdd(args.class, args.id, args.data)
|
|
290
|
-
if (result.startsWith("saved:") || result.startsWith("updated:")) {
|
|
291
|
-
const action = result.startsWith("saved:") ? "Saved" : "Updated"
|
|
292
|
-
sendMemoryNotification(ctx?.client, context.sessionID, config, action, args.class, args.id, null).catch(() => {})
|
|
293
|
-
}
|
|
294
|
-
return result
|
|
295
|
-
},
|
|
296
|
-
}),
|
|
297
|
-
|
|
298
|
-
ohc_get: tool({
|
|
299
|
-
description: "Get a specific memory record by class and id",
|
|
300
|
-
args: {
|
|
301
|
-
class: tool.schema.enum(CLASSES).describe("Memory class"),
|
|
302
|
-
id: tool.schema.string().describe("Record ID"),
|
|
303
|
-
},
|
|
304
|
-
async execute(args, context) {
|
|
305
|
-
setToolTitle(context, `fetch ${args.class}: ${args.id}`)
|
|
306
|
-
return handleFetch(args.class, args.id)
|
|
307
|
-
},
|
|
308
|
-
}),
|
|
309
|
-
|
|
310
|
-
ohc_list: tool({
|
|
311
|
-
description: "List recent memory records by class, sorted by recency",
|
|
312
|
-
args: {
|
|
313
|
-
class: tool.schema.enum(CLASSES).describe("Memory class"),
|
|
314
|
-
limit: tool.schema.number().optional().default(10).describe("Max results (max 100)"),
|
|
315
|
-
},
|
|
316
|
-
async execute(args, context) {
|
|
317
|
-
setToolTitle(context, `list ${args.class}`)
|
|
318
|
-
return handleList(args.class, args.limit)
|
|
319
|
-
},
|
|
320
|
-
}),
|
|
321
|
-
|
|
322
|
-
ohc_latest: tool({
|
|
323
|
-
description: "Get the latest active memory record by class",
|
|
324
|
-
args: {
|
|
325
|
-
class: tool.schema.enum(CLASSES).describe("Memory class"),
|
|
326
|
-
},
|
|
327
|
-
async execute(args, context) {
|
|
328
|
-
setToolTitle(context, `latest ${args.class}`)
|
|
329
|
-
return handleLatest(args.class)
|
|
330
|
-
},
|
|
331
|
-
}),
|
|
332
|
-
|
|
333
|
-
ohc_search: tool({
|
|
334
|
-
description: "Search memory records with keyword matching and relevance ranking across all classes",
|
|
335
|
-
args: {
|
|
336
|
-
query: tool.schema.string().describe("Search query string"),
|
|
337
|
-
scope: tool.schema.enum(["global", "local", "auto"]).optional().default("auto").describe("Search scope"),
|
|
338
|
-
classes: tool.schema.array(tool.schema.enum(CLASSES)).optional().describe("Memory classes to search (default: all)"),
|
|
339
|
-
project: tool.schema.string().optional().describe("Project filter"),
|
|
340
|
-
limit: tool.schema.number().optional().default(10).describe("Max results (max 50)"),
|
|
341
|
-
},
|
|
342
|
-
async execute(args, context) {
|
|
343
|
-
setToolTitle(context, `search: ${truncateText(args.query, 48)}`)
|
|
344
|
-
return handleSearch(args.query, args.scope, args.classes, args.project, args.limit)
|
|
345
|
-
},
|
|
346
|
-
}),
|
|
347
|
-
|
|
348
|
-
ohc_archive: tool({
|
|
349
|
-
description: "Soft-delete a memory record by setting its status to expired",
|
|
350
|
-
args: {
|
|
351
|
-
class: tool.schema.enum(CLASSES),
|
|
352
|
-
id: tool.schema.string(),
|
|
353
|
-
},
|
|
354
|
-
async execute(args, context) {
|
|
355
|
-
setToolTitle(context, `archive ${args.class}: ${args.id}`)
|
|
356
|
-
const result = handleArchive(args.class, args.id)
|
|
357
|
-
if (result.startsWith("archived:")) {
|
|
358
|
-
sendMemoryNotification(ctx?.client, context.sessionID, config, "Archived", args.class, args.id, null).catch(() => {})
|
|
359
|
-
}
|
|
360
|
-
return result
|
|
361
|
-
},
|
|
362
|
-
}),
|
|
363
|
-
},
|
|
364
|
-
}
|
|
365
|
-
}
|
package/lib/ohc/block-sync.mjs
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
export function syncCompressionBlocks(state, messages) {
|
|
2
|
-
const ms = state.prune.messages
|
|
3
|
-
if (!ms?.blocksById?.size) return
|
|
4
|
-
|
|
5
|
-
const messageIds = new Set()
|
|
6
|
-
for (const msg of messages) {
|
|
7
|
-
if (msg.info?.id) messageIds.add(msg.info.id)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const prevActive = new Set(ms.activeBlockIds)
|
|
11
|
-
|
|
12
|
-
ms.activeBlockIds.clear()
|
|
13
|
-
ms.activeByAnchorMessageId?.clear()
|
|
14
|
-
|
|
15
|
-
const now = Date.now()
|
|
16
|
-
const ordered = [...ms.blocksById.values()].sort(
|
|
17
|
-
(a, b) => (a.createdAt || 0) - (b.createdAt || 0) || a.blockId - b.blockId,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
for (const block of ordered) {
|
|
21
|
-
const hasOrigin = typeof block.compressMessageId === "string" &&
|
|
22
|
-
block.compressMessageId.length > 0 &&
|
|
23
|
-
messageIds.has(block.compressMessageId)
|
|
24
|
-
|
|
25
|
-
if (!hasOrigin) {
|
|
26
|
-
block.active = false
|
|
27
|
-
block.deactivatedAt = now
|
|
28
|
-
continue
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (block.deactivatedByUser) {
|
|
32
|
-
block.active = false
|
|
33
|
-
if (block.deactivatedAt === undefined) block.deactivatedAt = now
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const consumed = Array.isArray(block.consumedBlockIds) ? block.consumedBlockIds : []
|
|
38
|
-
for (const cid of consumed) {
|
|
39
|
-
if (!ms.activeBlockIds.has(cid)) continue
|
|
40
|
-
const cb = ms.blocksById.get(cid)
|
|
41
|
-
if (cb) {
|
|
42
|
-
cb.active = false
|
|
43
|
-
cb.deactivatedAt = now
|
|
44
|
-
cb.deactivatedByBlockId = block.blockId
|
|
45
|
-
}
|
|
46
|
-
ms.activeBlockIds.delete(cid)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
block.active = true
|
|
50
|
-
block.deactivatedAt = undefined
|
|
51
|
-
block.deactivatedByBlockId = undefined
|
|
52
|
-
ms.activeBlockIds.add(block.blockId)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
for (const entry of ms.byMessageId.values()) {
|
|
56
|
-
const all = Array.isArray(entry.allBlockIds)
|
|
57
|
-
? [...new Set(entry.allBlockIds.filter(id => Number.isInteger(id) && id > 0))]
|
|
58
|
-
: []
|
|
59
|
-
entry.allBlockIds = all
|
|
60
|
-
entry.activeBlockIds = all.filter(id => ms.activeBlockIds.has(id))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let deactivated = 0
|
|
64
|
-
let reactivated = 0
|
|
65
|
-
for (const id of prevActive) { if (!ms.activeBlockIds.has(id)) deactivated++ }
|
|
66
|
-
for (const id of ms.activeBlockIds) { if (!prevActive.has(id)) reactivated++ }
|
|
67
|
-
|
|
68
|
-
return { deactivated, reactivated }
|
|
69
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { parseBoundaryId, formatBlockRef } from "../message-ids.mjs"
|
|
2
|
-
import { totalTokens } from "../reaper.mjs"
|
|
3
|
-
|
|
4
|
-
export function buildSearchContext(state, rawMessages) {
|
|
5
|
-
const rawMessagesById = new Map()
|
|
6
|
-
const rawIndexById = new Map()
|
|
7
|
-
|
|
8
|
-
for (const msg of rawMessages) {
|
|
9
|
-
if (msg.info?.id) rawMessagesById.set(msg.info.id, msg)
|
|
10
|
-
}
|
|
11
|
-
for (let i = 0; i < rawMessages.length; i++) {
|
|
12
|
-
const msg = rawMessages[i]
|
|
13
|
-
if (msg?.info?.id) rawIndexById.set(msg.info.id, i)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const summaryByBlockId = new Map()
|
|
17
|
-
const ms = state.prune?.messages
|
|
18
|
-
if (ms?.blocksById) {
|
|
19
|
-
for (const [blockId, block] of ms.blocksById) {
|
|
20
|
-
if (block.active) summaryByBlockId.set(blockId, block)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return { rawMessages, rawMessagesById, rawIndexById, summaryByBlockId }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function resolveBoundaryIds(context, state, startId, endId) {
|
|
28
|
-
const lookup = buildBoundaryLookup(context, state)
|
|
29
|
-
|
|
30
|
-
const parsedStart = parseBoundaryId(startId)
|
|
31
|
-
const parsedEnd = parseBoundaryId(endId)
|
|
32
|
-
|
|
33
|
-
if (!parsedStart) throw new Error(`startId "${startId}" is invalid. Use ohcNNNN (message) or bkNN (block).`)
|
|
34
|
-
if (!parsedEnd) throw new Error(`endId "${endId}" is invalid. Use ohcNNNN (message) or bkNN (block).`)
|
|
35
|
-
|
|
36
|
-
const startRef = lookup.get(parsedStart.ref)
|
|
37
|
-
const endRef = lookup.get(parsedEnd.ref)
|
|
38
|
-
|
|
39
|
-
if (!startRef) throw new Error(`startId ${parsedStart.ref} not found in context.`)
|
|
40
|
-
if (!endRef) throw new Error(`endId ${parsedEnd.ref} not found in context.`)
|
|
41
|
-
|
|
42
|
-
if (startRef.rawIndex > endRef.rawIndex) {
|
|
43
|
-
throw new Error(`startId ${parsedStart.ref} appears after endId ${parsedEnd.ref}. Start must come before end.`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return { startReference: startRef, endReference: endRef }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function buildBoundaryLookup(context, state) {
|
|
50
|
-
const lookup = new Map()
|
|
51
|
-
|
|
52
|
-
for (const [msgRef, rawId] of state.messageIds.byRef) {
|
|
53
|
-
const rawMsg = context.rawMessagesById.get(rawId)
|
|
54
|
-
if (!rawMsg) continue
|
|
55
|
-
|
|
56
|
-
const rawIndex = context.rawIndexById.get(rawId)
|
|
57
|
-
if (rawIndex === undefined) continue
|
|
58
|
-
|
|
59
|
-
lookup.set(msgRef, { kind: "message", rawIndex, messageId: rawId })
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const summaries = Array.from(context.summaryByBlockId.values()).sort((a, b) => a.blockId - b.blockId)
|
|
63
|
-
for (const summary of summaries) {
|
|
64
|
-
const anchorMsg = context.rawMessagesById.get(summary.anchorMessageId)
|
|
65
|
-
if (!anchorMsg) continue
|
|
66
|
-
|
|
67
|
-
const rawIndex = context.rawIndexById.get(summary.anchorMessageId)
|
|
68
|
-
if (rawIndex === undefined) continue
|
|
69
|
-
|
|
70
|
-
const bkRef = formatBlockRef(summary.blockId)
|
|
71
|
-
if (!lookup.has(bkRef)) {
|
|
72
|
-
lookup.set(bkRef, {
|
|
73
|
-
kind: "compressed-block",
|
|
74
|
-
rawIndex,
|
|
75
|
-
blockId: summary.blockId,
|
|
76
|
-
anchorMessageId: summary.anchorMessageId,
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return lookup
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function resolveSelection(context, startReference, endReference) {
|
|
85
|
-
const messageIds = []
|
|
86
|
-
const messageSeen = new Set()
|
|
87
|
-
const toolIds = []
|
|
88
|
-
const toolSeen = new Set()
|
|
89
|
-
const requiredBlockIds = []
|
|
90
|
-
const requiredBlockSeen = new Set()
|
|
91
|
-
|
|
92
|
-
for (let i = startReference.rawIndex; i <= endReference.rawIndex; i++) {
|
|
93
|
-
const msg = context.rawMessages[i]
|
|
94
|
-
if (!msg) continue
|
|
95
|
-
|
|
96
|
-
const mid = msg.info?.id
|
|
97
|
-
if (mid && !messageSeen.has(mid)) {
|
|
98
|
-
messageSeen.add(mid)
|
|
99
|
-
messageIds.push(mid)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
103
|
-
for (const part of parts) {
|
|
104
|
-
if (part.type !== "tool" || !part.callID || toolSeen.has(part.callID)) continue
|
|
105
|
-
toolSeen.add(part.callID)
|
|
106
|
-
toolIds.push(part.callID)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const selectedIds = new Set(messageIds)
|
|
111
|
-
const summariesInRange = []
|
|
112
|
-
for (const block of context.summaryByBlockId.values()) {
|
|
113
|
-
if (!selectedIds.has(block.anchorMessageId)) continue
|
|
114
|
-
const idx = context.rawIndexById.get(block.anchorMessageId)
|
|
115
|
-
if (idx === undefined) continue
|
|
116
|
-
summariesInRange.push({ blockId: block.blockId, rawIndex: idx })
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
summariesInRange.sort((a, b) => a.rawIndex - b.rawIndex || a.blockId - b.blockId)
|
|
120
|
-
for (const s of summariesInRange) {
|
|
121
|
-
if (!requiredBlockSeen.has(s.blockId)) {
|
|
122
|
-
requiredBlockSeen.add(s.blockId)
|
|
123
|
-
requiredBlockIds.push(s.blockId)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (messageIds.length === 0) {
|
|
128
|
-
throw new Error("No messages found in the specified range.")
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { startReference, endReference, messageIds, toolIds, requiredBlockIds }
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function resolveAnchorMessageId(startReference) {
|
|
135
|
-
if (startReference.kind === "compressed-block") {
|
|
136
|
-
if (!startReference.anchorMessageId) throw new Error("Compressed block has no anchor message ID")
|
|
137
|
-
return startReference.anchorMessageId
|
|
138
|
-
}
|
|
139
|
-
if (!startReference.messageId) throw new Error("No message ID in start reference")
|
|
140
|
-
return startReference.messageId
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function validateNonOverlapping(ranges) {
|
|
144
|
-
const sorted = [...ranges].sort((a, b) => a.selection.startReference.rawIndex - b.selection.startReference.rawIndex)
|
|
145
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
146
|
-
const prev = sorted[i - 1]
|
|
147
|
-
const curr = sorted[i]
|
|
148
|
-
if (curr.selection.startReference.rawIndex <= prev.selection.endReference.rawIndex) {
|
|
149
|
-
throw new Error(`content[${prev.index}] (${prev.entry.startId}..${prev.entry.endId}) overlaps content[${curr.index}] (${curr.entry.startId}..${curr.entry.endId}).`)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
export function allocateBlockId(state) {
|
|
2
|
-
const ms = state.prune.messages
|
|
3
|
-
const id = ms.nextBlockId
|
|
4
|
-
ms.nextBlockId++
|
|
5
|
-
return id
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function allocateRunId(state) {
|
|
9
|
-
const ms = state.prune.messages
|
|
10
|
-
const id = ms.nextRunId
|
|
11
|
-
ms.nextRunId++
|
|
12
|
-
return id
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function wrapBlockSummary(blockId, summary) {
|
|
16
|
-
return `[OHC: Compressed bk${blockId}]\n\n${summary}\n<ohc-ref>bk${blockId}</ohc-ref>`
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function applyCompressionState(state, input, selection, anchorMessageId, blockId, storedSummary, consumedBlockIds) {
|
|
20
|
-
const ms = state.prune.messages
|
|
21
|
-
|
|
22
|
-
const block = {
|
|
23
|
-
blockId,
|
|
24
|
-
active: true,
|
|
25
|
-
topic: input.topic,
|
|
26
|
-
batchTopic: input.batchTopic || input.topic,
|
|
27
|
-
mode: input.mode || "range",
|
|
28
|
-
runId: input.runId,
|
|
29
|
-
compressMessageId: input.compressMessageId,
|
|
30
|
-
compressCallId: input.compressCallId || null,
|
|
31
|
-
anchorMessageId,
|
|
32
|
-
startId: input.startId,
|
|
33
|
-
endId: input.endId,
|
|
34
|
-
summary: storedSummary,
|
|
35
|
-
summaryTokens: input.summaryTokens || 0,
|
|
36
|
-
compressedTokens: input.compressedTokens || 0,
|
|
37
|
-
consumedBlockIds: Array.isArray(consumedBlockIds) ? consumedBlockIds : [],
|
|
38
|
-
deactivatedByBlockId: undefined,
|
|
39
|
-
deactivatedByUser: false,
|
|
40
|
-
deactivatedAt: undefined,
|
|
41
|
-
createdAt: Date.now(),
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
ms.blocksById.set(blockId, block)
|
|
45
|
-
ms.activeBlockIds.add(blockId)
|
|
46
|
-
|
|
47
|
-
if (anchorMessageId) {
|
|
48
|
-
ms.activeByAnchorMessageId.set(anchorMessageId, blockId)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (const rawMessageId of selection.messageIds) {
|
|
52
|
-
let entry = ms.byMessageId.get(rawMessageId)
|
|
53
|
-
if (!entry) {
|
|
54
|
-
entry = { tokenCount: 0, allBlockIds: [], activeBlockIds: [] }
|
|
55
|
-
ms.byMessageId.set(rawMessageId, entry)
|
|
56
|
-
}
|
|
57
|
-
if (!entry.allBlockIds.includes(blockId)) {
|
|
58
|
-
entry.allBlockIds.push(blockId)
|
|
59
|
-
}
|
|
60
|
-
if (!entry.activeBlockIds.includes(blockId)) {
|
|
61
|
-
entry.activeBlockIds.push(blockId)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
for (const cid of consumedBlockIds) {
|
|
66
|
-
const cb = ms.blocksById.get(cid)
|
|
67
|
-
if (cb) {
|
|
68
|
-
cb.active = false
|
|
69
|
-
cb.deactivatedAt = Date.now()
|
|
70
|
-
cb.deactivatedByBlockId = blockId
|
|
71
|
-
ms.activeBlockIds.delete(cid)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return block
|
|
76
|
-
}
|