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.
@@ -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.1",
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
- # /hmem-curate — hmem Curation Workflow
6
+ # hmem Curation
10
7
 
11
- You are the memory curator. Process **one agent per run**, then terminate.
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-by-Step
16
+ ## Step 0: Health Check First
16
17
 
17
18
  ```
18
- 1. get_audit_queue()
19
- Empty → write a short summary to LAST_CURATION.md and terminate
20
- → Not empty → take the FIRST agent from the list
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
- 2. read_agent_memory(agent_name, depth=5)
23
- → Study all entries carefully
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
- 3. Fix every issue found (see criteria below)
36
+ ---
26
37
 
27
- 4. mark_audited(agent_name)
38
+ ## Workflow: Prefix by Prefix
28
39
 
29
- 5. Append one line to LAST_CURATION.md:
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
- ## Quality Criteria
55
+ ## For Each Entry: Decide and Act
38
56
 
39
- ### L1 Quality
40
- | Check | Rule |
41
- |-------|------|
42
- | Too long | Single concise sentence, ~15–20 tokens. Fix with `fix_agent_memory(agent_name, id, content="shorter")` |
43
- | Too vague | "Fixed a bug" mark obsolete. "SQLite failed due to wrong path in .mcp.json" → keep |
44
- | Factually wrong | Fix content or mark obsolete. |
45
- | Duplicate of another entry | Merge the best content from both into the keeper (see merge workflow below), then mark the weaker entry obsolete. |
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
- ### Compound Node IDs
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
- ### Obsolete entries
53
- Entries marked `[!]` (or `[OBSOLETE]` in curator view) are already hidden from bulk reads — they do not need to be deleted.
54
- Leave them in place. The curator's job is to *mark* entries obsolete, not remove them.
68
+ ---
69
+
70
+ ## Marking Obsolete
71
+
72
+ Obsolete requires a correction reference. Three patterns:
55
73
 
56
- **Curator bypass:** As curator, you can mark entries obsolete **without** the `[✓ID]` correction reference that agents are required to include. Use this for stale entries where no correction exists (e.g., entries about deleted features, abandoned approaches).
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
- # But prefer including a correction reference when one exists:
63
- fix_agent_memory(agent_name, id, content="Outdated — see [✓E0076]", obsolete=true)
85
+ **C: Just stale, no correction needed**
86
+ ```
87
+ update_memory(id="T0005", content="...", irrelevant=true)
64
88
  ```
65
89
 
66
- ### Merging entries (duplicates and fragmented P entries)
90
+ Foreign file: curator may mark obsolete without `[✓ID]` for stale entries where no correction exists.
67
91
 
68
- **Merge workflow:**
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
- **For fragmented P entries** (same project, multiple entries):
78
- - Same workflow. Pick oldest as keeper.
79
- - Goal: one P entry per project, growing over time.
94
+ ## Consolidate Duplicates
80
95
 
81
- *Note: only carry over content with lasting value. Low-value session notes can be dropped.*
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
- ### Links cross-references
100
+ **Fragmented P-entries (same project, multiple entries):** same workflow. One P per project.
101
+
102
+ ---
84
103
 
85
- When two entries have a clear causal or contextual relationship (e.g. a P entry and the L/E entries that resulted from it, or an E entry and the D entry that documents the fix decision), add links at **both** entries so they resolve each other on drill-down:
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
- fix_agent_memory(agent_name, "P0001", links=["L0023", "E0009"])
89
- fix_agent_memory(agent_name, "L0023", links=["P0001"])
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
- `read_memory(id=X)` auto-resolves linked entries the agent sees both sides when drilling into either one.
113
+ Don't over-linkonly where navigation benefits.
94
114
 
95
- Don't over-link: only add links where the connection adds real navigational value, not just topical similarity.
115
+ ---
96
116
 
97
- ### Title/Body Quality (v5.1+)
117
+ ## Title/Body Quality
98
118
 
99
- Entries support explicit title/body separation via a blank line (like git commits). During curation:
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
- - Check if the auto-extracted title is meaningful. If it's truncated or vague, rewrite with explicit title + body:
122
+ - Auto-title truncated/meaningless? Rewrite with explicit title + body:
103
123
  ```
