hmem-mcp 6.3.1 → 6.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-context-inject.js +8 -1
- package/dist/cli-context-inject.js.map +1 -1
- package/dist/cli-init.js +96 -1
- package/dist/cli-init.js.map +1 -1
- package/dist/cli-log-exchange.js +9 -4
- package/dist/cli-log-exchange.js.map +1 -1
- package/dist/hmem-config.d.ts +7 -1
- package/dist/hmem-config.js +4 -1
- package/dist/hmem-config.js.map +1 -1
- package/dist/hmem-store.d.ts +10 -31
- package/dist/hmem-store.js +22 -187
- package/dist/hmem-store.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +133 -479
- package/dist/mcp-server.js.map +1 -1
- package/opencode-plugin/hmem.js +105 -0
- package/package.json +2 -1
- package/skills/hmem-curate/SKILL.md +158 -134
- package/skills/hmem-migrate-o/SKILL.md +1 -1
- package/skills/hmem-read/SKILL.md +1 -1
- package/skills/hmem-release/SKILL.md +2 -3
- package/skills/hmem-wipe/SKILL.md +6 -4
- package/skills/hmem-write/SKILL.md +28 -5
- package/skills/hmem-self-curate/SKILL.md +0 -194
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hmem OpenCode plugin
|
|
3
|
+
*
|
|
4
|
+
* Bridges OpenCode's plugin event system into hmem's CLI hook commands.
|
|
5
|
+
* Drop this file into ~/.config/opencode/plugins/ (global) or
|
|
6
|
+
* .opencode/plugins/ (project-local) and OpenCode will auto-load it.
|
|
7
|
+
*
|
|
8
|
+
* Mapping:
|
|
9
|
+
* chat.message → capture user prompt + write session marker
|
|
10
|
+
* event: message.part.updated → accumulate assistant text per session
|
|
11
|
+
* event: session.idle → spawn `hmem log-exchange` + `hmem checkpoint`
|
|
12
|
+
* experimental.session.compacting → inject hmem context into compaction prompt
|
|
13
|
+
*
|
|
14
|
+
* Requires `hmem` CLI on PATH and HMEM_PATH env var (auto-detected if unset).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
function spawnHmem(args, stdinJson) {
|
|
20
|
+
try {
|
|
21
|
+
const child = spawn("hmem", args, {
|
|
22
|
+
detached: true,
|
|
23
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
24
|
+
env: process.env,
|
|
25
|
+
});
|
|
26
|
+
if (stdinJson) {
|
|
27
|
+
child.stdin.end(stdinJson);
|
|
28
|
+
} else {
|
|
29
|
+
child.stdin.end();
|
|
30
|
+
}
|
|
31
|
+
child.unref();
|
|
32
|
+
} catch {
|
|
33
|
+
// hmem not installed or not on PATH — silently no-op
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function partsToText(parts) {
|
|
38
|
+
if (!Array.isArray(parts)) return "";
|
|
39
|
+
return parts
|
|
40
|
+
.filter(p => p && p.type === "text" && typeof p.text === "string")
|
|
41
|
+
.map(p => p.text)
|
|
42
|
+
.join("\n")
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default {
|
|
47
|
+
id: "hmem",
|
|
48
|
+
server: async () => {
|
|
49
|
+
const userMessages = new Map();
|
|
50
|
+
const assistantBuffers = new Map();
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"chat.message": async ({ sessionID }, { parts }) => {
|
|
54
|
+
const text = partsToText(parts);
|
|
55
|
+
if (text) userMessages.set(sessionID, text);
|
|
56
|
+
assistantBuffers.set(sessionID, "");
|
|
57
|
+
spawnHmem(["hook-startup"], JSON.stringify({ session_id: sessionID }));
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
event: async ({ event }) => {
|
|
61
|
+
if (!event || typeof event !== "object") return;
|
|
62
|
+
|
|
63
|
+
if (event.type === "message.part.updated") {
|
|
64
|
+
const part = event.properties?.part;
|
|
65
|
+
if (!part || part.type !== "text" || typeof part.text !== "string") return;
|
|
66
|
+
const sid = part.sessionID;
|
|
67
|
+
if (!sid) return;
|
|
68
|
+
assistantBuffers.set(sid, part.text);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (event.type === "session.idle") {
|
|
73
|
+
const sid = event.properties?.sessionID;
|
|
74
|
+
if (!sid) return;
|
|
75
|
+
const userMessage = userMessages.get(sid);
|
|
76
|
+
const assistantMessage = assistantBuffers.get(sid);
|
|
77
|
+
if (!userMessage || !assistantMessage) return;
|
|
78
|
+
userMessages.delete(sid);
|
|
79
|
+
assistantBuffers.delete(sid);
|
|
80
|
+
spawnHmem(["log-exchange"], JSON.stringify({
|
|
81
|
+
session_id: sid,
|
|
82
|
+
last_user_message: userMessage,
|
|
83
|
+
last_assistant_message: assistantMessage,
|
|
84
|
+
}));
|
|
85
|
+
spawnHmem(["checkpoint"], JSON.stringify({ session_id: sid }));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
"experimental.session.compacting": async ({ sessionID }, output) => {
|
|
90
|
+
try {
|
|
91
|
+
const result = spawnSync("hmem", ["context-inject"], {
|
|
92
|
+
input: JSON.stringify({ session_id: sessionID }),
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
timeout: 5000,
|
|
95
|
+
env: process.env,
|
|
96
|
+
});
|
|
97
|
+
const text = (result.stdout || "").trim();
|
|
98
|
+
if (text) output.context.push(text);
|
|
99
|
+
} catch {
|
|
100
|
+
// hmem not available — skip
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hmem-mcp",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.2",
|
|
4
4
|
"description": "Humanlike memory for AI agents — MCP server with 5-level lazy-loaded SQLite memory",
|
|
5
5
|
"author": "Bumblebiber",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"dist",
|
|
33
33
|
"skills",
|
|
34
34
|
"scripts",
|
|
35
|
+
"opencode-plugin",
|
|
35
36
|
"prebuilds",
|
|
36
37
|
"server.json",
|
|
37
38
|
"README.md",
|
|
@@ -1,206 +1,213 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hmem-curate
|
|
3
|
-
description:
|
|
4
|
-
Memory curation workflow. Use when asked to curate, audit, or clean up agent memories.
|
|
5
|
-
Processes one agent at a time — read, fix, mark audited, summarize, terminate.
|
|
6
|
-
Requires role: ceo.
|
|
3
|
+
description: Curate hmem memory — your own or a foreign .hmem file. Systematically review entries, mark obsolete/irrelevant/favorite, fix titles, consolidate duplicates. Use when asked to "aufräumen", "clean up memory", "curate", "Speicher bereinigen", "tidy up", or when memory_health() shows issues.
|
|
7
4
|
---
|
|
8
5
|
|
|
9
|
-
#
|
|
6
|
+
# hmem Curation
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
Curate hmem memory — mark obsolete/irrelevant/favorite, fix titles, consolidate duplicates, fix broken links.
|
|
9
|
+
|
|
10
|
+
**Two modes:**
|
|
11
|
+
- **Self-curation** (default): you curate your own memory. No extra params.
|
|
12
|
+
- **Foreign-file curation**: pass `hmem_path=/absolute/path/to/file.hmem` to `read_memory`, `update_memory`, `memory_health`, `find_related`. Sync and session cache are disabled. All updates land in that file.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
15
|
-
## Step
|
|
16
|
+
## Step 0: Health Check First
|
|
16
17
|
|
|
17
18
|
```
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
memory_health() # own store
|
|
20
|
+
memory_health(hmem_path="...") # foreign file
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Shows:
|
|
24
|
+
- **Broken links** — entries with refs to deleted IDs
|
|
25
|
+
- **Orphaned entries** — roots with no sub-nodes (likely draft stubs)
|
|
26
|
+
- **Stale favorites/pinned** — not accessed in >60 days (demote or verify)
|
|
27
|
+
- **Broken obsolete chains** — `[✓ID]` pointing to deleted entries
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
Useful before starting:
|
|
30
|
+
```
|
|
31
|
+
memory_stats()
|
|
32
|
+
read_memory(stale_days=60) # stale entries in own store
|
|
33
|
+
read_memory(stale_days=60, hmem_path="...") # same, foreign file
|
|
34
|
+
```
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
---
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
## Workflow: Prefix by Prefix
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
"- **AGENTNAME**: N entries — [OK | fixed L0003 | marked E0002 obsolete (dup) | consolidated P0004+P0007→P0004]"
|
|
40
|
+
Work one prefix at a time. Load all entries of a prefix with full depth:
|
|
31
41
|
|
|
32
|
-
6. Terminate.
|
|
33
42
|
```
|
|
43
|
+
read_memory(prefix="P", show_all=true)
|
|
44
|
+
read_memory(prefix="P", show_all=true, hmem_path="...")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`show_all=true` bypasses the bulk-read algorithm and session cache — every entry is expanded with L2+L3 children visible. Review the output directly.
|
|
48
|
+
|
|
49
|
+
**Order:** Start with the prefix with the most entries (usually P), then L, E, D, etc.
|
|
50
|
+
|
|
51
|
+
If context overflows mid-prefix, continue with the remaining entries — memory survives compression.
|
|
34
52
|
|
|
35
53
|
---
|
|
36
54
|
|
|
37
|
-
##
|
|
55
|
+
## For Each Entry: Decide and Act
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
57
|
+
| Decision | Action |
|
|
58
|
+
|----------|--------|
|
|
59
|
+
| Still valid and useful | Skip |
|
|
60
|
+
| Important reference (every session) | `update_memory(id="X", content="...", favorite=true)` |
|
|
61
|
+
| Outdated — a better entry exists | Mark obsolete (see below) |
|
|
62
|
+
| Just noise — not wrong, but irrelevant | `update_memory(id="X", content="...", irrelevant=true)` |
|
|
63
|
+
| Title vague or misleading | `update_memory(id="X", content="Better wording")` |
|
|
64
|
+
| Sub-node has valuable reference info | `update_memory(id="X.N", content="...", favorite=true)` |
|
|
46
65
|
|
|
47
|
-
|
|
48
|
-
Memory content lives in `memory_nodes` — not in flat `level_2/3` fields.
|
|
49
|
-
To fix an L2 or deeper node, use the compound ID: `fix_agent_memory(agent_name, "L0003.2", content="corrected text")`.
|
|
50
|
-
To navigate the tree: `read_memory` shows node IDs like `L0003.2`, `L0003.2.1` — use those directly.
|
|
66
|
+
Add `hmem_path="..."` to every call when curating a foreign file.
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Marking Obsolete
|
|
71
|
+
|
|
72
|
+
Obsolete requires a correction reference. Three patterns:
|
|
55
73
|
|
|
56
|
-
**
|
|
74
|
+
**A: Replacement exists already**
|
|
75
|
+
```
|
|
76
|
+
update_memory(id="E0023", content="Wrong approach — see [✓E0076]", obsolete=true)
|
|
77
|
+
```
|
|
57
78
|
|
|
79
|
+
**B: No replacement exists yet**
|
|
80
|
+
```
|
|
81
|
+
write_memory(prefix="L", content="Correct approach is XYZ\n\tDetails...") # -> L0090
|
|
82
|
+
update_memory(id="L0042", content="Superseded — see [✓L0090]", obsolete=true)
|
|
58
83
|
```
|
|
59
|
-
# Curator can bypass [✓ID] enforcement:
|
|
60
|
-
fix_agent_memory(agent_name, id, obsolete=true)
|
|
61
84
|
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
**C: Just stale, no correction needed**
|
|
86
|
+
```
|
|
87
|
+
update_memory(id="T0005", content="...", irrelevant=true)
|
|
64
88
|
```
|
|
65
89
|
|
|
66
|
-
|
|
90
|
+
Foreign file: curator may mark obsolete without `[✓ID]` for stale entries where no correction exists.
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
1. Read both entries fully (`read_memory(id=X)` for each)
|
|
70
|
-
2. Pick the **keeper** (usually the older/more informative one)
|
|
71
|
-
3. Fix the keeper's L1 if needed: `fix_agent_memory(agent_name, keeper_id, content="Broader title")`
|
|
72
|
-
4. Carry over the best content from the entry to be deleted:
|
|
73
|
-
- Existing nodes with better wording → `fix_agent_memory(agent_name, "KEEPER.2", content="improved text")`
|
|
74
|
-
- Content that only exists in the entry to be deleted → `append_agent_memory(agent_name, keeper_id, content="carried-over detail\n\tsub-detail")`
|
|
75
|
-
5. `fix_agent_memory(agent_name, fragment_id, obsolete=true)` once content is carried over
|
|
92
|
+
---
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
- Same workflow. Pick oldest as keeper.
|
|
79
|
-
- Goal: one P entry per project, growing over time.
|
|
94
|
+
## Consolidate Duplicates
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
1. Pick the **keeper** (more complete, usually older)
|
|
97
|
+
2. Copy unique info: `append_memory(id="P0029", content="Carry-over\n\tDetail")`
|
|
98
|
+
3. Mark duplicate obsolete: `update_memory(id="P0031", content="Merged into [✓P0029]", obsolete=true)`
|
|
82
99
|
|
|
83
|
-
|
|
100
|
+
**Fragmented P-entries (same project, multiple entries):** same workflow. One P per project.
|
|
101
|
+
|
|
102
|
+
---
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
## Links — Cross-References
|
|
105
|
+
|
|
106
|
+
When two entries have a clear causal/contextual relationship (e.g. a P and the L/E entries that resulted from it), add links at **both** so drill-down resolves them:
|
|
86
107
|
|
|
87
108
|
```
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fix_agent_memory(agent_name, "E0009", links=["P0001"])
|
|
109
|
+
update_memory(id="P0001", content="...", links=["L0023", "E0009"])
|
|
110
|
+
update_memory(id="L0023", content="...", links=["P0001"])
|
|
91
111
|
```
|
|
92
112
|
|
|
93
|
-
|
|
113
|
+
Don't over-link — only where navigation benefits.
|
|
94
114
|
|
|
95
|
-
|
|
115
|
+
---
|
|
96
116
|
|
|
97
|
-
|
|
117
|
+
## Title/Body Quality
|
|
98
118
|
|
|
99
|
-
|
|
119
|
+
Every node has a **title** (short, ~50 chars) and optional **body** (blank line separator). During curation:
|
|
100
120
|
|
|
101
121
|
**Root entries (L1):**
|
|
102
|
-
-
|
|
122
|
+
- Auto-title truncated/meaningless? Rewrite with explicit title + body:
|
|
103
123
|
```
|
|
104
|
-
|
|
124
|
+
update_memory(id="L0042", content="Clear title\n\nDetailed L1 body that was too long for a title")
|
|
105
125
|
```
|
|
106
|
-
- Old entries without body separation still work — the title is auto-extracted from `level_1`. Only rewrite if the auto-title is genuinely bad.
|
|
107
126
|
|
|
108
127
|
**Child nodes (L2+):**
|
|
109
|
-
-
|
|
128
|
+
- Dense content? Split into title + body:
|
|
110
129
|
```
|
|
111
|
-
|
|
130
|
+
update_memory(id="L0003.2", content="Short node title\n\nDetailed explanation")
|
|
112
131
|
```
|
|
113
|
-
- Nodes with short, clear content don't need body separation — leave them as-is.
|
|
114
132
|
|
|
115
|
-
**
|
|
116
|
-
|
|
117
|
-
- Node has >200 chars of content crammed into one line
|
|
118
|
-
- Content is valuable but hard to scan in listings (title = full text)
|
|
133
|
+
**Rewrite when:** auto-title is truncated mid-word, node has >200 chars crammed in one line, content is valuable but unscannable.
|
|
134
|
+
**Don't rewrite when:** title is already clear, or entry has low access count and marginal value.
|
|
119
135
|
|
|
120
|
-
|
|
121
|
-
- Title is already clear and navigable
|
|
122
|
-
- Entry has low access count and marginal value (not worth the effort)
|
|
136
|
+
---
|
|
123
137
|
|
|
124
|
-
|
|
138
|
+
## P-Entry Standard-Schema (R0009)
|
|
125
139
|
|
|
126
|
-
|
|
140
|
+
P-entries follow the standard L2 structure:
|
|
127
141
|
`.1 Overview`, `.2 Codebase`, `.3 Usage`, `.4 Context`, `.5 Deployment`, `.6 Bugs`, `.7 Protocol`, `.8 Open tasks`, `.9 Ideas`
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
- **Missing
|
|
131
|
-
- **Wrong order:**
|
|
132
|
-
- **Empty
|
|
133
|
-
- **L1 body:**
|
|
134
|
-
|
|
135
|
-
### O-entry curation (v5.1.2+)
|
|
136
|
-
|
|
137
|
-
O-entries may need curation for titles, tags, and summaries:
|
|
143
|
+
Check P-entries during curation:
|
|
144
|
+
- **Missing section:** `append_memory(id="P00XX", content="\tOverview\n\t\tCurrent state: ...")`
|
|
145
|
+
- **Wrong order:** Restructure — order is fixed per R0009
|
|
146
|
+
- **Empty section:** OK to omit, but content must be in the right section if present
|
|
147
|
+
- **L1 body:** One-line project summary: `Name | Status | Stack | Description`
|
|
138
148
|
|
|
139
|
-
|
|
140
|
-
- Old O-entries may have title "unassigned" or generic titles like "hmem-mcp". Fix with `fix_agent_memory(agent_name, id, content="Descriptive session title")`.
|
|
141
|
-
- Good title: "Title/Body Separation design + v5.1.0 release". Bad: "hmem-mcp".
|
|
149
|
+
---
|
|
142
150
|
|
|
143
|
-
|
|
144
|
-
- O-entries should have a `#session` tag and optionally topic tags (e.g. `#npm`, `#release`, `#refactor`).
|
|
145
|
-
- Add missing tags: `fix_agent_memory(agent_name, id, tags=["#session", "#release"])`
|
|
151
|
+
## O-Entries (Session Logs)
|
|
146
152
|
|
|
147
|
-
**
|
|
148
|
-
- O-entries with many exchanges (>10) should have a `[CP]` checkpoint summary. If missing, write one:
|
|
149
|
-
`append_agent_memory(agent_name, "O00XX", content="\t[CP] Factual 3-8 sentence summary of the session")`
|
|
150
|
-
Then tag it: the auto-tagger picks up `[CP]` prefixed nodes on the next checkpoint run.
|
|
153
|
+
O-entries accumulate via the Stop hook. They're excluded from bulk reads by default — **leave them alone**. Focus curation time on L, E, D, P.
|
|
151
154
|
|
|
152
155
|
**Special tagged nodes — do not modify:**
|
|
153
|
-
-
|
|
154
|
-
-
|
|
156
|
+
- `#checkpoint-summary` — auto-generated `[CP]` summaries
|
|
157
|
+
- `#skill-dialog` — skill activation exchanges
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
**Exception — old O-entries with bad titles/missing tags:**
|
|
160
|
+
- `update_memory(id="O0042", content="Descriptive session title")`
|
|
161
|
+
- `update_memory(id="O0042", content="...", tags=["#session", "#release"])`
|
|
157
162
|
|
|
158
|
-
|
|
163
|
+
---
|
|
159
164
|
|
|
160
|
-
|
|
161
|
-
fix_agent_memory(agent_name, id, obsolete=true)
|
|
162
|
-
```
|
|
165
|
+
## Bulk Operations
|
|
163
166
|
|
|
164
|
-
|
|
167
|
+
For large-scale changes across many entries:
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
| Tool | Purpose |
|
|
170
|
+
|------|---------|
|
|
171
|
+
| `update_many(updates=[...])` | Batch flag updates across multiple IDs |
|
|
172
|
+
| `tag_bulk(ids=[...], add_tags=[...], remove_tags=[...])` | Add/remove tags across many entries |
|
|
173
|
+
| `tag_rename(old_tag, new_tag)` | Rename a tag globally |
|
|
167
174
|
|
|
168
|
-
|
|
169
|
-
Navigator entries go stale when code moves. Check: does the file/line referenced still exist?
|
|
170
|
-
If stale and the agent hasn't updated it: mark obsolete via `fix_agent_memory(agent_name, id, obsolete=true)`.
|
|
171
|
-
Do NOT fix stale N entries yourself — the agent who wrote them must verify and update.
|
|
175
|
+
(These operate on your own store only — for foreign files, iterate manually with `update_memory(hmem_path=...)`.)
|
|
172
176
|
|
|
173
177
|
---
|
|
174
178
|
|
|
175
|
-
|
|
179
|
+
## Relocate Misplaced Nodes
|
|
176
180
|
|
|
177
|
-
|
|
181
|
+
`move_memory` cuts and re-inserts a sub-node under a new parent, rewriting all IDs + links + `[✓ID]` refs.
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
move_memory(source_id="P0029.15", target_parent_id="L0074")
|
|
185
|
+
move_memory(source_id="P0029.15", target_parent_id="P0029.20")
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Constraints:** source must be a sub-node (not root); cannot move into own subtree. Operates on own store only.
|
|
178
189
|
|
|
179
190
|
---
|
|
180
191
|
|
|
181
|
-
##
|
|
192
|
+
## Favorite Audit
|
|
182
193
|
|
|
183
|
-
|
|
194
|
+
- **Too many?** >10% favorites → demote less important: `update_memory(id="X", content="...", favorite=false)`
|
|
195
|
+
- **Missing?** Reference entries (API endpoints, key decisions, patterns) should be favorites.
|
|
196
|
+
- **Sub-node better than root?** Favorite the sub-node instead.
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
## Project experiences and summaries (5 entries)
|
|
198
|
+
---
|
|
187
199
|
|
|
188
|
-
|
|
189
|
-
2.1 Architecture: Node.js polling daemon with file-based IPC
|
|
190
|
-
2.2 Key decisions: SQLite for hmem, MCP for tool protocol
|
|
191
|
-
[+7 more → P0001]
|
|
192
|
-
Links: L0045, D0003
|
|
200
|
+
## Stale Entries
|
|
193
201
|
|
|
194
|
-
|
|
195
|
-
...
|
|
202
|
+
Entries older than 1 month with `access_count = 0`: mark obsolete.
|
|
196
203
|
|
|
197
|
-
|
|
204
|
+
```
|
|
205
|
+
update_memory(id="L0042", obsolete=true)
|
|
198
206
|
```
|
|
199
207
|
|
|
200
|
-
**
|
|
201
|
-
**Non-expanded entries** show latest child + `[+N more → ID]` hint.
|
|
208
|
+
**Exception:** unique lessons or error patterns with no equivalent — keep even if never accessed.
|
|
202
209
|
|
|
203
|
-
|
|
210
|
+
The V2 algorithm uses time-weighted scoring (`access_count / log2(age_in_days + 2)`) — old+low-access entries naturally sink; old+high-access stay visible.
|
|
204
211
|
|
|
205
212
|
---
|
|
206
213
|
|
|
@@ -208,22 +215,39 @@ Use `read_memory(show_obsolete=true)` to see all obsolete entries.
|
|
|
208
215
|
|
|
209
216
|
| Store | Max entries | Action when over |
|
|
210
217
|
|-------|-------------|-----------------|
|
|
211
|
-
| Personal | 300 | Triage: duplicates → low-access old
|
|
218
|
+
| Personal | 300 | Triage: duplicates → low-access old → generic lessons |
|
|
212
219
|
| Company | 200 | Same |
|
|
213
220
|
|
|
214
|
-
**Triage order (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
221
|
+
**Triage order:** exact duplicates → stale (access=0, >1 month) → fragmented P entries → borderline.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Quick Reference
|
|
226
|
+
|
|
227
|
+
| Tool | When |
|
|
228
|
+
|------|------|
|
|
229
|
+
| `memory_health()` | **Start here** — broken links, orphans, stale favorites |
|
|
230
|
+
| `memory_stats()` | Overview before starting |
|
|
231
|
+
| `read_memory(stale_days=60)` | Prime curation targets |
|
|
232
|
+
| `read_memory(prefix="X", show_all=true)` | Load entire prefix |
|
|
233
|
+
| `update_memory(id, content, favorite=true)` | Always-show reference |
|
|
234
|
+
| `update_memory(id, content, irrelevant=true)` | Hide from bulk reads |
|
|
235
|
+
| `update_memory(id, content, obsolete=true)` | Mark wrong (needs [✓ID]) |
|
|
236
|
+
| `append_memory(id, content)` | Merge info into keeper |
|
|
237
|
+
| `move_memory(source_id, target_parent_id)` | Relocate misplaced sub-node |
|
|
238
|
+
| `update_many(updates=[...])` | Batch flag updates |
|
|
239
|
+
| `tag_bulk / tag_rename` | Tag maintenance |
|
|
240
|
+
| `read_memory(show_obsolete=true)` | Review already-obsolete |
|
|
241
|
+
| `find_related(id)` | Discover connections / spot duplicates |
|
|
242
|
+
|
|
243
|
+
All four read/update/health/find tools accept `hmem_path="..."` for foreign-file curation.
|
|
219
244
|
|
|
220
245
|
---
|
|
221
246
|
|
|
222
247
|
## Rules
|
|
223
248
|
|
|
224
249
|
- Never invent or fabricate memories.
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
- One
|
|
229
|
-
- Always write to LAST_CURATION.md, even for clean runs ("OK — nothing to fix").
|
|
250
|
+
- Prefer fix/consolidate/mark-obsolete over deletion. Obsolete entries are hidden from bulk reads anyway.
|
|
251
|
+
- **When in doubt, skip.** False obsolete/irrelevant is harder to undo than leaving an entry alone.
|
|
252
|
+
- **Preserve learning value.** E (errors) and L (lessons) about *why* something failed stay valuable even after the bug is fixed — only mark obsolete if the analysis is wrong.
|
|
253
|
+
- **One prefix per batch.** Don't try all 200+ entries at once.
|
|
@@ -136,7 +136,7 @@ The migration tags old O-entries as `#legacy`. These still use the old flat form
|
|
|
136
136
|
|
|
137
137
|
1. **Keep for now** — they don't hurt anything, just take space
|
|
138
138
|
2. **Mark irrelevant** — `update_memory(id="O0042", irrelevant=true)` for entries you don't need
|
|
139
|
-
3. **Delete later** — run `/hmem-
|
|
139
|
+
3. **Delete later** — run `/hmem-curate` to review and clean up `#legacy` entries
|
|
140
140
|
4. **Auto-purge** — irrelevant entries older than 30 days are automatically deleted
|
|
141
141
|
|
|
142
142
|
Recommendation: keep them for a week to make sure everything works, then curate.
|
|
@@ -382,7 +382,7 @@ After any `read_memory()` call, scan for:
|
|
|
382
382
|
- Don't curate during time-critical tasks (the user is waiting for a bug fix, not curation)
|
|
383
383
|
- Don't mark entries irrelevant if you're unsure — ask the user first
|
|
384
384
|
|
|
385
|
-
For a thorough deep-clean, use the `/hmem-
|
|
385
|
+
For a thorough deep-clean, use the `/hmem-curate` skill.
|
|
386
386
|
|
|
387
387
|
---
|
|
388
388
|
|
|
@@ -47,8 +47,7 @@ Every code change can affect skill documentation. Check each skill against the c
|
|
|
47
47
|
| **hmem-read** | read_memory output format, load_project display, O-entry format | Changes to `formatBulkRead`, `formatRecentOEntries`, `load_project` rendering |
|
|
48
48
|
| **hmem-config** | New config parameters, changed defaults, removed options | Changes to `HmemConfig` interface, `DEFAULT_CONFIG`, `loadHmemConfig` |
|
|
49
49
|
| **hmem-update** | New migration steps, new post-update checks | Any schema change, new features that need post-update setup |
|
|
50
|
-
| **hmem-curate** | Curation rules, new node types, new tags | New tagged node types (#checkpoint-summary, #skill-dialog), schema changes |
|
|
51
|
-
| **hmem-self-curate** | Same as curate but for agent self-curation | Same triggers as hmem-curate |
|
|
50
|
+
| **hmem-curate** | Curation rules (self + foreign-file), hmem_path param, new node types, new tags | New tagged node types (#checkpoint-summary, #skill-dialog), schema changes, hmem_path-capable tool changes |
|
|
52
51
|
| **hmem-new-project** | P-entry schema (R0009), write_memory format | Changes to P-entry structure or write format |
|
|
53
52
|
| **hmem-setup** | Hook scripts, init flow, MCP config format | Changes to hooks, CLI commands, environment variables |
|
|
54
53
|
| **hmem-wipe** | Checkpoint references, context threshold | Changes to checkpointMode, contextTokenThreshold |
|
|
@@ -131,7 +130,7 @@ git push
|
|
|
131
130
|
| Code area | Skills to check |
|
|
132
131
|
|-----------|----------------|
|
|
133
132
|
| hmem-store.ts (write/read) | hmem-write, hmem-read, hmem-curate |
|
|
134
|
-
| hmem-store.ts (O-entries) | hmem-read, hmem-
|
|
133
|
+
| hmem-store.ts (O-entries) | hmem-read, hmem-curate |
|
|
135
134
|
| hmem-config.ts | hmem-config, hmem-update, hmem-setup |
|
|
136
135
|
| mcp-server.ts (tools) | hmem-write, hmem-read (tool params) |
|
|
137
136
|
| mcp-server.ts (load_project) | hmem-read, hmem-new-project |
|
|
@@ -76,15 +76,17 @@ Do NOT attempt to run /clear yourself — it is a built-in CLI command only the
|
|
|
76
76
|
|
|
77
77
|
## What happens after /clear
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Context is restored **automatically** by the `SessionStart[clear]` hook — no
|
|
80
|
+
agent action needed after /clear. Do **NOT** call `load_project` or `read_memory`
|
|
81
|
+
during or after this skill; the next session's first UserPromptSubmit hook will
|
|
82
|
+
trigger the normal hmem-read flow with a verified active project ID.
|
|
83
|
+
|
|
84
|
+
The hook:
|
|
80
85
|
1. Resets the MCP session cache
|
|
81
86
|
2. Injects recent conversation exchanges from the project's O-entry transcript
|
|
82
87
|
3. Injects the active project briefing (overview expanded)
|
|
83
88
|
4. Injects recent O-entry titles + rules
|
|
84
89
|
|
|
85
|
-
The agent then calls `load_project` and has full context to continue working.
|
|
86
|
-
No manual restoration needed.
|
|
87
|
-
|
|
88
90
|
## Why this flow works
|
|
89
91
|
|
|
90
92
|
- **O-entries are covered.** The Stop hook logs every exchange to the active
|
|
@@ -131,10 +131,20 @@ append_memory(id="P0048.6", content="Auto-sync fails with multiple .hmem in CWD
|
|
|
131
131
|
```
|
|
132
132
|
Custom prefixes are merged with the defaults — they don't replace them. Without registering, the system will reject the prefix.
|
|
133
133
|
|
|
134
|
-
###
|
|
134
|
+
### Schema-Enforced Entries (P and any prefix with a defined schema)
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
The MCP server enforces schemas for any prefix that has a `schemas` entry in `hmem.config.json`.
|
|
137
|
+
For those prefixes, two rules apply:
|
|
138
|
+
|
|
139
|
+
1. **`write_memory`**: L2 node names must match the defined section names (error otherwise).
|
|
140
|
+
2. **`append_memory` to root entry (e.g. `P0029`, no dot)**: **blocked** — you cannot add new L2 sections.
|
|
141
|
+
You must append to a specific section: `append_memory(id="P0029.N", content="...")`.
|
|
142
|
+
|
|
143
|
+
By default, `P` has a schema. If `L`, `D`, or other prefixes are configured with schemas, the same rules apply.
|
|
144
|
+
|
|
145
|
+
### P-Entry Standard Schema
|
|
146
|
+
|
|
147
|
+
Every project entry MUST follow this structure.
|
|
138
148
|
|
|
139
149
|
**L1 Title:** `Name | Status | Tech Stack | GH: owner/repo | Short description`
|
|
140
150
|
The GH field is optional — include it when a GitHub repo exists, omit otherwise.
|
|
@@ -327,7 +337,8 @@ read_memory(id="P0029") # shows root + all L2 titles
|
|
|
327
337
|
```
|
|
328
338
|
Do any L2 titles match the sub-topic?
|
|
329
339
|
|
|
330
|
-
- **No match** → `append_memory(id="P0029", content="...")` adds a new L2
|
|
340
|
+
- **No match (and no schema)** → `append_memory(id="P0029", content="...")` adds a new L2
|
|
341
|
+
- **No match (schema-constrained entry like P)** → find the closest existing section and append there (L2 additions are blocked)
|
|
331
342
|
- **Match found (e.g. .15)** → continue to Step 3
|
|
332
343
|
|
|
333
344
|
**Step 3 — Drill into that L2**
|
|
@@ -411,12 +422,24 @@ Appends new child nodes under an existing root or node. Existing children are pr
|
|
|
411
422
|
Content indentation is **relative to the parent** — 0 tabs = direct child of `id`.
|
|
412
423
|
Body works the same as in `write_memory` — blank line separates title from body.
|
|
413
424
|
|
|
425
|
+
> **Schema enforcement:** For entries with a defined schema (e.g., all P-entries), appending
|
|
426
|
+
> to the root (e.g., `id="P0029"`) is **blocked** — that would create a new L2 section outside
|
|
427
|
+
> the schema. You must target a specific section: `append_memory(id="P0029.3", content="...")`.
|
|
428
|
+
> For entries without a schema (L, D, E, etc. by default), root appends are allowed.
|
|
429
|
+
|
|
414
430
|
```
|
|
415
431
|
append_memory(
|
|
416
432
|
id="L0003",
|
|
417
433
|
content="New finding discovered later\n\nDetailed explanation of what was found and why it matters.\nThis can span multiple lines.\n\tSub-detail about it"
|
|
418
434
|
)
|
|
419
435
|
# → adds L0003.N (L2 with title + body) and L0003.N.1 (L3)
|
|
436
|
+
# ↑ only works if L has no schema defined; use L0003.N for schema-constrained entries
|
|
437
|
+
|
|
438
|
+
append_memory(
|
|
439
|
+
id="P0029.3",
|
|
440
|
+
content="New detail in the Usage section"
|
|
441
|
+
)
|
|
442
|
+
# → adds P0029.3.M (L3 under section .3) — correct way for schema-constrained entries
|
|
420
443
|
|
|
421
444
|
append_memory(
|
|
422
445
|
id="L0003.2",
|
|
@@ -434,7 +457,7 @@ Use when: you have new context to add without replacing what's there.
|
|
|
434
457
|
| L1 wording is wrong/outdated | `update_memory` |
|
|
435
458
|
| A sub-node has wrong detail | `update_memory` |
|
|
436
459
|
| You have new info to add | `append_memory` |
|
|
437
|
-
| Entry is completely wrong |
|
|
460
|
+
| Entry is completely wrong | mark obsolete with `[✓newId]`, then `write_memory` for the correction |
|
|
438
461
|
|
|
439
462
|
---
|
|
440
463
|
|