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.
Files changed (158) hide show
  1. package/CONTEXT.md +18 -0
  2. package/ETHOS.md +15 -0
  3. package/README.md +135 -292
  4. package/bootstrap.mjs +174 -499
  5. package/harness/agents/openhermes.md +87 -0
  6. package/harness/codex/CONSTITUTION.md +70 -148
  7. package/harness/codex/ROUTING.md +126 -0
  8. package/harness/commands/oh-doctor.md +26 -0
  9. package/harness/instructions/CONVENTIONS.md +206 -206
  10. package/harness/instructions/RUNTIME.md +54 -31
  11. package/harness/skills/oh-builder/SKILL.md +98 -0
  12. package/harness/skills/oh-caveman/SKILL.md +33 -0
  13. package/harness/skills/oh-expert/SKILL.md +121 -0
  14. package/harness/skills/oh-freeze/SKILL.md +28 -0
  15. package/harness/skills/oh-gauntlet/SKILL.md +119 -0
  16. package/harness/skills/oh-grill/SKILL.md +77 -0
  17. package/harness/skills/oh-guard/SKILL.md +33 -0
  18. package/harness/skills/oh-handoff/SKILL.md +33 -0
  19. package/harness/skills/oh-health/SKILL.md +90 -0
  20. package/harness/skills/oh-init/SKILL.md +78 -0
  21. package/harness/skills/oh-investigate/SKILL.md +35 -0
  22. package/harness/skills/oh-issue/SKILL.md +36 -0
  23. package/harness/skills/oh-learn/SKILL.md +28 -0
  24. package/harness/skills/oh-manifest/SKILL.md +84 -0
  25. package/harness/skills/oh-plan-review/SKILL.md +128 -0
  26. package/harness/skills/oh-planner/SKILL.md +157 -0
  27. package/harness/skills/oh-prd/SKILL.md +35 -0
  28. package/harness/skills/oh-retro/SKILL.md +33 -0
  29. package/harness/skills/oh-review/SKILL.md +110 -0
  30. package/harness/skills/oh-security/SKILL.md +110 -0
  31. package/harness/skills/oh-ship/SKILL.md +39 -0
  32. package/harness/skills/oh-skill-craft/SKILL.md +107 -0
  33. package/harness/skills/oh-skills-link/SKILL.md +29 -0
  34. package/harness/skills/oh-skills-list/SKILL.md +31 -0
  35. package/harness/skills/oh-triage/SKILL.md +36 -0
  36. package/index.mjs +3 -58
  37. package/lib/harness-resolver.mjs +77 -0
  38. package/lib/logger.mjs +62 -0
  39. package/package.json +49 -53
  40. package/test/plugins-behavioral.test.mjs +64 -0
  41. package/test/plugins.test.mjs +62 -0
  42. package/autorecall.mjs +0 -237
  43. package/curator.mjs +0 -455
  44. package/harness/commands/build-fix.md +0 -60
  45. package/harness/commands/checkpoint.md +0 -68
  46. package/harness/commands/code-review.md +0 -71
  47. package/harness/commands/doctor.md +0 -42
  48. package/harness/commands/eval.md +0 -89
  49. package/harness/commands/go-build.md +0 -87
  50. package/harness/commands/go-review.md +0 -71
  51. package/harness/commands/harness-audit.md +0 -90
  52. package/harness/commands/learn.md +0 -37
  53. package/harness/commands/loop-start.md +0 -38
  54. package/harness/commands/loop-status.md +0 -30
  55. package/harness/commands/memory-search.md +0 -37
  56. package/harness/commands/model-route.md +0 -32
  57. package/harness/commands/ohc.md +0 -13
  58. package/harness/commands/orchestrate.md +0 -88
  59. package/harness/commands/plan.md +0 -53
  60. package/harness/commands/quality-gate.md +0 -35
  61. package/harness/commands/refactor-clean.md +0 -102
  62. package/harness/commands/rust-build.md +0 -78
  63. package/harness/commands/rust-review.md +0 -65
  64. package/harness/commands/security.md +0 -93
  65. package/harness/commands/setup-pm.md +0 -65
  66. package/harness/commands/skill-create.md +0 -99
  67. package/harness/commands/test-coverage.md +0 -80
  68. package/harness/commands/update-codemaps.md +0 -81
  69. package/harness/commands/update-docs.md +0 -67
  70. package/harness/commands/verify.md +0 -68
  71. package/harness/prompts/architect.txt +0 -189
  72. package/harness/prompts/build-cpp.md +0 -98
  73. package/harness/prompts/build-error-resolver.md +0 -44
  74. package/harness/prompts/build-go.md +0 -340
  75. package/harness/prompts/build-java.md +0 -140
  76. package/harness/prompts/build-kotlin.md +0 -137
  77. package/harness/prompts/build-rust.md +0 -108
  78. package/harness/prompts/code-reviewer.md +0 -40
  79. package/harness/prompts/doc-updater.md +0 -206
  80. package/harness/prompts/docs-lookup.md +0 -71
  81. package/harness/prompts/e2e-runner.txt +0 -317
  82. package/harness/prompts/explore.md +0 -42
  83. package/harness/prompts/harness-optimizer.md +0 -42
  84. package/harness/prompts/loop-operator.md +0 -53
  85. package/harness/prompts/planner.md +0 -37
  86. package/harness/prompts/refactor-cleaner.md +0 -256
  87. package/harness/prompts/review-cpp.md +0 -81
  88. package/harness/prompts/review-database.md +0 -261
  89. package/harness/prompts/review-go.md +0 -257
  90. package/harness/prompts/review-java.md +0 -113
  91. package/harness/prompts/review-kotlin.md +0 -143
  92. package/harness/prompts/review-python.md +0 -101
  93. package/harness/prompts/review-rust.md +0 -77
  94. package/harness/prompts/security-reviewer.md +0 -42
  95. package/harness/prompts/tdd-guide.md +0 -228
  96. package/harness/rules/audit.md +0 -84
  97. package/harness/rules/checkpointing.md +0 -75
  98. package/harness/rules/context-loading.md +0 -33
  99. package/harness/rules/credential-exposure.md +0 -0
  100. package/harness/rules/delegation.md +0 -80
  101. package/harness/rules/handoff.md +0 -267
  102. package/harness/rules/memory-management.md +0 -28
  103. package/harness/rules/precedence.md +0 -52
  104. package/harness/rules/promotion.md +0 -46
  105. package/harness/rules/ranking.md +0 -64
  106. package/harness/rules/retrieval.md +0 -94
  107. package/harness/rules/runtime-guards.md +0 -196
  108. package/harness/rules/self-heal.md +0 -79
  109. package/harness/rules/session-start.md +0 -34
  110. package/harness/rules/skills-management.md +0 -165
  111. package/harness/rules/state-drift.md +0 -192
  112. package/harness/rules/verification.md +0 -88
  113. package/harness/scripts/sync-commands.mjs +0 -259
  114. package/harness/skills/.bundled_manifest +0 -17
  115. package/harness/skills/.usage.json +0 -6
  116. package/harness/skills/api-design/SKILL.md +0 -523
  117. package/harness/skills/backend-patterns/SKILL.md +0 -598
  118. package/harness/skills/coding-standards/SKILL.md +0 -549
  119. package/harness/skills/e2e-testing/SKILL.md +0 -326
  120. package/harness/skills/frontend-patterns/SKILL.md +0 -642
  121. package/harness/skills/frontend-slides/SKILL.md +0 -184
  122. package/harness/skills/security-review/SKILL.md +0 -495
  123. package/harness/skills/strategic-compact/SKILL.md +0 -131
  124. package/harness/skills/tdd-workflow/SKILL.md +0 -463
  125. package/harness/skills/verification-loop/SKILL.md +0 -126
  126. package/lib/ambient-memory.mjs +0 -167
  127. package/lib/handoff.mjs +0 -176
  128. package/lib/hardening.mjs +0 -128
  129. package/lib/memory-tools-plugin.mjs +0 -365
  130. package/lib/ohc/block-sync.mjs +0 -69
  131. package/lib/ohc/compress/search.mjs +0 -152
  132. package/lib/ohc/compress/state.mjs +0 -76
  133. package/lib/ohc/config.mjs +0 -186
  134. package/lib/ohc/message-ids.mjs +0 -168
  135. package/lib/ohc/notify.mjs +0 -154
  136. package/lib/ohc/protected-patterns.mjs +0 -54
  137. package/lib/ohc/prune-apply.mjs +0 -134
  138. package/lib/ohc/pruner.mjs +0 -610
  139. package/lib/ohc/reaper.mjs +0 -70
  140. package/lib/ohc/state.mjs +0 -266
  141. package/lib/ohc/strategies/deduplication.mjs +0 -72
  142. package/lib/ohc/strategies/index.mjs +0 -2
  143. package/lib/ohc/strategies/purge-errors.mjs +0 -43
  144. package/lib/ohc/token-utils.mjs +0 -26
  145. package/lib/ohc/updater.mjs +0 -133
  146. package/lib/paths.mjs +0 -50
  147. package/lib/schema-validator.mjs +0 -77
  148. package/lib/search.mjs +0 -48
  149. package/schemas/audit.schema.json +0 -82
  150. package/schemas/backlog.schema.json +0 -63
  151. package/schemas/checkpoint.schema.json +0 -65
  152. package/schemas/constraint.schema.json +0 -62
  153. package/schemas/decision.schema.json +0 -63
  154. package/schemas/instinct.schema.json +0 -63
  155. package/schemas/loop-state.schema.json +0 -33
  156. package/schemas/mistake.schema.json +0 -64
  157. package/schemas/verification_receipt.schema.json +0 -88
  158. 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
- }
@@ -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
- }