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.
Files changed (80) hide show
  1. package/README.md +126 -207
  2. package/autorecall.mjs +79 -12
  3. package/bootstrap.mjs +123 -24
  4. package/curator.mjs +4 -40
  5. package/harness/commands/harness-audit.md +1 -1
  6. package/harness/commands/learn.md +2 -2
  7. package/harness/commands/memory-search.md +2 -2
  8. package/harness/commands/ohc.md +13 -0
  9. package/harness/constitution/soul.md +16 -4
  10. package/harness/instructions/RUNTIME.md +6 -3
  11. package/harness/prompts/architect.txt +14 -0
  12. package/harness/prompts/build-cpp.md +15 -1
  13. package/harness/prompts/build-error-resolver.md +15 -9
  14. package/harness/prompts/build-go.md +14 -0
  15. package/harness/prompts/build-java.md +15 -1
  16. package/harness/prompts/build-kotlin.md +15 -1
  17. package/harness/prompts/build-rust.md +14 -0
  18. package/harness/prompts/code-reviewer.md +15 -9
  19. package/harness/prompts/doc-updater.md +13 -0
  20. package/harness/prompts/docs-lookup.md +11 -0
  21. package/harness/prompts/e2e-runner.txt +12 -0
  22. package/harness/prompts/explore.md +16 -4
  23. package/harness/prompts/harness-optimizer.md +12 -0
  24. package/harness/prompts/loop-operator.md +11 -0
  25. package/harness/prompts/planner.md +15 -9
  26. package/harness/prompts/refactor-cleaner.md +14 -0
  27. package/harness/prompts/review-cpp.md +14 -1
  28. package/harness/prompts/review-database.md +13 -0
  29. package/harness/prompts/review-go.md +13 -0
  30. package/harness/prompts/review-java.md +14 -1
  31. package/harness/prompts/review-kotlin.md +13 -0
  32. package/harness/prompts/review-python.md +14 -1
  33. package/harness/prompts/review-rust.md +13 -0
  34. package/harness/prompts/security-reviewer.md +15 -9
  35. package/harness/prompts/tdd-guide.md +14 -0
  36. package/harness/rules/audit.md +2 -2
  37. package/harness/rules/delegation.md +0 -2
  38. package/harness/rules/handoff.md +267 -0
  39. package/harness/rules/memory-management.md +4 -4
  40. package/harness/rules/precedence.md +1 -1
  41. package/harness/rules/retrieval.md +5 -5
  42. package/harness/rules/runtime-guards.md +1 -1
  43. package/harness/rules/self-heal.md +1 -1
  44. package/harness/rules/session-start.md +5 -5
  45. package/harness/rules/skills-management.md +2 -2
  46. package/harness/rules/verification.md +4 -4
  47. package/harness/scripts/sync-commands.mjs +259 -0
  48. package/index.mjs +6 -2
  49. package/lib/ambient-memory.mjs +167 -0
  50. package/lib/handoff.mjs +176 -0
  51. package/lib/hardening.mjs +13 -8
  52. package/lib/memory-tools-plugin.mjs +107 -54
  53. package/lib/ohc/block-sync.mjs +69 -0
  54. package/lib/ohc/compress/search.mjs +152 -0
  55. package/lib/ohc/compress/state.mjs +76 -0
  56. package/lib/ohc/config.mjs +172 -16
  57. package/lib/ohc/message-ids.mjs +168 -0
  58. package/lib/ohc/notify.mjs +150 -0
  59. package/lib/ohc/protected-patterns.mjs +54 -0
  60. package/lib/ohc/prune-apply.mjs +134 -0
  61. package/lib/ohc/pruner.mjs +406 -55
  62. package/lib/ohc/reaper.mjs +12 -3
  63. package/lib/ohc/state.mjs +246 -15
  64. package/lib/ohc/strategies/deduplication.mjs +72 -0
  65. package/lib/ohc/strategies/index.mjs +2 -0
  66. package/lib/ohc/strategies/purge-errors.mjs +43 -0
  67. package/lib/ohc/token-utils.mjs +26 -0
  68. package/lib/ohc/updater.mjs +36 -13
  69. package/lib/paths.mjs +0 -3
  70. package/lib/search.mjs +48 -0
  71. package/package.json +6 -2
  72. package/schemas/audit.schema.json +22 -1
  73. package/schemas/backlog.schema.json +23 -2
  74. package/schemas/checkpoint.schema.json +23 -2
  75. package/schemas/constraint.schema.json +23 -2
  76. package/schemas/decision.schema.json +23 -2
  77. package/schemas/instinct.schema.json +23 -2
  78. package/schemas/mistake.schema.json +23 -2
  79. package/schemas/verification_receipt.schema.json +23 -2
  80. package/skill-builder.mjs +12 -23
