openhermes 1.12.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -207
- package/autorecall.mjs +79 -12
- package/bootstrap.mjs +123 -24
- package/curator.mjs +4 -40
- package/harness/commands/harness-audit.md +1 -1
- package/harness/commands/learn.md +2 -2
- package/harness/commands/memory-search.md +2 -2
- package/harness/commands/ohc.md +13 -0
- package/harness/constitution/soul.md +16 -4
- package/harness/instructions/RUNTIME.md +6 -3
- package/harness/prompts/architect.txt +14 -0
- package/harness/prompts/build-cpp.md +15 -1
- package/harness/prompts/build-error-resolver.md +15 -9
- package/harness/prompts/build-go.md +14 -0
- package/harness/prompts/build-java.md +15 -1
- package/harness/prompts/build-kotlin.md +15 -1
- package/harness/prompts/build-rust.md +14 -0
- package/harness/prompts/code-reviewer.md +15 -9
- package/harness/prompts/doc-updater.md +13 -0
- package/harness/prompts/docs-lookup.md +11 -0
- package/harness/prompts/e2e-runner.txt +12 -0
- package/harness/prompts/explore.md +16 -4
- package/harness/prompts/harness-optimizer.md +12 -0
- package/harness/prompts/loop-operator.md +11 -0
- package/harness/prompts/planner.md +15 -9
- package/harness/prompts/refactor-cleaner.md +14 -0
- package/harness/prompts/review-cpp.md +14 -1
- package/harness/prompts/review-database.md +13 -0
- package/harness/prompts/review-go.md +13 -0
- package/harness/prompts/review-java.md +14 -1
- package/harness/prompts/review-kotlin.md +13 -0
- package/harness/prompts/review-python.md +14 -1
- package/harness/prompts/review-rust.md +13 -0
- package/harness/prompts/security-reviewer.md +15 -9
- package/harness/prompts/tdd-guide.md +14 -0
- package/harness/rules/audit.md +2 -2
- package/harness/rules/delegation.md +0 -2
- package/harness/rules/handoff.md +267 -0
- package/harness/rules/memory-management.md +4 -4
- package/harness/rules/precedence.md +1 -1
- package/harness/rules/retrieval.md +5 -5
- package/harness/rules/runtime-guards.md +1 -1
- package/harness/rules/self-heal.md +1 -1
- package/harness/rules/session-start.md +5 -5
- package/harness/rules/skills-management.md +2 -2
- package/harness/rules/verification.md +4 -4
- package/harness/scripts/sync-commands.mjs +259 -0
- package/index.mjs +6 -2
- package/lib/ambient-memory.mjs +167 -0
- package/lib/handoff.mjs +176 -0
- package/lib/hardening.mjs +13 -8
- package/lib/memory-tools-plugin.mjs +107 -54
- package/lib/ohc/block-sync.mjs +69 -0
- package/lib/ohc/compress/search.mjs +152 -0
- package/lib/ohc/compress/state.mjs +76 -0
- package/lib/ohc/config.mjs +172 -16
- package/lib/ohc/message-ids.mjs +168 -0
- package/lib/ohc/notify.mjs +150 -0
- package/lib/ohc/protected-patterns.mjs +54 -0
- package/lib/ohc/prune-apply.mjs +134 -0
- package/lib/ohc/pruner.mjs +406 -55
- package/lib/ohc/reaper.mjs +12 -3
- package/lib/ohc/state.mjs +246 -15
- package/lib/ohc/strategies/deduplication.mjs +72 -0
- package/lib/ohc/strategies/index.mjs +2 -0
- package/lib/ohc/strategies/purge-errors.mjs +43 -0
- package/lib/ohc/token-utils.mjs +26 -0
- package/lib/ohc/updater.mjs +36 -13
- package/lib/paths.mjs +0 -3
- package/lib/search.mjs +48 -0
- package/package.json +6 -2
- package/schemas/audit.schema.json +22 -1
- package/schemas/backlog.schema.json +23 -2
- package/schemas/checkpoint.schema.json +23 -2
- package/schemas/constraint.schema.json +23 -2
- package/schemas/decision.schema.json +23 -2
- package/schemas/instinct.schema.json +23 -2
- package/schemas/mistake.schema.json +23 -2
- package/schemas/verification_receipt.schema.json +23 -2
- package/skill-builder.mjs +12 -23
package/lib/handoff.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// OpenHermes Agent Handoff — lightweight helper library
|
|
2
|
+
// Convention-based, no runtime deps. Agents import and call.
|
|
3
|
+
|
|
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
|
+
|
|
11
|
+
function nextId() {
|
|
12
|
+
_idSeq++
|
|
13
|
+
return `ho_${Date.now().toString(36)}_${_idSeq}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
|
|
17
|
+
|
|
18
|
+
// Estimate task complexity from file count and description.
|
|
19
|
+
export function assessComplexity(fileCount = 1, description = "", patterns = []) {
|
|
20
|
+
const pScore = Array.isArray(patterns) ? patterns.length : 0
|
|
21
|
+
const level = fileCount > 50 || pScore > 5 ? "very-large"
|
|
22
|
+
: fileCount > 10 || pScore > 2 ? "hard"
|
|
23
|
+
: fileCount > 2 || pScore > 0 ? "medium"
|
|
24
|
+
: "easy"
|
|
25
|
+
return {
|
|
26
|
+
level,
|
|
27
|
+
fileCount: clamp(fileCount, 0, 9999),
|
|
28
|
+
patterns: pScore,
|
|
29
|
+
strategy: level === "easy" ? "direct"
|
|
30
|
+
: level === "medium" ? "sequential-or-fan-out"
|
|
31
|
+
: level === "hard" ? "sequential-multi"
|
|
32
|
+
: "fan-out"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build a structured handoff request string for the `task` tool prompt.
|
|
37
|
+
export function handoffRequest(opts) {
|
|
38
|
+
const {
|
|
39
|
+
agent,
|
|
40
|
+
phase = "execute",
|
|
41
|
+
context = "",
|
|
42
|
+
goal = "",
|
|
43
|
+
expected = "",
|
|
44
|
+
permissions = "",
|
|
45
|
+
limits = "",
|
|
46
|
+
complexity,
|
|
47
|
+
} = opts || {}
|
|
48
|
+
const id = nextId()
|
|
49
|
+
const c = complexity || assessComplexity()
|
|
50
|
+
const lines = [
|
|
51
|
+
`## HANDOFF REQUEST`,
|
|
52
|
+
`Agent: ${agent}`,
|
|
53
|
+
`Task ID: ${id}`,
|
|
54
|
+
`Phase: ${phase}`,
|
|
55
|
+
`Complexity: ${c.level}`,
|
|
56
|
+
``,
|
|
57
|
+
`### Context`,
|
|
58
|
+
String(context).trim() || "(no context provided)",
|
|
59
|
+
``,
|
|
60
|
+
`### Goal`,
|
|
61
|
+
String(goal).trim() || "(no goal provided)",
|
|
62
|
+
``,
|
|
63
|
+
`### Expected Output`,
|
|
64
|
+
String(expected).trim() || "Return structured result with status, summary, details, receipts, next, learning.",
|
|
65
|
+
``,
|
|
66
|
+
`### Permissions`,
|
|
67
|
+
String(permissions).trim() || "(default permissions apply)",
|
|
68
|
+
``,
|
|
69
|
+
`### Limits`,
|
|
70
|
+
String(limits).trim() || "(no explicit limits)",
|
|
71
|
+
``,
|
|
72
|
+
`### Handoff Result Format`,
|
|
73
|
+
`When done, return:`,
|
|
74
|
+
`## HANDOFF RESULT`,
|
|
75
|
+
`Status: success | failure | partial`,
|
|
76
|
+
`Task ID: ${id}`,
|
|
77
|
+
`### Summary`,
|
|
78
|
+
`### Details`,
|
|
79
|
+
`### Receipts`,
|
|
80
|
+
`### Next`,
|
|
81
|
+
`### Learning`,
|
|
82
|
+
].join("\n")
|
|
83
|
+
return { id, prompt: lines }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate a subagent's result string has required sections.
|
|
87
|
+
export function parseHandoffResult(text) {
|
|
88
|
+
if (!text || typeof text !== "string") return { status: "invalid", error: "no result text" }
|
|
89
|
+
const statusMatch = text.match(/Status:\s*(success|failure|partial)/i)
|
|
90
|
+
const hasSummary = /### Summary/i.test(text)
|
|
91
|
+
const hasDetails = /### Details/i.test(text)
|
|
92
|
+
const hasReceipts = /### Receipts/i.test(text)
|
|
93
|
+
const summaryMatch = text.match(/### Summary\s*\n\s*(.+)/i)
|
|
94
|
+
return {
|
|
95
|
+
status: statusMatch ? statusMatch[1].toLowerCase() : "unknown",
|
|
96
|
+
summary: summaryMatch ? summaryMatch[1].trim() : "",
|
|
97
|
+
hasSummary,
|
|
98
|
+
hasDetails,
|
|
99
|
+
hasReceipts,
|
|
100
|
+
valid: !!(statusMatch && hasSummary),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Agent capability/role definitions for reference.
|
|
105
|
+
export const AGENT_ROLES = {
|
|
106
|
+
OpenHermes: { tier: 3, edit: true, exec: true, desc: "Primary agent — all tools, all permissions" },
|
|
107
|
+
architect: { tier: 1, edit: false, exec: false, desc: "System architecture design" },
|
|
108
|
+
planner: { tier: 1, edit: false, exec: false, desc: "Feature/refactor planning" },
|
|
109
|
+
"code-reviewer": { tier: 1, edit: false, exec: false, desc: "Code quality review" },
|
|
110
|
+
"security-reviewer": { tier: 1, edit: false, exec: false, desc: "Security audit (report only, no patches)" },
|
|
111
|
+
explore: { tier: 1, edit: false, exec: false, desc: "Read-only codebase exploration" },
|
|
112
|
+
"build-error-resolver": { tier: 2, edit: true, exec: true, desc: "Build/type error fixes" },
|
|
113
|
+
"doc-updater": { tier: 2, edit: true, exec: true, desc: "Doc/codemap updates" },
|
|
114
|
+
"refactor-cleaner": { tier: 2, edit: true, exec: true, desc: "Dead code cleanup" },
|
|
115
|
+
"tdd-guide": { tier: 2, edit: true, exec: true, desc: "TDD red-green-refactor" },
|
|
116
|
+
"loop-operator": { tier: 3, edit: true, exec: true, desc: "Managed autonomous loops" },
|
|
117
|
+
"e2e-runner": { tier: 3, edit: true, exec: true, desc: "Playwright E2E tests" },
|
|
118
|
+
"docs-lookup": { tier: 1, edit: false, exec: true, desc: "MCP doc lookup" },
|
|
119
|
+
"harness-optimizer": { tier: 1, edit: false, exec: true, desc: "Harness config audit" },
|
|
120
|
+
"review-database": { tier: 1, edit: false, exec: true, desc: "PostgreSQL review" },
|
|
121
|
+
"review-go": { tier: 1, edit: false, exec: true, desc: "Go code review" },
|
|
122
|
+
"build-go": { tier: 2, edit: true, exec: true, desc: "Go build fix" },
|
|
123
|
+
"review-java": { tier: 1, edit: false, exec: true, desc: "Java review" },
|
|
124
|
+
"build-java": { tier: 2, edit: true, exec: true, desc: "Java build fix" },
|
|
125
|
+
"review-kotlin": { tier: 1, edit: false, exec: true, desc: "Kotlin review" },
|
|
126
|
+
"build-kotlin": { tier: 2, edit: true, exec: true, desc: "Kotlin build fix" },
|
|
127
|
+
"review-cpp": { tier: 1, edit: false, exec: true, desc: "C++ review" },
|
|
128
|
+
"build-cpp": { tier: 2, edit: true, exec: true, desc: "C++ build fix" },
|
|
129
|
+
"review-rust": { tier: 1, edit: false, exec: true, desc: "Rust review" },
|
|
130
|
+
"build-rust": { tier: 2, edit: true, exec: true, desc: "Rust build fix" },
|
|
131
|
+
"review-python": { tier: 1, edit: false, exec: true, desc: "Python review" },
|
|
132
|
+
"general": { tier: 2, edit: true, exec: true, desc: "General purpose" },
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Suggest best agent for a task type.
|
|
136
|
+
export function suggestAgent(taskType) {
|
|
137
|
+
const map = {
|
|
138
|
+
architecture: "architect",
|
|
139
|
+
plan: "planner",
|
|
140
|
+
"code-review": "code-reviewer",
|
|
141
|
+
security: "security-reviewer",
|
|
142
|
+
explore: "explore",
|
|
143
|
+
"build-fix": "build-error-resolver",
|
|
144
|
+
docs: "doc-updater",
|
|
145
|
+
"dead-code": "refactor-cleaner",
|
|
146
|
+
tdd: "tdd-guide",
|
|
147
|
+
e2e: "e2e-runner",
|
|
148
|
+
"doc-lookup": "docs-lookup",
|
|
149
|
+
"db-review": "review-database",
|
|
150
|
+
"go-review": "review-go",
|
|
151
|
+
"go-build": "build-go",
|
|
152
|
+
"java-review": "review-java",
|
|
153
|
+
"java-build": "build-java",
|
|
154
|
+
"kotlin-review": "review-kotlin",
|
|
155
|
+
"kotlin-build": "build-kotlin",
|
|
156
|
+
"cpp-review": "review-cpp",
|
|
157
|
+
"cpp-build": "build-cpp",
|
|
158
|
+
"rust-review": "review-rust",
|
|
159
|
+
"rust-build": "build-rust",
|
|
160
|
+
"python-review": "review-python",
|
|
161
|
+
loop: "loop-operator",
|
|
162
|
+
"harness-audit": "harness-optimizer",
|
|
163
|
+
}
|
|
164
|
+
return map[taskType] || null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Quick permission check: can agent X do action Y?
|
|
168
|
+
export function canAgent(agentName, action) {
|
|
169
|
+
const role = AGENT_ROLES[agentName]
|
|
170
|
+
if (!role) return false
|
|
171
|
+
if (action === "edit") return role.edit
|
|
172
|
+
if (action === "exec" || action === "bash") return role.exec
|
|
173
|
+
if (action === "read") return true
|
|
174
|
+
if (action === "delegate") return role.tier >= 1
|
|
175
|
+
return false
|
|
176
|
+
}
|
package/lib/hardening.mjs
CHANGED
|
@@ -96,13 +96,8 @@ function atomicWriteJson(filePath, data) {
|
|
|
96
96
|
try {
|
|
97
97
|
fs.renameSync(tmpPath, filePath)
|
|
98
98
|
} catch (err) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fs.renameSync(tmpPath, filePath)
|
|
102
|
-
} catch (retryErr) {
|
|
103
|
-
try { fs.rmSync(tmpPath, { force: true }) } catch {}
|
|
104
|
-
throw retryErr
|
|
105
|
-
}
|
|
99
|
+
fs.copyFileSync(tmpPath, filePath)
|
|
100
|
+
fs.rmSync(tmpPath, { force: true })
|
|
106
101
|
}
|
|
107
102
|
}
|
|
108
103
|
|
|
@@ -120,4 +115,14 @@ function readJsonl(fp) {
|
|
|
120
115
|
} catch { return [] }
|
|
121
116
|
}
|
|
122
117
|
|
|
123
|
-
|
|
118
|
+
function buildEnvironmentFingerprint(root, directory, project) {
|
|
119
|
+
return fingerprintEnvironment({
|
|
120
|
+
cwd: directory,
|
|
121
|
+
harnessRoot: root,
|
|
122
|
+
projectRoot: directory,
|
|
123
|
+
project: project?.name || path.basename(directory),
|
|
124
|
+
sessionId: project?.session_id || null,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { atomicWriteJson, buildEnvironmentFingerprint, fingerprintEnvironment, fingerprintFile, isPlainObject, isTruthy, readJson, readJsonl, redactSensitiveText, sanitizeRecord, truncateText }
|
|
@@ -7,6 +7,9 @@ import os from "node:os"
|
|
|
7
7
|
import { atomicWriteJson, fingerprintEnvironment, readJson, readJsonl, sanitizeRecord, truncateText } from "./hardening.mjs"
|
|
8
8
|
import { findUnsupportedSchemaKeywords, validateSchema } from "./schema-validator.mjs"
|
|
9
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"
|
|
10
13
|
|
|
11
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
15
|
const SCHEMAS_DIR = path.resolve(__dirname, "..", "schemas")
|
|
@@ -85,10 +88,9 @@ function queryList(cls, limit = 10) {
|
|
|
85
88
|
return sortRecent(filterActive(readJsonl(path.join(classDir(cls), "mistakes.jsonl")))).slice(0, limit)
|
|
86
89
|
}
|
|
87
90
|
const dir = classDir(cls)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return sortRecent(filterActive(entries)).slice(0, limit)
|
|
91
|
+
const index = readJson(path.join(dir, "index.json"), [])
|
|
92
|
+
if (!Array.isArray(index)) return []
|
|
93
|
+
return sortRecent(filterActive(index)).slice(0, limit)
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
function queryGet(cls, id) {
|
|
@@ -96,27 +98,6 @@ function queryGet(cls, id) {
|
|
|
96
98
|
return readJson(path.join(classDir(cls), `${id}.json`), null)
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
function scoreRelevance(r, query, project) {
|
|
100
|
-
const q = query.toLowerCase()
|
|
101
|
-
let score = 0
|
|
102
|
-
const fields = [r.summary, r.id, r.description, r.mission, r.current_state, r.failure, r.root_cause, r.fix, r.prevention, r.command, r.project, r.scope, ...(Array.isArray(r.tags) ? r.tags : []), ...(Array.isArray(r.next_actions) ? r.next_actions : []), ...(Array.isArray(r.refs) ? r.refs : [])].filter(Boolean)
|
|
103
|
-
for (const f of fields) {
|
|
104
|
-
const str = String(f).toLowerCase()
|
|
105
|
-
let idx = 0; let count = 0
|
|
106
|
-
while ((idx = str.indexOf(q, idx)) !== -1) { count++; idx += q.length }
|
|
107
|
-
score += count * 10
|
|
108
|
-
if (str.startsWith(q)) score += 5
|
|
109
|
-
if (str.includes(q)) score += 2
|
|
110
|
-
}
|
|
111
|
-
if (r.project && r.project.toLowerCase() === (project || "").toLowerCase()) score += 20
|
|
112
|
-
if (r.project && project && r.project.toLowerCase().includes(project.toLowerCase())) score += 10
|
|
113
|
-
const age = Date.now() - Date.parse(r.updated_at || r.created_at || 0)
|
|
114
|
-
if (!Number.isNaN(age)) score += Math.max(0, 10 - age / 604800000)
|
|
115
|
-
if (r.status === "active") score += 3
|
|
116
|
-
if (r.status === "closed") score -= 2
|
|
117
|
-
return score
|
|
118
|
-
}
|
|
119
|
-
|
|
120
101
|
function enforceAuditEvidence(record) {
|
|
121
102
|
const prov = isPlainObject(record.provenance) ? record.provenance : {}
|
|
122
103
|
return ["db_refs", "file_refs", "log_refs"].some(k => Array.isArray(prov[k]) && prov[k].some(i => typeof i === "string" && i.trim()))
|
|
@@ -127,22 +108,71 @@ function setToolTitle(context, title) {
|
|
|
127
108
|
context.metadata({ title })
|
|
128
109
|
}
|
|
129
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
|
+
|
|
130
149
|
function handleAdd(cls, id, dataStr) {
|
|
131
150
|
let parsed
|
|
132
|
-
try { parsed = JSON.parse(dataStr) } catch (e) {
|
|
133
|
-
if (!isPlainObject(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
|
+
}
|
|
134
156
|
if (!id?.trim()) return "non-blank id is required"
|
|
157
|
+
id = sanitizeId(id.trim())
|
|
135
158
|
|
|
136
159
|
const now = new Date().toISOString()
|
|
137
|
-
const existing = queryGet(cls, id
|
|
160
|
+
const existing = queryGet(cls, id)
|
|
161
|
+
const existingIsActive = existing && !hasExpired(existing)
|
|
138
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 }
|
|
139
163
|
|
|
140
164
|
const schema = readJson(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
|
|
141
165
|
if (schema) {
|
|
166
|
+
fillSchemaDefaults(record, schema)
|
|
167
|
+
|
|
142
168
|
const required = Array.isArray(schema.required) ? schema.required : []
|
|
143
169
|
if (required.includes("provenance")) {
|
|
144
|
-
if (!record.provenance) record.provenance = { session_id: `auto-${now}` }
|
|
145
|
-
|
|
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
|
+
}
|
|
146
176
|
}
|
|
147
177
|
if (required.includes("trigger") && !record.trigger) record.trigger = "manual"
|
|
148
178
|
if (required.includes("success_count") && record.success_count == null) record.success_count = 0
|
|
@@ -159,13 +189,14 @@ function handleAdd(cls, id, dataStr) {
|
|
|
159
189
|
if (cls === "mistake") upsertMistake(record)
|
|
160
190
|
else writeObject(cls, record)
|
|
161
191
|
|
|
162
|
-
const action =
|
|
192
|
+
const action = existingIsActive ? "updated" : "saved"
|
|
163
193
|
return `${action}: ${id.trim()}`
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
function handleFetch(cls, id) {
|
|
167
197
|
if (!id?.trim()) return "non-blank id is required"
|
|
168
|
-
|
|
198
|
+
id = sanitizeId(id.trim())
|
|
199
|
+
const record = queryGet(cls, id)
|
|
169
200
|
if (!record) return `not found: ${id}`
|
|
170
201
|
return stableStringify(record)
|
|
171
202
|
}
|
|
@@ -190,35 +221,46 @@ function handleSearch(query, scope, classes, project, limit) {
|
|
|
190
221
|
if (!q) return "non-blank query is required"
|
|
191
222
|
const clsList = Array.isArray(classes) && classes.length ? classes : CLASSES
|
|
192
223
|
const lim = Math.min(limit ?? 10, 50)
|
|
193
|
-
let
|
|
224
|
+
let candidates = []
|
|
194
225
|
for (const cls of clsList) {
|
|
195
226
|
if (cls === "mistake") {
|
|
196
227
|
for (const m of readJsonl(path.join(classDir(cls), "mistakes.jsonl"))) {
|
|
197
|
-
if (!hasExpired(m))
|
|
228
|
+
if (!hasExpired(m)) candidates.push({ ...m, _cls: cls })
|
|
198
229
|
}
|
|
199
230
|
} else {
|
|
200
231
|
const dir = classDir(cls)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
for (const
|
|
204
|
-
|
|
205
|
-
if (r && !hasExpired(r)) records.push(r)
|
|
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 })
|
|
206
236
|
}
|
|
207
237
|
}
|
|
208
238
|
}
|
|
209
|
-
if (scope === "global")
|
|
210
|
-
else if (scope === "local")
|
|
211
|
-
|
|
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 || "") }))
|
|
212
245
|
.filter(e => e.score > 0)
|
|
213
246
|
.sort((a, b) => b.score - a.score)
|
|
214
247
|
.slice(0, lim)
|
|
215
|
-
|
|
216
|
-
|
|
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")
|
|
217
258
|
}
|
|
218
259
|
|
|
219
260
|
function handleArchive(cls, id) {
|
|
220
261
|
if (!id?.trim()) return "non-blank id is required"
|
|
221
|
-
|
|
262
|
+
id = sanitizeId(id.trim())
|
|
263
|
+
const record = queryGet(cls, id)
|
|
222
264
|
if (!record) return `not found: ${id}`
|
|
223
265
|
record.status = "expired"
|
|
224
266
|
record.updated_at = new Date().toISOString()
|
|
@@ -227,13 +269,15 @@ function handleArchive(cls, id) {
|
|
|
227
269
|
return `archived: ${id.trim()}`
|
|
228
270
|
}
|
|
229
271
|
|
|
230
|
-
export const MemoryToolsPlugin = async () => {
|
|
272
|
+
export const MemoryToolsPlugin = async (ctx) => {
|
|
231
273
|
fs.mkdirSync(MEMORY_DIR, { recursive: true })
|
|
232
274
|
fs.mkdirSync(getRuntimeRoot(), { recursive: true })
|
|
233
275
|
|
|
276
|
+
const config = loadConfig()
|
|
277
|
+
|
|
234
278
|
return {
|
|
235
279
|
tool: {
|
|
236
|
-
|
|
280
|
+
ohc_save: tool({
|
|
237
281
|
description: "Save a new memory record or update an existing one by class and id",
|
|
238
282
|
args: {
|
|
239
283
|
class: tool.schema.enum(CLASSES),
|
|
@@ -242,11 +286,16 @@ export const MemoryToolsPlugin = async () => {
|
|
|
242
286
|
},
|
|
243
287
|
async execute(args, context) {
|
|
244
288
|
setToolTitle(context, `save ${args.class}: ${args.id}`)
|
|
245
|
-
|
|
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
|
|
246
295
|
},
|
|
247
296
|
}),
|
|
248
297
|
|
|
249
|
-
|
|
298
|
+
ohc_get: tool({
|
|
250
299
|
description: "Get a specific memory record by class and id",
|
|
251
300
|
args: {
|
|
252
301
|
class: tool.schema.enum(CLASSES).describe("Memory class"),
|
|
@@ -258,7 +307,7 @@ export const MemoryToolsPlugin = async () => {
|
|
|
258
307
|
},
|
|
259
308
|
}),
|
|
260
309
|
|
|
261
|
-
|
|
310
|
+
ohc_list: tool({
|
|
262
311
|
description: "List recent memory records by class, sorted by recency",
|
|
263
312
|
args: {
|
|
264
313
|
class: tool.schema.enum(CLASSES).describe("Memory class"),
|
|
@@ -270,7 +319,7 @@ export const MemoryToolsPlugin = async () => {
|
|
|
270
319
|
},
|
|
271
320
|
}),
|
|
272
321
|
|
|
273
|
-
|
|
322
|
+
ohc_latest: tool({
|
|
274
323
|
description: "Get the latest active memory record by class",
|
|
275
324
|
args: {
|
|
276
325
|
class: tool.schema.enum(CLASSES).describe("Memory class"),
|
|
@@ -281,7 +330,7 @@ export const MemoryToolsPlugin = async () => {
|
|
|
281
330
|
},
|
|
282
331
|
}),
|
|
283
332
|
|
|
284
|
-
|
|
333
|
+
ohc_search: tool({
|
|
285
334
|
description: "Search memory records with keyword matching and relevance ranking across all classes",
|
|
286
335
|
args: {
|
|
287
336
|
query: tool.schema.string().describe("Search query string"),
|
|
@@ -296,7 +345,7 @@ export const MemoryToolsPlugin = async () => {
|
|
|
296
345
|
},
|
|
297
346
|
}),
|
|
298
347
|
|
|
299
|
-
|
|
348
|
+
ohc_archive: tool({
|
|
300
349
|
description: "Soft-delete a memory record by setting its status to expired",
|
|
301
350
|
args: {
|
|
302
351
|
class: tool.schema.enum(CLASSES),
|
|
@@ -304,7 +353,11 @@ export const MemoryToolsPlugin = async () => {
|
|
|
304
353
|
},
|
|
305
354
|
async execute(args, context) {
|
|
306
355
|
setToolTitle(context, `archive ${args.class}: ${args.id}`)
|
|
307
|
-
|
|
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
|
|
308
361
|
},
|
|
309
362
|
}),
|
|
310
363
|
},
|
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
}
|