skills/memstate/SKILL.md (15414 bytes)
1 --- 2 name: memstate 3 description: > 4 Persistent, versioned memory via the memstated daemon. Data lives in a 5 single SQLite file on your machine. No cloud, no API key. Use for storing 6 facts, recalling memory, managing projects, and full-text or semantic 7 search of agent summaries. Supports Markdown and direct keypath = value 8 assignment. 9 license: MIT 10 tags: 11 - memory 12 - agent-memory 13 - memstate 14 - versioned-memory 15 - keypath 16 --- 17 18 # Memstate Memory Management 19 20 This skill talks to a local memstated daemon over HTTP loopback. The 21 daemon stores data in a SQLite file at `$MEMSTATE_DB` (default 22 `~/.memstate/memstate.db`). No API key. If `MEMSTATE_ADDR` is set, the 23 scripts attach to that daemon. Otherwise each script spawns a private 24 daemon on a random port for the duration of the script and stops it on 25 exit. 26 27 Scripts live in this skill's `scripts/` directory (installed at 28 `~/.claude/skills/memstate/scripts/`). All examples below assume you 29 run them from there or by absolute path. 30 31 ## Core Concepts 32 33 | Concept | Description | 34 |---|---| 35 | **Project** | Top-level container for memories, keyed by `project_id`. Auto-created on first write. | 36 | **Keypath** | Dot-separated path (`decisions.auth_provider`) unique within a project. Stored exactly as you write it. Nothing is auto-prefixed. | 37 | **Memory** | One fact or markdown section stored at a keypath, with full version history. Memory ids are integers. | 38 | **Versioning** | A write to an existing keypath supersedes the old value. The response returns the old version as `superseded`. Writes are synchronous. Data is queryable when the script returns. | 39 | **Tombstone** | A delete adds a tombstone version. History is never destroyed. A new write to the keypath revives it. | 40 41 ## Naming conventions: follow EXACTLY 42 43 Every deviation fragments the store into disconnected near-duplicates. 44 45 - **project_id**: Omit `--project`. Every script derives the default 46 from the git repository name (or the directory basename), slugged to 47 snake_case, so all sessions in one repository share one project 48 automatically. Pass `--project` only to reach a DIFFERENT project, 49 and then only an id that `memstate_get.py --list-projects` lists. 50 Never invent a variant. `my-app`, `myapp`, and `my_app_dev` are three 51 different projects. 52 - **keypath segments**: Lowercase snake_case only (`[a-z0-9_]`), joined 53 by dots. Dates are `YYYY_MM_DD` inside a segment: 54 `task.summary.2026_07_04`. Never `2026-07-04`, camelCase, or spaces. 55 - **keypath shape**: `<area>.<topic>` or `<area>.<topic>.<detail>`. 56 Preferred area segments: `decisions`, `todo`, `notes`, `gotchas`, 57 `questions`, `files`, `config`, `arch`, `task.summary.<date>`. 58 - **One keypath = one fact.** To update a fact, write the SAME keypath 59 with the new value. Versioning keeps the old one. Do not create a 60 sibling keypath for a new value of the same fact. 61 - **Git branches**: Keypaths describe the main/default branch unless 62 stated otherwise. Facts that are only true on an unmerged branch go 63 under `branches.<branch_slug>.<area>...`, with the branch name 64 slugged to snake_case (`feature/foo-bar` → 65 `branches.feature_foo_bar.todo`). When the branch merges, write the 66 durable outcomes to normal top-level keypaths, then recursively 67 delete the `branches.<branch_slug>` subtree. If the branch is 68 abandoned, delete the subtree. Branch-independent knowledge 69 (decisions taken, gotchas, architecture) always lives at the top 70 level. 71 - **category**: The kind of memory. ONE lowercase word from `decision`, 72 `config`, `status`, `note`, `gotcha`, `reference`, `learning`. 73 Filterable in search. 74 - **topics**: Subject tags, lowercase snake_case (`auth,embeddings`). 75 Search matches any listed topic. 76 - **source**: A short provenance string, for example `claude-code 77 session 2026_07_04` or `user decision`. Shown in history. 78 79 ## User scope: facts that are not about this project 80 81 Every memory lives in a project by default. Some facts are about the 82 user or the machine and hold in every repository: an installed tool, a 83 path, the Ollama URL, a working preference, the user's role. These go 84 in the reserved project `_user`. Every script reaches it with 85 `--scope user` (never with `--project`). 86 87 A fact belongs in the user scope only when ALL three hold: 88 89 1. It stays true if this repository is deleted. It is not about any code. 90 2. It is true in every repository, for this user or for this machine. 91 3. It describes the user or the host, not work. Never a decision, todo, 92 task summary, note, or gotcha about code. Those stay in the project. 93 94 The daemon enforces this line with a keypath allowlist. A write outside 95 it returns HTTP 400 and nothing is stored. Allowed shapes: 96 97 | Shape | Meaning | Example | 98 |---|---|---| 99 | `preferences.<topic>` | stated global working preferences | `preferences.commit_style` | 100 | `profile.<topic>` | who the user is; never credentials | `profile.role` | 101 | `host.<host_slug>.env.<topic>` | OS, shell, paths, runtimes, ports | `host.mbp.env.go_bin` | 102 | `host.<host_slug>.tools.<topic>` | how an installed tool is configured | `host.mbp.tools.ollama_url` | 103 104 `<host_slug>` is the first label of the hostname, slugged like a project 105 id (`Matts-MBP.local` → `matts_mbp`). `memstate_get.py` prints it as 106 `user.host`. Reads prune other machines' `host.*` subtrees; searches with 107 `--scope user` drop them. 108 109 ```bash 110 python3 scripts/memstate_set.py --scope user --keypath preferences.commit_style \ 111 --value "short subjects, no trailers" --category config 112 python3 scripts/memstate_set.py --scope user --keypath host.matts_mbp.tools.ollama_url \ 113 --value "http://127.0.0.1:11434, model qwen3-embedding:4b" --category config 114 python3 scripts/memstate_get.py --scope user --keypath preferences --include-content 115 ``` 116 117 Heading extraction (`memstate_remember.py --scope user` without 118 `--keypath`) fits only `## Preferences` and `## Profile`. Host facts need 119 an explicit `--keypath`. One bad section rejects the whole call. 120 121 Never store secrets, tokens, or credentials in any scope. The 122 denied-prompt rule applies to the user scope too. 123 124 ## Session project (MCP sessions only) 125 126 The MCP proxy defaults every call to the repository of its working 127 directory, and that is almost always right. When the prompt is clearly 128 about another subject (for example "set up my nginx config" from the home 129 directory), the agent pins its session once with `project_name` on the 130 first tool call. An existing id pins freely. A new id also needs 131 `new_project=true` and is refused when it looks like an existing id 132 (`regress_tests` vs `regress_test`). One pin per session. 133 134 One rule for creating projects, enforced by the proxy and by these 135 scripts alike: a write never creates a project unless it targets the git 136 repository you are in, or the call carries `new_project=true` (scripts: 137 `--new-project`). This holds for the cwd project outside a repository, for 138 an explicit project id, and for a soft-deleted project, which the flag 139 revives. A name that resembles an existing project is refused outright; 140 use the existing project, or create the new one with the `memstate` CLI, 141 which is the human escape. The home directory has no default project for 142 writes, and its name is never accepted as a project: pin a project or use 143 the user scope. Ids that start with `_` are reserved; the user scope is 144 `--scope user`, never `--project`, and `--list-projects` does not show it. 145 `--new-project` is an error where nothing can be created (`--scope user`, 146 or the git repository you are in). Reads are never gated. These scripts 147 are one-shot and have no session: pass `--project` instead of 148 `project_name`. A pinned MCP session publishes its pin for the recall hook 149 as `<db dir>/recall/pins/<pid>`, removed when the proxy exits. 150 151 ## Workflows 152 153 ### Before starting a task (recall) 154 155 ```bash 156 # 1. Browse this repo's keypath tree (names only, no content) 157 python3 scripts/memstate_get.py 158 159 # 2. Read a subtree with content 160 python3 scripts/memstate_get.py --keypath decisions --include-content 161 162 # 3. Search when you do not know the keypath 163 python3 scripts/memstate_search.py --query "how is authentication configured" 164 python3 scripts/memstate_search.py --query "auth setup" --mode semantic 165 166 # Other projects: --list-projects shows ids. --project targets one. 167 python3 scripts/memstate_get.py --list-projects 168 ``` 169 170 ### After completing a task (remember) 171 172 ```bash 173 # One short fact at one keypath 174 python3 scripts/memstate_set.py \ 175 --keypath config.port --value "8080" --category config 176 177 # Markdown summary, split by ## headings (one memory per section). 178 # Each section lands at the top-level <heading_slug> keypath. ### 179 # headings nest one more dot segment. Prose before the first ## lands 180 # at `preamble`. Pass --root notes to nest sections under a prefix, or 181 # --keypath to store ALL content as one memory. 182 python3 scripts/memstate_remember.py \ 183 --content "## Decisions\nSwitched JWT -> sessions.\n\n## Gotchas\nCookie must be SameSite=Lax." \ 184 --source "claude-code session 2026_07_04" --category note 185 ``` 186 187 Heading names `TODOs`, `Decisions`, `Open Questions`, `Files`, 188 `Notes`, `Gotchas` collapse to the canonical segments `todo`, 189 `decisions`, `questions`, `files`, `notes`, `gotchas`. 190 191 ### History and cleanup 192 193 ```bash 194 # How a fact changed over time (newest first, includes tombstones) 195 python3 scripts/memstate_history.py --project my_app --keypath config.port 196 197 # Tombstone one keypath. History is kept. A new write revives it. 198 python3 scripts/memstate_delete.py --keypath config.old_setting 199 200 # Tombstone a whole subtree, e.g. a merged branch's state 201 python3 scripts/memstate_delete.py --keypath branches.feature_foo_bar --recursive 202 203 # Soft-delete a project (any later write to the same id revives it) 204 python3 scripts/memstate_delete_project.py --project my_app 205 ``` 206 207 ## Script reference 208 209 ### `memstate_set.py`: one fact at one keypath 210 211 ```bash 212 python3 scripts/memstate_set.py \ 213 --keypath KEYPATH --value VALUE \ 214 [--project ID] [--new-project] [--source TEXT] [--category WORD] [--topics TAG1,TAG2] 215 ``` 216 217 **Response:** `action` (`created` | `superseded` | `unchanged`), 218 `stored` (the new memory), `superseded` (the prior version, if any). 219 `unchanged` means identical content AND metadata. No new version was 220 written. 221 222 ### `memstate_remember.py`: markdown or text 223 224 ```bash 225 python3 scripts/memstate_remember.py \ 226 --content "MARKDOWN" \ 227 [--project ID] [--new-project] 228 [--keypath KEYPATH] # store everything as ONE memory here 229 [--root PREFIX] # heading-split mode: nest sections under this prefix (default: none) 230 [--source TEXT] [--category WORD] [--topics TAG1,TAG2] 231 ``` 232 233 The split is a deterministic parse of `##`+ headings. No LLM, no job 234 queue. The operation is synchronous. **Response:** `method` 235 (`explicit` | `headings`), `items[]` each with `keypath`, `action`, 236 `stored`, `superseded?`. 237 238 ### `memstate_get.py`: browse and retrieve 239 240 ```bash 241 python3 scripts/memstate_get.py # this repo's tree (names only) 242 python3 scripts/memstate_get.py --keypath KP --include-content 243 python3 scripts/memstate_get.py --list-projects # all project ids 244 python3 scripts/memstate_get.py --project ID --keypath KP # another project 245 python3 scripts/memstate_get.py --memory-id N # one memory by integer id 246 ``` 247 248 **Response:** the projects list returns `projects[]`. The tree returns 249 `domains[]` and `total_memories`. A subtree returns `memories[]` and 250 `total_count`. The tree gives names only. Pass a keypath (with 251 `--include-content`) to read values. 252 253 ### `memstate_search.py`: find current memories 254 255 ```bash 256 python3 scripts/memstate_search.py --query "PLAIN WORDS" \ 257 [--project ID] # default: this repo's project 258 [--all-projects] # search the whole store 259 [--mode hybrid|fts|semantic] # hybrid (default) = any word + meaning, fused; fts = every word; semantic = meaning only (needs Ollama) 260 [--threshold 0.0-1.0] # semantic and hybrid, default 0.5 261 [--category WORD] [--topics TAG1,TAG2] # topics = match any 262 [--keypath-prefix KP] # only this keypath or below, e.g. branches.feature_x 263 [--limit N] # default 20 264 ``` 265 266 Only the current version of each keypath is searchable. Tombstoned 267 keypaths and soft-deleted projects never match. Query text is plain 268 words. Punctuation is safe, and there is no boolean syntax. 269 **Response:** `results[]`, `total_found`, `query`, `mode` (+ `score` 270 per result and `threshold`/`model` in semantic and hybrid modes; hybrid 271 adds `sources` per result and `degraded` when the embedder was 272 unavailable and only FTS hits are present). 273 274 ### `memstate_history.py`: version chain of one keypath 275 276 ```bash 277 python3 scripts/memstate_history.py --keypath KP [--project ID] 278 python3 scripts/memstate_history.py --memory-id N 279 ``` 280 281 **Response:** `versions[]` (newest first, includes tombstones), 282 `total_versions`. 283 284 ### `memstate_delete.py`: tombstone keypath(s) 285 286 ```bash 287 python3 scripts/memstate_delete.py --keypath KP [--project ID] [--recursive] 288 ``` 289 290 **Response:** `deleted_count`, `deleted_keypaths[]`. 291 292 ### `memstate_delete_project.py`: soft-delete a project 293 294 ```bash 295 python3 scripts/memstate_delete_project.py --project ID 296 ``` 297 298 **Response:** `project_id`, `deleted_count`. Reads fail afterwards. 299 Any write to the same project_id revives it with all memories intact. 300 301 ## Best practices 302 303 1. **Let the default id work.** Omit `--project`. The repo-derived 304 default keeps every session in one repo on one project. Only pass 305 an explicit id that `--list-projects` shows. 306 2. **Update, do not duplicate.** Same keypath, new value. The version 307 chain is the changelog. 308 3. **Search before browsing.** `memstate_search.py` beats a walk of 309 the tree when you know roughly what you want. The default hybrid 310 mode already matches by meaning; use `--mode fts` when you need every 311 word to match. 312 4. **Only current versions surface.** Search and get return the latest 313 non-deleted version per keypath. Use `memstate_history.py` to see 314 the past. There is no `is_latest` flag to check. 315 5. **Categorize decisions.** `--category decision` on architecture 316 choices makes them retrievable with one filtered search. 317 6. **Keep branch state quarantined.** Put in-flight branch facts under 318 `branches.<slug>.*`. Promote them to the top level on merge, then 319 recursively delete the subtree. Scope a search to one branch with 320 `--keypath-prefix branches.<slug>`. 321 7. **Never save a denied prompt.** A denied prompt is a tool call that the 322 user or a permission check denied. Do not save it under any keypath, in 323 any category, or in any summary. Do not save it as a warning for a later 324 session. No reason overrides this rule. 325 8. **Keep user facts out of projects, and work out of the user scope.** 326 A tool path or a global preference goes to `--scope user`; a 327 decision, todo, or summary never does. The daemon rejects the latter. 328 329 ## Connecting to the daemon 330 331 ```bash 332 export MEMSTATE_ADDR="127.0.0.1:8765" # attach to a shared daemon 333 export MEMSTATE_LOCAL_URL="http://127.0.0.1:9000/api/v1" # or full URL override 334 ``` 335 336 With neither set, each script spawns its own daemon on a random port 337 against `$MEMSTATE_DB` and stops it on exit. A manually started 338 `memstated --addr HOST:PORT` probes the port first. If another 339 memstated already owns the port, it exits quietly. If an unrelated 340 process owns the port, it exits loudly (code 2).