@@ -56,10 +56,10 @@ Self-improving agents rot by saving too much. These rules prevent memory spam:
56
56
 
57
57
  ## Retrieval Implementation
58
58
 
59
- 1. Start with `latest_memory(class)` for the most likely relevant class.
60
- 2. Then use `search_memory(query, classes, project, limit)` with narrow, task-shaped filters.
61
- 3. Use `fetch_memory(class, id)` only for specific records surfaced by step 1 or 2.
62
- 4. Use `list_memory(class, limit)` only when you need a small class sample or a bounded discovery pass.
59
+ 1. Start with `ohc_latest(class)` for the most likely relevant class.
60
+ 2. Then use `ohc_search(query, classes, project, limit)` with narrow, task-shaped filters.
61
+ 3. Use `ohc_get(class, id)` only for specific records surfaced by step 1 or 2.
62
+ 4. Use `ohc_list(class, limit)` only when you need a small class sample or a bounded discovery pass.
63
63
  5. Never read full memory index files for routine task work.
64
64
  6. Read whole indexes only when the task is explicitly about auditing, repairing, or regenerating the index itself.
65
65
  7. For project-level file search with grep/glob patterns: delegate to `explore` subagent.
@@ -69,7 +69,7 @@ Self-improving agents rot by saving too much. These rules prevent memory spam:
69
69
 
70
70
  **NEVER start broad. Always needle-precision first.**
71
71
 
72
- 1. Start with the single most targeted tool for the question: `grep` for a pattern, `glob` for a filename, `latest_memory` for a memory class, `search_memory` with narrow filters.
72
+ 1. Start with the single most targeted tool for the question: `grep` for a pattern, `glob` for a filename, `ohc_latest` for a memory class, `ohc_search` with narrow filters.
73
73
  2. Read the minimum number of files to answer the question — often 1-3, not 16+.
74
74
  3. Stop immediately when you have enough signal to answer.
75
75
  4. Only broaden when every precise method is exhausted and the answer is still missing.
