openhermes 2.8.0 → 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 -512
  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 -60
  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 -482
  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 -171
  128. package/lib/hardening.mjs +0 -146
  129. package/lib/memory-tools-plugin.mjs +0 -368
  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 -185
  134. package/lib/ohc/message-ids.mjs +0 -178
  135. package/lib/ohc/notify.mjs +0 -135
  136. package/lib/ohc/protected-patterns.mjs +0 -55
  137. package/lib/ohc/prune-apply.mjs +0 -134
  138. package/lib/ohc/pruner.mjs +0 -608
  139. package/lib/ohc/reaper.mjs +0 -70
  140. package/lib/ohc/state.mjs +0 -265
  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 -132
  146. package/lib/paths.mjs +0 -49
  147. package/lib/schema-validator.mjs +0 -79
  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,368 +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
- if (cls === "decision") { e.choice = r.choice; e.reason = r.reason }
41
- if (cls === "constraint") { e.description = r.description }
42
- if (cls === "instinct") { e.source = r.source }
43
- if (cls === "checkpoint") { e.mission = r.mission }
44
- if (cls === "verification_receipt") { e.artifact = r.artifact; e.result = r.result }
45
- return e
46
- }
47
-
48
- function hasExpired(r) {
49
- if (r?.status === "expired" || r?.status === "decayed") return true
50
- if (r?.decay_at && Date.parse(r.decay_at) < Date.now()) return true
51
- if (r?.expires_at && Date.parse(r.expires_at) < Date.now()) return true
52
- return false
53
- }
54
-
55
- function sortRecent(entries) {
56
- function ts(e) { return e?.updated_at ?? e?.created_at ?? "" }
57
- return [...entries].sort((a, b) => {
58
- const at = Date.parse(ts(a)), bt = Date.parse(ts(b))
59
- if (!Number.isNaN(at) && !Number.isNaN(bt) && at !== bt) return bt - at
60
- return String(ts(b)).localeCompare(String(ts(a)))
61
- })
62
- }
63
-
64
- function filterActive(entries) { return entries.filter(e => !hasExpired(e)) }
65
-
66
- // CAUTION: read-modify-write — safe within single session; concurrent sessions may race
67
- function writeObject(cls, record) {
68
- const dir = classDir(cls)
69
- fs.mkdirSync(dir, { recursive: true })
70
- const fp = path.join(dir, `${record.id}.json`)
71
- atomicWriteJson(fp, record)
72
- const indexPath = path.join(dir, "index.json")
73
- let index = readJson(indexPath, [])
74
- if (!Array.isArray(index)) index = []
75
- const idx = index.findIndex(e => e?.id === record.id)
76
- const entry = buildEntry(cls, record)
77
- if (idx >= 0) index[idx] = entry; else index.push(entry)
78
- atomicWriteJson(indexPath, index)
79
- }
80
-
81
- function upsertMistake(record) {
82
- const dir = classDir("mistake")
83
- fs.mkdirSync(dir, { recursive: true })
84
- const fp = path.join(dir, "mistakes.jsonl")
85
- let entries = readJsonl(fp)
86
- const idx = entries.findIndex(e => e?.id === record.id)
87
- if (idx >= 0) entries[idx] = record; else entries.push(record)
88
- const text = entries.map(e => stableStringify(e)).join("\n")
89
- const tmp = fp + ".tmp"
90
- fs.writeFileSync(tmp, text ? `${text}\n` : "", "utf8")
91
- fs.renameSync(tmp, fp)
92
- }
93
-
94
- function queryList(cls, limit = 10) {
95
- if (cls === "mistake") {
96
- return sortRecent(filterActive(readJsonl(path.join(classDir(cls), "mistakes.jsonl")))).slice(0, limit)
97
- }
98
- const dir = classDir(cls)
99
- const index = readJson(path.join(dir, "index.json"), [])
100
- if (!Array.isArray(index)) return []
101
- return sortRecent(filterActive(index)).slice(0, limit)
102
- }
103
-
104
- function queryGet(cls, id) {
105
- if (cls === "mistake") return readJsonl(path.join(classDir(cls), "mistakes.jsonl")).find(e => e?.id === id) ?? null
106
- return readJson(path.join(classDir(cls), `${id}.json`), null)
107
- }
108
-
109
- function enforceAuditEvidence(record) {
110
- const prov = isPlainObject(record.provenance) ? record.provenance : {}
111
- return ["db_refs", "file_refs", "log_refs"].some(k => Array.isArray(prov[k]) && prov[k].some(i => typeof i === "string" && i.trim()))
112
- }
113
-
114
- function setToolTitle(context, title) {
115
- if (!context || typeof context.metadata !== "function") return
116
- context.metadata({ title })
117
- }
118
-
119
- function fillSchemaDefaults(record, schema) {
120
- const required = Array.isArray(schema.required) ? schema.required : []
121
- for (const field of required) {
122
- const prop = schema.properties?.[field]
123
- if (!prop) { if (!(field in record)) record[field] = null; continue }
124
-
125
- const types = Array.isArray(prop.type) ? prop.type : [prop.type]
126
- const t = types.find(t => t !== "null") || types[0]
127
-
128
- if (t === "object" && prop.properties) {
129
- if (record[field] == null) record[field] = {}
130
- if (isPlainObject(record[field])) fillSchemaDefaults(record[field], prop)
131
- continue
132
- }
133
-
134
- if (field in record) continue
135
- if (prop.default !== undefined) { record[field] = structuredClone(prop.default); continue }
136
- if (prop.const !== undefined) { record[field] = prop.const; continue }
137
- if (prop.enum) { record[field] = prop.enum[0]; continue }
138
- if (t === "integer" || t === "number") {
139
- record[field] = prop.minimum !== undefined ? prop.minimum : 0
140
- continue
141
- }
142
- switch (t) {
143
- case "string":
144
- record[field] = prop.minLength > 1 ? "x".repeat(prop.minLength) : field
145
- break
146
- case "boolean": record[field] = false; break
147
- case "array": record[field] = []; break
148
- default: record[field] = field; break
149
- }
150
- }
151
- }
152
-
153
- function sanitizeId(raw) {
154
- return raw.replace(/[<>:"/\\|?*]/g, "_").replace(/\.\./g, "_").trim()
155
- }
156
-
157
- function handleAdd(cls, id, dataStr) {
158
- let parsed
159
- try { parsed = JSON.parse(dataStr) } catch (e) { parsed = null }
160
- if (!isPlainObject(parsed)) {
161
- const txt = String(dataStr ?? parsed ?? "").slice(0, 200)
162
- parsed = { summary: txt }
163
- }
164
- if (!id?.trim()) return "non-blank id is required"
165
- id = sanitizeId(id.trim())
166
-
167
- const now = new Date().toISOString()
168
- const existing = queryGet(cls, id)
169
- const existingIsActive = existing && !hasExpired(existing)
170
- 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 }
171
-
172
- const schema = readJson(path.join(SCHEMAS_DIR, `${cls}.schema.json`), null)
173
- if (schema) {
174
- fillSchemaDefaults(record, schema)
175
-
176
- const required = Array.isArray(schema.required) ? schema.required : []
177
- if (required.includes("provenance")) {
178
- if (!record.provenance?.session_id) record.provenance = { ...record.provenance, session_id: `auto-${now}` }
179
- if (cls === "audit" && !enforceAuditEvidence(record)) {
180
- const prov = record.provenance
181
- if (!Array.isArray(prov.file_refs)) prov.file_refs = []
182
- if (!prov.file_refs.some(r => typeof r === "string" && r.trim())) prov.file_refs.push("auto-filled")
183
- }
184
- }
185
-
186
- const unsupported = findUnsupportedSchemaKeywords(schema)
187
- if (unsupported.length) return `unsupported schema: ${unsupported.join(", ")}`
188
- const errs = validateSchema(schema, record, "$")
189
- if (cls === "audit" && !enforceAuditEvidence(record)) errs.push("$.provenance must include at least one non-empty evidence ref")
190
- if (errs.length) return `validation: ${errs.join("; ")}`
191
- }
192
-
193
- if (cls === "mistake") upsertMistake(record)
194
- else writeObject(cls, record)
195
-
196
- const action = existingIsActive ? "updated" : "saved"
197
- return `${action}: ${id.trim()}`
198
- }
199
-
200
- function handleFetch(cls, id) {
201
- if (!id?.trim()) return "non-blank id is required"
202
- id = sanitizeId(id.trim())
203
- const record = queryGet(cls, id)
204
- if (!record) return `not found: ${id}`
205
- return stableStringify(record)
206
- }
207
-
208
- function handleList(cls, limit = 10) {
209
- const entries = queryList(cls, Math.min(limit, 100))
210
- const lines = entries.map(e => ` ${e.id}: ${truncateText(e.summary || "", 80)}`)
211
- return `${entries.length} ${cls} record${entries.length === 1 ? "" : "s"}\n` + lines.join("\n")
212
- }
213
-
214
- function handleLatest(cls) {
215
- const list = queryList(cls, 100)
216
- const active = filterActive(list)
217
- if (!active[0]?.id) return "no active records"
218
- const record = cls === "mistake" ? active[0] : queryGet(cls, active[0].id)
219
- if (!record) return "no active records"
220
- return stableStringify(record)
221
- }
222
-
223
- function handleSearch(query, scope, classes, project, limit) {
224
- const q = (query || "").trim()
225
- if (!q) return "non-blank query is required"
226
- const clsList = Array.isArray(classes) && classes.length ? classes : CLASSES
227
- const lim = Math.min(limit ?? 10, 50)
228
- let candidates = []
229
- for (const cls of clsList) {
230
- if (cls === "mistake") {
231
- for (const m of readJsonl(path.join(classDir(cls), "mistakes.jsonl"))) {
232
- if (!hasExpired(m)) candidates.push({ ...m, _cls: cls })
233
- }
234
- } else {
235
- const dir = classDir(cls)
236
- const index = readJson(path.join(dir, "index.json"), [])
237
- if (!Array.isArray(index)) continue
238
- for (const entry of index) {
239
- if (!hasExpired(entry)) candidates.push({ ...entry, _cls: cls })
240
- }
241
- }
242
- }
243
- if (scope === "global") candidates = candidates.filter(r => r.scope === "global" || !r.scope)
244
- else if (scope === "local") candidates = candidates.filter(r => r.scope === "project" || r.scope === "session")
245
-
246
- // Score using index entries (summary-based)
247
- const scored = candidates
248
- .map(e => ({ ...e, score: scoreRelevance(e, q, project || "") }))
249
- .filter(e => e.score > 0)
250
- .sort((a, b) => b.score - a.score)
251
- .slice(0, lim)
252
-
253
- const results = scored.map(e => {
254
- if (e._cls === "mistake") return e
255
- const full = queryGet(e._cls, e.id)
256
- return full ? { ...full, score: e.score } : e
257
- })
258
-
259
- const lines = results.map(e => ` ${e.id} (${e.score}pt): ${truncateText(e.summary || "", 80)}`)
260
- return `${results.length} result${results.length === 1 ? "" : "s"} for '${q}'\n` + lines.join("\n")
261
- }
262
-
263
- function handleArchive(cls, id) {
264
- if (!id?.trim()) return "non-blank id is required"
265
- id = sanitizeId(id.trim())
266
- const record = queryGet(cls, id)
267
- if (!record) return `not found: ${id}`
268
- record.status = "expired"
269
- record.updated_at = new Date().toISOString()
270
- if (cls === "mistake") upsertMistake(record)
271
- else writeObject(cls, record)
272
- return `archived: ${id.trim()}`
273
- }
274
-
275
- export const MemoryToolsPlugin = async (ctx) => {
276
- fs.mkdirSync(MEMORY_DIR, { recursive: true })
277
- fs.mkdirSync(getRuntimeRoot(), { recursive: true })
278
-
279
- const config = loadConfig()
280
-
281
- return {
282
- tool: {
283
- ohc_save: tool({
284
- description: "Save a new memory record or update an existing one by class and id",
285
- args: {
286
- class: tool.schema.enum(CLASSES),
287
- id: tool.schema.string(),
288
- data: tool.schema.string(),
289
- },
290
- async execute(args, context) {
291
- setToolTitle(context, `save ${args.class}: ${args.id}`)
292
- const result = handleAdd(args.class, args.id, args.data)
293
- if (result.startsWith("saved:") || result.startsWith("updated:")) {
294
- const action = result.startsWith("saved:") ? "Saved" : "Updated"
295
- sendMemoryNotification(ctx?.client, context.sessionID, config, action, args.class, args.id, null).catch(() => {})
296
- }
297
- return result
298
- },
299
- }),
300
-
301
- ohc_get: tool({
302
- description: "Get a specific memory record by class and id",
303
- args: {
304
- class: tool.schema.enum(CLASSES).describe("Memory class"),
305
- id: tool.schema.string().describe("Record ID"),
306
- },
307
- async execute(args, context) {
308
- setToolTitle(context, `fetch ${args.class}: ${args.id}`)
309
- return handleFetch(args.class, args.id)
310
- },
311
- }),
312
-
313
- ohc_list: tool({
314
- description: "List recent memory records by class, sorted by recency",
315
- args: {
316
- class: tool.schema.enum(CLASSES).describe("Memory class"),
317
- limit: tool.schema.number().optional().default(10).describe("Max results (max 100)"),
318
- },
319
- async execute(args, context) {
320
- setToolTitle(context, `list ${args.class}`)
321
- return handleList(args.class, args.limit)
322
- },
323
- }),
324
-
325
- ohc_latest: tool({
326
- description: "Get the latest active memory record by class",
327
- args: {
328
- class: tool.schema.enum(CLASSES).describe("Memory class"),
329
- },
330
- async execute(args, context) {
331
- setToolTitle(context, `latest ${args.class}`)
332
- return handleLatest(args.class)
333
- },
334
- }),
335
-
336
- ohc_search: tool({
337
- description: "Search memory records with keyword matching and relevance ranking across all classes",
338
- args: {
339
- query: tool.schema.string().describe("Search query string"),
340
- scope: tool.schema.enum(["global", "local", "auto"]).optional().default("auto").describe("Search scope"),
341
- classes: tool.schema.array(tool.schema.enum(CLASSES)).optional().describe("Memory classes to search (default: all)"),
342
- project: tool.schema.string().optional().describe("Project filter"),
343
- limit: tool.schema.number().optional().default(10).describe("Max results (max 50)"),
344
- },
345
- async execute(args, context) {
346
- setToolTitle(context, `search: ${truncateText(args.query, 48)}`)
347
- return handleSearch(args.query, args.scope, args.classes, args.project, args.limit)
348
- },
349
- }),
350
-
351
- ohc_archive: tool({
352
- description: "Soft-delete a memory record by setting its status to expired",
353
- args: {
354
- class: tool.schema.enum(CLASSES),
355
- id: tool.schema.string(),
356
- },
357
- async execute(args, context) {
358
- setToolTitle(context, `archive ${args.class}: ${args.id}`)
359
- const result = handleArchive(args.class, args.id)
360
- if (result.startsWith("archived:")) {
361
- sendMemoryNotification(ctx?.client, context.sessionID, config, "Archived", args.class, args.id, null).catch(() => {})
362
- }
363
- return result
364
- },
365
- }),
366
- },
367
- }
368
- }
@@ -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 `[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
- }