104
- fix_agent_memory(agent_name, id, content="Clear navigation title\n\nOriginal detailed L1 text that was too long for a title")
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
- - Same principle: if a node has dense content, split into title + body:
128
+ - Dense content? Split into title + body:
110
129
  ```
111
- fix_agent_memory(agent_name, "L0003.2", content="Short node title\n\nDetailed explanation\nspanning multiple lines")
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
- **When to rewrite old entries:**
116
- - Auto-title is truncated mid-word or meaningless
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
- **When NOT to rewrite:**
121
- - Title is already clear and navigable
122
- - Entry has low access count and marginal value (not worth the effort)
136
+ ---
123
137
 
124
- ### P-Entry Standard-Schema (R0009)
138
+ ## P-Entry Standard-Schema (R0009)
125
139
 
126
- All P-entries must follow the standard L2 structure (see R0009):
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
- During curation, check P-entries against this schema:
130
- - **Missing sections:** Add them using `append_agent_memory(agent_name, "P00XX", content="\tOverview\n\t\tCurrent state: ...")`
131
- - **Wrong order:** Flag and restructure — order is fixed per R0009
132
- - **Empty sections:** OK to omit, but if content exists it must be in the right section
133
- - **L1 body:** Should be a one-line project summary in format `Name | Status | Stack | Description`
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
- **Titles:**
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
- **Tags:**
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
- **Checkpoint summaries:**
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
- - **`#checkpoint-summary`** nodes: Auto-generated `[CP]` summaries. Leave as-is.
154
- - **`#skill-dialog`** exchange nodes: Skill activations filtered from `load_project`. Leave as-is.
156
+ - `#checkpoint-summary` auto-generated `[CP]` summaries
157
+ - `#skill-dialog` skill activation exchanges
155
158
 
156
- ### Stale entries auto-mark obsolete
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
- Entries older than 1 month with `access_count = 0` (no `(Nx accessed)` suffix in curator read) should be marked obsolete automatically.
163
+ ---
159
164
 
160
- ```
161
- fix_agent_memory(agent_name, id, obsolete=true)
162
- ```
165
+ ## Bulk Operations
163
166
 
164
- Exception: unique lessons or error patterns with no equivalent elsewhere — keep even if never accessed.
167
+ For large-scale changes across many entries:
165
168
 
166
- **Note:** The V2 algorithm uses **time-weighted scoring** (`access_count / log2(age_in_days + 2)`) for "most accessed" ranking. This means genuinely stale entries (old + low access) naturally sink — but an old entry that's still frequently accessed stays visible.
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
- ### N entriesflag stale code pointers
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
- ### Quick audit with titles_only
179
+ ## Relocate Misplaced Nodes
176
180
 
177
- Use `read_memory(titles_only=true)` for a compact overview of an agent's memory before drilling in. Shows V2-selected entries as one line each with `(N)` child counts — useful for spotting duplicates, poor titles, or category imbalances at a glance.
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
- ## V2 Bulk-Read Output
192
+ ## Favorite Audit
182
193
 
183
- The default `read_memory()` now returns **grouped output** by prefix category:
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
- P0001 02-14 Das Althing — Node.js/TS Multi-Agent-Orchestrator
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
- ## Lessons learned and best practices (78 entries)
195
- ...
202
+ Entries older than 1 month with `access_count = 0`: mark obsolete.
196
203
 
197
- --- 5 obsolete entries hidden (E0023, D0007, ...) — top 3 shown above ---
204
+ ```
205
+ update_memory(id="L0042", obsolete=true)
198
206
  ```
199
207
 
200
- **Expanded entries** (newest, most-accessed, favorites) show all L2 children + links.
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
- Use `read_memory(show_obsolete=true)` to see all obsolete entries.
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 entries → generic lessons |
218
+ | Personal | 300 | Triage: duplicates → low-access old → generic lessons |
212
219
  | Company | 200 | Same |
213
220
 
214
- **Triage order (over limit):**
215
- 1. Mark exact duplicates obsolete (after merging content into keeper)
216
- 2. Mark stale entries obsolete (access_count = 0, >1 month old)
217
- 3. Consolidate fragmented P entries
218
- 4. Mark borderline entries obsolete
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
- - Never add new content only fix, consolidate, or mark obsolete. Never delete.
226
- - Obsolete entries are hidden from bulk reads they don't need to be removed.
227
- - Skip yourself (the curator agent) if you appear in the queue.
228
- - One agent per run be called again for the next agent.
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-self-curate` to review and clean up `#legacy` entries
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-self-curate` skill.
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-self-curate, hmem-curate |
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
- The `SessionStart[clear]` hook automatically:
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
- ### P-Entry Standard Schema (enforced by MCP server)
134
+ ### Schema-Enforced Entries (P and any prefix with a defined schema)
135
135
 
136
- Every project entry MUST follow this structure. The MCP server validates L2 nodes
137
- against the standard categories when `prefix="P"`.
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 | curator: `delete_agent_memory` + `write_memory` |
460
+ | Entry is completely wrong | mark obsolete with `[✓newId]`, then `write_memory` for the correction |
438
461
 
439
462
  ---
440
463