@@ -100,7 +100,7 @@ function detectStateDrift(compressedBuffer) {
100
100
 
101
101
  ## Enforcement Points
102
102
 
103
- ### Memory Write (add_memory)
103
+ ### Memory Write (ohc_save)
104
104
  ```javascript
105
105
  // In openhermes-memory MCP server
106
106
  function putMemoryObject(obj) {
@@ -35,7 +35,7 @@ Self-correction escalates through structured tiers. There is no self-termination
35
35
  - Build failure → `build-error-resolver`
36
36
  - Logic/scope/other → `diagnose` skill + `code-reviewer`
37
37
  - Security → `security-reviewer`
38
- - Config/tool → `openhermes-optimizer` + openhermes audit
38
+ - Config/tool → `harness-optimizer` + openhermes audit
39
39
  2. If structural (affects openhermes behavior across projects), generate a backlog item.
40
40
  3. Run an openhermes audit to check for broken references, stale constraints, or provenance gaps.
41
41
  4. Document findings and updated prevention rules.
@@ -8,13 +8,13 @@ Run this at the start of every new session and every resume before substantive w
8
8
  2. Load openhermes status from `%USERPROFILE%\.config\opencode\ohc.json` if rule paths or memory locations are needed.
9
9
  3. **Read autorecall cache**: If `openhermes\memory\recall\cache.json` exists, load it — it contains active checkpoint, constraints, decisions, and mistakes from the prior session. The autorecall plugin writes this at session start. Use this context before probing MCP tools.
10
10
  4. Check only the smallest relevant curated memory slice in `openhermes\memory\`:
11
- - latest checkpoint via `latest_memory`
12
- - active decisions via `latest_memory` or a narrow `search_memory`
13
- - active constraints via `latest_memory` or a narrow `search_memory`
11
+ - latest checkpoint via `ohc_latest`
12
+ - active decisions via `ohc_latest` or a narrow `ohc_search`
13
+ - active constraints via `ohc_latest` or a narrow `ohc_search`
14
14
  - recent same-type mistakes only if the task matches a known pattern
15
15
  - do not read whole memory indexes unless the task is explicitly about index auditing or repair
16
16
  5. If no relevant memory exists, proceed fresh without pretending there is prior state.
17
- 6. If last openhermes audit is missing or older than 7 days, flag `/openhermes-audit` as due.
17
+ 6. If last openhermes audit is missing or older than 7 days, flag `/harness-audit` as due.
18
18
  7. Before substantial work, choose the smallest correct path:
19
19
  - native read/grep/glob for search/gather
20
20
  - `explore` subagent for multi-file analysis
@@ -23,7 +23,7 @@ Run this at the start of every new session and every resume before substantive w
23
23
  ## User Entry Points
24
24
 
25
25
  - `/openhermes`: bootstrap openhermes state, summarize current readiness, and surface due actions.
26
- - `/openhermes-audit`: run an openhermes audit workflow and return findings.
26
+ - `/harness-audit`: run an openhermes audit workflow and return findings.
27
27
 
28
28
  ## Output Contract
29
29
 
@@ -130,7 +130,7 @@ The agent can create, update, and delete skills during sessions. This is the ski
130
130
 
131
131
  - Never create a skill from a single data point.
132
132
  - Minimum: 3 verified successes or 3 same-type mistakes in 7 days.
133
- - Check existing skills via `search_memory` before creating to avoid duplicates.
133
+ - Check existing skills via `ohc_search` before creating to avoid duplicates.
134
134
 
135
135
  ### Skill Quality Gates
136
136
 
@@ -162,4 +162,4 @@ Skills live in three locations (discovered by OpenCode):
162
162
  After creating or updating a skill:
163
163
  1. Run the workflow defined in the SKILL.md.
164
164
  2. Verify it produces the expected outcome.
165
- 3. Write a verification receipt via `add_memory` with class `verification_receipt`.
165
+ 3. Write a verification receipt via `ohc_save` with class `verification_receipt`.
@@ -14,7 +14,7 @@ Verification receipts prove that an artifact was observed in a particular state.
14
14
 
15
15
  ## Verification Cache (Memory-Backed)
16
16
 
17
- Successful verifications are stored via `add_memory` so repeated checks of unchanged artifacts are skipped.
17
+ Successful verifications are stored via `ohc_save` so repeated checks of unchanged artifacts are skipped.
18
18
 
19
19
  ### Cache Key
20
20
 
@@ -24,14 +24,14 @@ Each verification receipt is keyed by:
24
24
 
25
25
  ### Cache Lifecycle
26
26
 
27
- 1. **Before trusting a claim**: search memory (`fetch_memory` or `list_memory`) for matching receipt.
27
+ 1. **Before trusting a claim**: search memory (`ohc_get` or `ohc_list`) for matching receipt.
28
28
  2. **Receipt found + fingerprint matches**: artifact unchanged. Trust cached result. Skip re-verify.
29
29
  3. **Receipt found + fingerprint differs**: artifact changed. Re-verify. Stale receipt is invalid.
30
30
  4. **No receipt found**: verify fresh. Store receipt on success.
31
31
 
32
32
  ### Receipt Storage
33
33
 
34
- Use `add_memory` with class `verification_receipt` — a dedicated memory class (schema: `schemas\verification_receipt.schema.json`). Receipts are stored as file-per-object in `memory\verification_receipts\<id>.json`.
34
+ Use `ohc_save` with class `verification_receipt` — a dedicated memory class (schema: `schemas\verification_receipt.schema.json`). Receipts are stored as file-per-object in `memory\verification_receipts\<id>.json`.
35
35
 
36
36
  Required fields:
37
37
  - **artifact**: path or logical identity of the verified artifact
@@ -69,7 +69,7 @@ Receipts stored under `decision` with `v:` prefix are deprecated. Migrate to `ve
69
69
  When verification reveals evidence contradicts a document or user claim:
70
70
 
71
71
  1. **Pause** — do not proceed on either source.
72
- 2. **Log** — `add_memory` as `constraint` or `backlog` with both claim and contradictory evidence.
72
+ 2. **Log** — `ohc_save` as `constraint` or `backlog` with both claim and contradictory evidence.
73
73
  3. **Flag** — ask user about the discrepancy. Present both sides.
74
74
  4. **Resolve** — let user decide which source is authoritative. Update document if needed.
75
75
 
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const REPO_ROOT = path.resolve(__dirname, "..", "..")
8
+ const COMMANDS_DIR = path.join(REPO_ROOT, "harness", "commands")
9
+ const BOOTSTRAP_FILE = path.join(REPO_ROOT, "bootstrap.mjs")
10
+ const README_FILE = path.join(REPO_ROOT, "README.md")
11
+
12
+ const COMMANDS_START = "<!-- COMMANDS:START -->"
13
+ const COMMANDS_END = "<!-- COMMANDS:END -->"
14
+
15
+ const HOOK_ONLY = new Set(["update-me"])
16
+
17
+ function parseRegistry(src) {
18
+ const commands = {}
19
+ const blockMatch = src.match(/config\.command\s*=\s*\{([\s\S]*?)\n\s*\}/)
20
+ if (!blockMatch) return commands
21
+
22
+ let rest = blockMatch[1]
23
+ const entryRe = /^\s*"([^"]+)"\s*:\s*\{([\s\S]*?)\},?\s*$/gm
24
+ let m
25
+ while ((m = entryRe.exec(rest)) !== null) {
26
+ const name = m[1]
27
+ const body = m[2]
28
+ const agent = body.match(/agent:\s*"([^"]+)"/)?.[1] ?? ""
29
+ const description = body.match(/description:\s*"([^"]+)"/)?.[1] ?? ""
30
+ const template = body.match(/template:\s*ct\("([^"]+)"\)/) ? m[0].match(/template:\s*(ct\("[^"]+"\)|""|'')/)?.[1] : ""
31
+ const hasFile = template.includes("ct(")
32
+ commands[name] = { name, agent, description, hasFile }
33
+ }
34
+ return commands
35
+ }
36
+
37
+ function parseCommandFile(filePath) {
38
+ const src = fs.readFileSync(filePath, "utf8")
39
+ const name = path.basename(filePath, ".md")
40
+ const description = src.match(/^description:\s*(.+)$/m)?.[1] ?? ""
41
+ const agent = src.match(/^agent:\s*(.+)$/m)?.[1] ?? ""
42
+ return { name, description, agent }
43
+ }
44
+
45
+ function scanCommandFiles() {
46
+ const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith(".md"))
47
+ const map = {}
48
+ for (const f of files) {
49
+ const parsed = parseCommandFile(path.join(COMMANDS_DIR, f))
50
+ map[parsed.name] = parsed
51
+ }
52
+ return map
53
+ }
54
+
55
+ function getHookCommands() {
56
+ return [...HOOK_ONLY].map(name => ({ name, agent: "", description: "Hook-handled (see plugin source)" }))
57
+ }
58
+
59
+ function printReport(registry, filesOnDisk, hookCommands, cmdNames) {
60
+ console.log("")
61
+ console.log(" COMMAND SYNC REPORT")
62
+ console.log(" " + "=".repeat(50))
63
+ const registered = new Set(Object.keys(registry))
64
+ const onDisk = new Set(Object.keys(filesOnDisk))
65
+ const allHooks = new Set(hookCommands.map(h => h.name))
66
+ const hookNoReg = [...allHooks].filter(n => !registered.has(n) && !onDisk.has(n))
67
+
68
+ console.log(` Registered in bootstrap.mjs: ${registered.size}`)
69
+ console.log(` Files in harness/commands/: ${onDisk.size}`)
70
+ console.log(` Hook-only (no registry): ${hookNoReg.length}`)
71
+ console.log(` Total real commands: ${cmdNames.length}`)
72
+ console.log("")
73
+
74
+ const registeredNoFile = [...registered].filter(n => !onDisk.has(n))
75
+ const fileNotRegistered = [...onDisk].filter(n => !registered.has(n) && !allHooks.has(n))
76
+
77
+ if (registeredNoFile.length) {
78
+ console.log(" ⚠ REGISTERED, NO FILE:")
79
+ for (const n of registeredNoFile) {
80
+ const e = registry[n]
81
+ console.log(` /${n} — ${e.description}`)
82
+ }
83
+ console.log("")
84
+ }
85
+
86
+ if (fileNotRegistered.length) {
87
+ console.log(" ⚠ FILE EXISTS, NOT REGISTERED:")
88
+ for (const n of fileNotRegistered) {
89
+ console.log(` /${n}`)
90
+ }
91
+ console.log("")
92
+ }
93
+
94
+ if (hookNoReg.length) {
95
+ console.log(" ℹ HOOK-ONLY (no registry, no file):")
96
+ for (const n of hookNoReg) {
97
+ console.log(` /${n}`)
98
+ }
99
+ console.log("")
100
+ }
101
+
102
+ if (!registeredNoFile.length && !fileNotRegistered.length) {
103
+ console.log(" ✓ Everything in sync — no gaps.")
104
+ console.log("")
105
+ }
106
+ }
107
+
108
+ function generateMissingFiles(registry, filesOnDisk) {
109
+ let count = 0
110
+ for (const [name, entry] of Object.entries(registry)) {
111
+ if (filesOnDisk[name]) continue
112
+
113
+ const templateFile = `${name}.md`
114
+ const agent = entry.agent || "OpenHermes"
115
+ const desc = entry.description
116
+
117
+ let md = `---
118
+ description: ${desc}
119
+ agent: ${agent}
120
+ subtask: true
121
+ ---
122
+
123
+ # ${name.charAt(0).toUpperCase() + name.slice(1)} Command
124
+
125
+ `
126
+
127
+ if (name === "ohc") {
128
+ md += `OHC context management — hook-handled.\n\nRun \`/ohc status\` to check context usage, \`/ohc compress [targetTokens] [focus]\` to free space.\n\n$ARGUMENTS\n`
129
+ } else {
130
+ md += `${desc}: $ARGUMENTS\n`
131
+ }
132
+
133
+ fs.writeFileSync(path.join(COMMANDS_DIR, templateFile), md, "utf8")
134
+ console.log(` ✓ Created harness/commands/${templateFile}`)
135
+ count++
136
+ }
137
+
138
+ if (count === 0) console.log(" No missing files to generate.")
139
+ return count
140
+ }
141
+
142
+ function getCommandNames(registry, hookCommands) {
143
+ const registered = new Set(Object.keys(registry))
144
+ const hookOnly = hookCommands.filter(h => !registered.has(h.name)).map(h => h.name)
145
+ const names = [...registered, ...hookOnly]
146
+ names.sort()
147
+ return names
148
+ }
149
+
150
+ function formatCommandRow(names) {
151
+ const count = names.length
152
+ const list = names.map(n => `\`/${n}\``).join(", ")
153
+ return `| **${count} slash commands** | ${list} |`
154
+ }
155
+
156
+ function formatBacktickList(names) {
157
+ return ` ${names.map(n => `\\\`/${n}\\\``).join(", ")}`
158
+ }
159
+
160
+ function syncBootstrap(registry, hookCommands) {
161
+ let src = fs.readFileSync(BOOTSTRAP_FILE, "utf8")
162
+ const cmdNames = getCommandNames(registry, hookCommands)
163
+ const row = formatCommandRow(cmdNames)
164
+
165
+ const startIdx = src.indexOf(COMMANDS_START)
166
+ const endIdx = src.indexOf(COMMANDS_END)
167
+
168
+ if (startIdx === -1 || endIdx === -1) {
169
+ console.log(" ✗ Markers not found in bootstrap.mjs. Inserting markers.")
170
+ const plugRowRe = /(\|\s+\*\*Plugins\s*\*\*.*\|)/
171
+ const match = src.match(plugRowRe)
172
+ if (!match) {
173
+ console.log(" ✗ Could not find Plugins row to anchor insertion.")
174
+ return false
175
+ }
176
+ const insertAfter = match.index + match[0].length
177
+ const newRow = `\n| **Slash commands** | ${COMMANDS_START} ${COMMANDS_END} |`
178
+ src = src.slice(0, insertAfter) + newRow + src.slice(insertAfter)
179
+ // Re-find markers for content replacement below
180
+ const newStart = src.indexOf(COMMANDS_START)
181
+ const newEnd = src.indexOf(COMMANDS_END)
182
+ if (newStart !== -1 && newEnd !== -1 && newEnd > newStart) {
183
+ const content = formatBacktickList(cmdNames)
184
+ src = src.slice(0, newStart + COMMANDS_START.length) + content + src.slice(newEnd)
185
+ }
186
+ fs.writeFileSync(BOOTSTRAP_FILE, src, "utf8")
187
+ console.log(" ✓ Inserted Slash commands row in bootstrap.mjs")
188
+ return true
189
+ }
190
+
191
+ if (endIdx <= startIdx) {
192
+ console.log(" ✗ Markers out of order in bootstrap.mjs")
193
+ return false
194
+ }
195
+
196
+ const content = formatBacktickList(cmdNames)
197
+ src = src.slice(0, startIdx + COMMANDS_START.length) + content + src.slice(endIdx)
198
+ fs.writeFileSync(BOOTSTRAP_FILE, src, "utf8")
199
+ console.log(" ✓ Updated Slash commands row in bootstrap.mjs")
200
+ return true
201
+ }
202
+
203
+ function syncReadme(registry, hookCommands) {
204
+ let src = fs.readFileSync(README_FILE, "utf8")
205
+ const cmdNames = getCommandNames(registry, hookCommands)
206
+ const row = formatCommandRow(cmdNames)
207
+
208
+ const tableRowRe = /^\|\s*\*\*(\d+)\s*slash commands\s*\*\*.*\|$/m
209
+ const match = src.match(tableRowRe)
210
+ if (!match) {
211
+ console.log(" ✗ Could not find slash commands row in README.md")
212
+ return false
213
+ }
214
+
215
+ src = src.replace(tableRowRe, row)
216
+ fs.writeFileSync(README_FILE, src, "utf8")
217
+ console.log(` ✓ Updated README.md: ${cmdNames.length} commands`)
218
+ return true
219
+ }
220
+
221
+ // Main
222
+ const args = process.argv.slice(2)
223
+ const doAudit = args.includes("--audit") || args.length === 0
224
+ const doGenerate = args.includes("--generate") || args.length === 0
225
+ const doSyncBootstrap = args.includes("--sync-bootstrap") || args.includes("--sync") || args.length === 0
226
+ const doSyncReadme = args.includes("--sync-readme") || args.includes("--sync") || args.length === 0
227
+
228
+ console.log(" ▸ syncing command definitions...")
229
+ console.log("")
230
+
231
+ const bootstrapSrc = fs.readFileSync(BOOTSTRAP_FILE, "utf8")
232
+ const registry = parseRegistry(bootstrapSrc)
233
+ const filesOnDisk = scanCommandFiles()
234
+ const hookCommands = getHookCommands()
235
+ const cmdNames = getCommandNames(registry, hookCommands)
236
+
237
+ if (doAudit) printReport(registry, filesOnDisk, hookCommands, cmdNames)
238
+
239
+ if (doGenerate) {
240
+ console.log(" [generate]")
241
+ generateMissingFiles(registry, filesOnDisk)
242
+ console.log("")
243
+ }
244
+
245
+ if (doSyncBootstrap) {
246
+ console.log(" [sync-bootstrap]")
247
+ const ok = syncBootstrap(registry, hookCommands)
248
+ if (!ok) process.exitCode = 1
249
+ console.log("")
250
+ }
251
+
252
+ if (doSyncReadme) {
253
+ console.log(" [sync-readme]")
254
+ const ok = syncReadme(registry, hookCommands)
255
+ if (!ok) process.exitCode = 1
256
+ console.log("")
257
+ }
258
+
259
+ console.log(" done.")
package/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { CuratorPlugin } from "./curator.mjs"
3
3
  import { SkillBuilderPlugin } from "./skill-builder.mjs"
4
4
  import { BootstrapPlugin } from "./bootstrap.mjs"
5
5
  import { MemoryToolsPlugin } from "./lib/memory-tools-plugin.mjs"
6
+ import { AmbientMemoryPlugin } from "./lib/ambient-memory.mjs"
6
7
  import { OhcPlugin } from "./lib/ohc/pruner.mjs"
7
8
  import { UpdaterPlugin } from "./lib/ohc/updater.mjs"
8
9
 
@@ -14,15 +15,17 @@ function chain(...fns) {
14
15
  }
15
16
 
16
17
  export default async (input) => {
17
- const [bootstrap, autorecall, curator, skillBuilder, memoryTools, ohc, updater] = await Promise.all([
18
+ const results = await Promise.allSettled([
18
19
  BootstrapPlugin(input),
19
20
  AutorecallPlugin(input),
20
21
  CuratorPlugin(input),
21
22
  SkillBuilderPlugin(input),
22
23
  MemoryToolsPlugin(input),
24
+ AmbientMemoryPlugin(input),
23
25
  OhcPlugin(input),
24
26
  UpdaterPlugin(input),
25
27
  ])
28
+ const [bootstrap, autorecall, curator, skillBuilder, memoryTools, ambient, ohc, updater] = results.map(r => r.status === 'fulfilled' ? r.value : {})
26
29
 
27
30
  const merged = {}
28
31
 
@@ -33,6 +36,7 @@ export default async (input) => {
33
36
 
34
37
  merged["experimental.chat.system.transform"] = chain(ohc["experimental.chat.system.transform"])
35
38
  merged["experimental.chat.messages.transform"] = chain(
39
+ ambient["experimental.chat.messages.transform"],
36
40
  bootstrap["experimental.chat.messages.transform"],
37
41
  ohc["experimental.chat.messages.transform"],
38
42
  )
@@ -41,7 +45,7 @@ export default async (input) => {
41
45
  const eventHandlers = [autorecall.event, curator.event, skillBuilder.event].filter(Boolean)
42
46
  if (eventHandlers.length) {
43
47
  merged.event = async (payload) => {
44
- await Promise.all(eventHandlers.map(fn => fn(payload)))
48
+ await Promise.allSettled(eventHandlers.map(fn => fn(payload)))
45
49
  }
46
50
  }
47
51
 
@@ -0,0 +1,167 @@
1
+ import path from "node:path"
2
+ import fs from "node:fs"
3
+ import { readJson, readJsonl, truncateText } from "./hardening.mjs"
4
+ import { getMemoryRoot, getRecallRoot, getDataRoot } from "./paths.mjs"
5
+ import { scoreRelevance } from "./search.mjs"
6
+
7
+ const PLURALS = { audit: "audits", checkpoint: "checkpoints", mistake: "mistakes", instinct: "instincts", decision: "decisions", constraint: "constraints", backlog: "backlog", verification_receipt: "verification_receipts" }
8
+ const CLASSES = ["audit", "checkpoint", "mistake", "instinct", "decision", "constraint", "backlog", "verification_receipt"]
9
+
10
+ const MEMORY_QUERY_PATTERNS = [
11
+ /where\s+(did|do)\s+we\s+(leave\s+off|left\s+off)/i,
12
+ /where\s+were\s+we/i,
13
+ /what\s+(was|is)\s+(the\s+)?(last|latest|most\s+recent)\s+(checkpoint|decision|constraint|feature|task|change|issue|fix|bug|request|request)/i,
14
+ /remind\s+me\s+(about|of)/i,
15
+ /what\s+were\s+we\s+(working\s+on|doing)/i,
16
+ /what\s+(happened|changed)\s+(last|yesterday|before|previously)/i,
17
+ /previous\s+session/i,
18
+ /last\s+time/i,
19
+ /recap/i,
20
+ /what\s+is\s+(the\s+)?(status|state)\s+of/i,
21
+ /did\s+we\s+(decide|agree|finish|resolve|discuss)/i,
22
+ /where\s+(did|do)\s+i\s+(leave\s+off|left\s+off)/i,
23
+ ]
24
+
25
+ function hasExpired(r) {
26
+ if (r?.status === "expired" || r?.status === "decayed") return true
27
+ if (r?.decay_at && Date.parse(r.decay_at) < Date.now()) return true
28
+ if (r?.expires_at && Date.parse(r.expires_at) < Date.now()) return true
29
+ return false
30
+ }
31
+
32
+ function filterActive(entries) { return entries.filter(e => !hasExpired(e)) }
33
+
34
+ function detectMemoryQuery(text) {
35
+ return MEMORY_QUERY_PATTERNS.some(p => p.test(text))
36
+ }
37
+
38
+ function extractSearchTerms(text) {
39
+ const cleaned = text.replace(/<[^>]+>/g, "").replace(/[^\w\s-]/g, " ").trim()
40
+ return cleaned.slice(0, 200)
41
+ }
42
+
43
+ function classDir(cls) { return path.join(getMemoryRoot(), PLURALS[cls]) }
44
+
45
+ function readAllRecords() {
46
+ const records = []
47
+ for (const cls of CLASSES) {
48
+ if (cls === "mistake") {
49
+ for (const m of readJsonl(path.join(classDir(cls), "mistakes.jsonl"))) {
50
+ if (!hasExpired(m)) records.push({ ...m, _cls: cls })
51
+ }
52
+ } else {
53
+ const dir = classDir(cls)
54
+ const index = readJson(path.join(dir, "index.json"), [])
55
+ if (!Array.isArray(index)) continue
56
+ for (const entry of index) {
57
+ if (!hasExpired(entry)) {
58
+ records.push({ ...entry, _cls: cls })
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return records
64
+ }
65
+
66
+ function autoSearch(query) {
67
+ const q = (query || "").trim()
68
+ if (!q) return []
69
+
70
+ const records = readAllRecords()
71
+ const scored = records
72
+ .map(r => ({ ...r, _score: scoreRelevance(r, q, "") }))
73
+ .filter(r => r._score > 0)
74
+ .sort((a, b) => b._score - a._score)
75
+ .slice(0, 5)
76
+
77
+ return scored
78
+ }
79
+
80
+ function buildMemoryBlock() {
81
+ const parts = []
82
+
83
+ const ckIndex = readJson(path.join(getMemoryRoot(), "checkpoints", "index.json"), [])
84
+ if (Array.isArray(ckIndex) && ckIndex.length > 0) {
85
+ const latest = [...ckIndex].sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))[0]
86
+ if (latest) {
87
+ const cp = readJson(path.join(getMemoryRoot(), "checkpoints", `${latest.id}.json`), null)
88
+ if (cp) {
89
+ parts.push(`Checkpoint: ${cp.summary || "N/A"}`)
90
+ if (cp.mission) parts.push(`Mission: ${truncateText(cp.mission, 200)}`)
91
+ if (Array.isArray(cp.next_actions) && cp.next_actions.length) parts.push(`Next: ${cp.next_actions.slice(0, 2).join("; ")}`)
92
+ }
93
+ }
94
+ }
95
+
96
+ const ctIndex = readJson(path.join(getMemoryRoot(), "constraints", "index.json"), [])
97
+ if (Array.isArray(ctIndex)) {
98
+ const active = ctIndex.filter(e => e.status === "active")
99
+ if (active.length) parts.push(`Constraints (${active.length} active)`)
100
+ }
101
+
102
+ const dcIndex = readJson(path.join(getMemoryRoot(), "decisions", "index.json"), [])
103
+ if (Array.isArray(dcIndex)) {
104
+ const recent = filterActive(dcIndex).slice(0, 2)
105
+ if (recent.length) parts.push(`Recent decisions: ${recent.map(d => truncateText(d.summary || "", 80)).join(" | ")}`)
106
+ }
107
+
108
+ const bkIndex = readJson(path.join(getMemoryRoot(), "backlog", "index.json"), [])
109
+ if (Array.isArray(bkIndex)) {
110
+ const open = bkIndex.filter(e => e.status === "open")
111
+ if (open.length) parts.push(`Backlog: ${open.length} open`)
112
+ }
113
+
114
+ return parts.length ? parts.join("\n") : "No active memory records found."
115
+ }
116
+
117
+ export const AmbientMemoryPlugin = async () => ({
118
+ "experimental.chat.messages.transform": async (_input, output) => {
119
+ if (!output.messages?.length) return
120
+
121
+ const firstUser = output.messages.find(m => m.info?.role === "user")
122
+ if (!firstUser?.parts?.length) return
123
+
124
+ const hasMemoryTag = firstUser.parts.some(p => p.type === "text" && p.text.includes("OPENHERMES_MEMORY"))
125
+
126
+ if (!hasMemoryTag) {
127
+ const block = buildMemoryBlock()
128
+ const tag = `<OPENHERMES_MEMORY>\n${block}\n</OPENHERMES_MEMORY>`
129
+ const textPart = firstUser.parts.find(p => p.type === "text")
130
+ if (textPart) {
131
+ textPart.text = `${tag}\n\n${textPart.text}`
132
+ }
133
+ }
134
+
135
+ const lastUser = [...output.messages].reverse().find(m => m.info?.role === "user")
136
+ if (!lastUser) return
137
+
138
+ const text = lastUser.parts?.find(p => p.type === "text")?.text || ""
139
+ if (!text || !detectMemoryQuery(text)) return
140
+
141
+ const terms = extractSearchTerms(text)
142
+ if (!terms) return
143
+
144
+ const results = autoSearch(terms)
145
+ if (!results.length) return
146
+
147
+ const contextBlock = [
148
+ `<memory-context>`,
149
+ `[System note: auto-retrieved from durable memory — NOT new user input]`,
150
+ ...results.slice(0, 3).map(r => {
151
+ const cls = r.class || "?";
152
+ const label = `${cls}:${r.id}`
153
+ return `- ${label}: ${truncateText(r.summary || "", 120)}`
154
+ }),
155
+ results.length > 3 ? ` ... and ${results.length - 3} more results` : null,
156
+ `</memory-context>`,
157
+ ].filter(Boolean).join("\n")
158
+
159
+ const idx = output.messages.indexOf(lastUser)
160
+ if (idx > 0) {
161
+ output.messages.splice(idx, 0, {
162
+ parts: [{ type: "text", text: contextBlock }],
163
+ info: { role: "system" },
164
+ })
165
+ }
166
+ }
167
+ })