Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ All notable changes to MeMesh are documented here.
statistics are frozen. Evidence tooling only; nothing ships in the package.
- **Capture liveness: MeMesh can now tell a quiet hook from a broken one
(#327).** Every capture hook leaves an outcome record on every exit path —
`wrote`, `skipped` with a reason, or `error` with a label (never the
exception text) — appended to `hook-outcomes.jsonl` beside the database.
`wrote`, `notified` (the hook printed something a person or model reads
and stored nothing), `skipped` with a reason, or `error` with a label
(never the exception text) — appended to `hook-outcomes.jsonl` beside the database.
`memesh doctor` gains a **Capture liveness** row, and `memesh doctor --json`
/ `GET /v1/doctor` carry per-hook figures and per-type week-over-week write
counts under a new `capture` field. Only hooks whose trigger means a write
Expand Down Expand Up @@ -113,6 +114,24 @@ All notable changes to MeMesh are documented here.
Without `replace` the append semantics are exactly as before. The
contentless FTS index is deleted with the exact text that was indexed, so
the old words stop matching.
The memory keeps the `type` it already has: `type` is required only on a
`replace` whose name does not exist yet, and passing a different one is how
a memory is reclassified. A `replace` on a memory archived with `forget` is
refused — remember it again without `replace` first.
- **The remember receipt reports the title the row holds (#324).** The response
used to carry the title the text *would* have produced, so a memory that kept
its own headline was described with one it never had. MCP `remember` and
`POST /v1/remember` return the stored value; the CLI no longer re-reads the
row to work around it.
- **The over-cap refusal counts observations, not paragraphs (#324).** A single
paragraph of 101 list items yields 101 observations, and the message said
"101 paragraphs" — the wrong unit for the thing being capped.
- **The MCP and exported schemas state the three forms (#324).** `remember`
declares `anyOf` — `note`, or `name` + `type`, or `name` + `replace` — so a
client reading the schema can tell which fields go together instead of
inferring it from an error.
- **`memesh import` rejects notes-only flags on the JSON path (#324).** Passing
a note-directory option without `--notes` used to be accepted and ignored.
- **Note files are ingested as memories (#324).** `memesh import --notes
<dir>`, and the Stop hook for the project's own memory directory, upsert
one memory per frontmatter note file, tagged `source:note-file`, with the
Expand Down
2 changes: 1 addition & 1 deletion README.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Alle Befehle und Tools: [docs/api/API_REFERENCE.md](docs/api/API_REFERENCE.md).
| Tool | Was es tut |
|------|-------------|
| `work_package` | Einen begrenzten, nicht vertrauenswürdigen Kalender-Digest oder ein Claude-Code-Transkript-Paket unter einem passenden MCP-Workspace-Root vorbereiten; genau ein striktes Ergebnis zur menschlichen Prüfung einreichen oder ohne dauerhafte Änderung zurückstellen. Die Transkript-Einreichung bewahrt begrenzte redigierte Quellzüge auf; Dateipfad, verborgenes Denken, Provider-, Embedding- oder Vektordaten werden nicht offengelegt. |
| `remember` | Wissen mit Beobachtungen, Relationen und Tags speichern |
| `remember` | Wissen mit Beobachtungen, Relationen und Tags speichern — oder Freitext als `note` übergeben, woraus Titel, Beobachtungen und Name abgeleitet werden; `replace` korrigiert eine Erinnerung an Ort und Stelle |
| `recall` | Lokale FTS5-Suche mit Multi-Faktor-Bewertung (Relevanz, Aktualität, Häufigkeit, Konfidenz, Abruf-Auswirkung) |
| `forget` | Soft-Archivierung (löscht nie) oder entfernt spezifische Beobachtungen |
| `export` | Memories als JSON sichern, migrieren oder zwischen kompatiblen Agenten übertragen |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ Full command and tool reference: [docs/api/API_REFERENCE.md](docs/api/API_REFERE
| Tool | What it does |
|------|-------------|
| `work_package` | Prepare one bounded untrusted calendar digest or Claude Code transcript package under one matching MCP workspace root; submit one strict result for pending human review, or defer without durable change. Transcript submission retains bounded redacted source turns; no file path, hidden reasoning, provider, embedding, or vector data is exposed. |
| `remember` | Store knowledge with observations, relations, and tags |
| `remember` | Store knowledge as observations, relations and tags — or pass free text as `note` and the title, observations and name are derived; `replace` corrects a memory in place |
| `recall` | Local FTS5 search with multi-factor scoring (relevance, recency, frequency, confidence, recall impact) |
| `forget` | Soft-archive (never deletes) or remove specific observations |
| `export` | Back up, migrate, or move memories as JSON between compatible agents |
Expand Down
2 changes: 1 addition & 1 deletion README.zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ memesh serve # 啟動本機 server 並印出儀表板網址
| 工具 | 做什麼 |
|------|--------|
| `work_package` | 準備一份有界限且不受信任的日曆摘要,或從唯一符合的 MCP workspace root 準備 Claude Code transcript 套件;提交一份嚴格結果等待人工審核,或延後而不產生耐久變更。Transcript 提交會保留有界且已遮蔽的來源輪次;不會暴露檔案路徑、隱藏推理、provider、embedding 或 vector 資料。 |
| `remember` | 用觀察、關係和標籤儲存知識 |
| `remember` | 用觀察、關係和標籤儲存知識;也可以只給一段自由文字(`note`),標題、觀察和名稱會自動推導出來;`replace` 則是直接改掉既有的那一筆 |
| `recall` | 本機 FTS5 搜尋,包含多因素評分(相關性、近期性、頻率、信心、回憶影響) |
| `forget` | 軟歸檔(永不刪除)或移除特定觀察 |
| `export` | 以 JSON 備份、搬遷記憶,或在相容代理之間轉移 |
Expand Down
4 changes: 2 additions & 2 deletions dist/skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@
},
{
"path": "skills/memesh/SKILL.md",
"sha256": "a4181c34f4e346911d3c92fe4274264e4649eb3a10f4be619ce3e1bbacf7213e",
"bytes": 20532
"sha256": "c34a9740a5c6531dd4b600ae28cae9061497e126849f53c679827c8c60a69f73",
"bytes": 20669
}
]
}
14 changes: 13 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,13 +247,24 @@ The dashboard is a client of the ordinary HTTP API — no private endpoints —

```
Tool call: remember({name, type, observations, tags, relations})
or: remember({note}) # free text, #324
-> Zod validation (RememberSchema)
-> resolveRememberInput()
-> deriveNote() when `note` was given
-> title from the first line, one observation per paragraph
-> name from a slug of the title + a digest of the text
-> replace: true only:
-> refuse when the memory was archived with forget
-> snapshot the previous title/observations/tags FIRST
-> KnowledgeGraph.clearEntityData(name) # the snapshot must precede this
-> stored type is kept unless a different `type` was passed
-> KnowledgeGraph.createEntity(name, type, {observations, tags})
-> replace: true only: the snapshot -> metadata.replaced_history (after the write)
-> INSERT OR IGNORE into entities
-> INSERT observations
-> Rebuild FTS5 index
-> INSERT OR IGNORE tags
-> Preserve original type on duplicate entity names
-> Preserve original type on duplicate entity names (append path)
-> KnowledgeGraph.createRelation() for each relation
-> Return {stored: true, entityId, ...}
```
Expand Down Expand Up @@ -410,6 +421,7 @@ Hook commands are defined in `hooks/hooks.json`: eight run at Claude Code lifecy
- **Trigger**: `Stop` event (when Claude finishes responding)
- **Matcher**: `*` (all sessions)
- **Behavior**: Extracts session knowledge (files edited, errors fixed, decisions made) with deterministic rules and stores it as entities in the knowledge graph. It reads the newest exact-project injection record under the database directory's `sessions/` folder, strips hook-output echoes, and increments `recall_hits` only for explicit `[mem:id]` citations that match entities injected into that session. `recall_misses` stays unchanged because absence of a citation is not proof that the memory was unused. Opt-out via `MEMESH_AUTO_CAPTURE=false`
- **On the same Stop (#324)**, `scripts/hooks/_stop-notes.js` also ingests the project's memory directory as note files and, separately, prints a remember nudge. Only the ingestion is a write, and only it is gated on `MEMESH_AUTO_CAPTURE=false`; the nudge sits outside that check.

### Pre-Compact (`scripts/hooks/pre-compact.js`)

Expand Down
19 changes: 13 additions & 6 deletions docs/api/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ Two forms. **Structured**: `name` + `type`, with `title` / `observations`. **Not
- `name` (when absent) = slug of the title + `-` + the first 8 hex characters of the SHA-256 of the cleaned text, so the same text twice is one memory (the second call adds nothing); two different texts landing on the same name is possible but very unlikely, not impossible — the suffix is only 32 bits; a title with no ASCII letters or digits slugs to `note`;
- `type` defaults to `"note"`.

The note is cleaned before anything is derived from it: control characters (other than newline and tab) are removed and credential-shaped substrings are replaced with `***REDACTED***`. It may be at most 20,000 characters, and the paragraphs it splits into may not derive more than 100 observations — a paragraph made only of list items yields one observation per item, so a single paragraph can push the count over the limit on its own; beyond that the call is rejected. `note` cannot be combined with `title` or `observations`. A note sent to a `name` that already exists appends its observations and leaves the existing title alone.
The note is cleaned before anything is derived from it: control characters (other than newline and tab) are removed and credential-shaped substrings are replaced with `***REDACTED***`. It may be at most 20,000 characters, and the paragraphs it splits into may not derive more than 100 observations — a paragraph made only of list items yields one observation per item, so a single paragraph can push the count over the limit on its own; beyond that the call is rejected. One derived observation longer than 10,000 characters is **silently truncated** to that length with a trailing `…` — unlike a structured `observations` entry of the same length, which is rejected. Nothing in the response says it happened, so a caller sending one very long paragraph should split it rather than rely on the cap. `note` cannot be combined with `title` or `observations`. A note sent to a `name` that already exists appends its observations and leaves the existing title alone.

**Replace**: `replace: true` with a `name` rewrites that memory: its observations are replaced by the ones given (or derived from `note`), its tags too when `tags` is given (omitted tags are kept), its title when `title` or `note` is given. The previous title, observations and tags are appended to `metadata.replaced_history` as `{ replaced_at, title, observations, tags }`, so the wrong line leaves recall but is not lost. The history keeps the newest 20 versions and at most 64 KB: older versions are dropped first, and a single version larger than that keeps the observations that fit and is marked `truncated: true`. Relations are untouched by a replace. `recall` results do not carry the history — they carry `metadata.replaced_history_count` — so read the versions from `export` or `GET /v1/entities/:name`. The keyword index is rewritten in the same transaction. On a name that does not exist yet, `replace: true` simply creates the memory and reports `replaced: false`. `replace` with `note` requires an explicit `name`.
**Replace**: `replace: true` with a `name` rewrites that memory: its observations are replaced by the ones given (or derived from `note`), its tags too when `tags` is given (omitted tags are kept), its title when `title` or `note` is given. The previous title, observations and tags are appended to `metadata.replaced_history` as `{ replaced_at, title, observations, tags }`, so the wrong line leaves recall but is not lost. The history keeps the newest 20 versions and at most 64 KB: older versions are dropped first, and a single version larger than that keeps the observations that fit and is marked `truncated: true`. Relations are untouched by a replace. `recall` results do not carry the history — they carry `metadata.replaced_history_count` — so read the versions from `export` or `GET /v1/entities/:name`. The keyword index is rewritten in the same transaction. On a name that does not exist yet there is no stored type to inherit, so `replace: true` needs an explicit `type`; with one it creates the memory and reports `replaced: false`, without one it is rejected. A memory archived with `forget` refuses `replace` outright: remember it again without `replace` to bring it back, then replace it. `replace` with `note` requires an explicit `name`.

**Input Schema**:

Expand Down Expand Up @@ -1427,6 +1427,8 @@ never an absolute path.
skipped, and one run reads at most 500 files (the rest are reported as "more"
and picked up by the next run; unchanged files are recognised from their size
and mtime without being read). Credential-shaped text is redacted.
A note file splits into observations exactly as a `note` string does,
including the silent truncation described under `remember`.

Under Claude Code the Stop hook runs the same ingestion on the memory directory
next to the session transcript, throttled by mtime and capped at 100 file reads
Expand Down Expand Up @@ -1465,9 +1467,9 @@ memory layer saved anything lately, and if not, why not". `memesh doctor --json`
"hooks": [
{
"hook": "post-commit", "runs": 20, "triggeredRuns": 5, "writes": 0,
"skips": 20, "errors": 0,
"skips": 20, "errors": 0, "notifies": 0,
"lastRunAt": "2026-09-08T00:00:00.000Z", "firstTriggeredAt": "2026-09-04T00:00:00.000Z",
"lastWriteAt": null, "lastEntity": null, "lastSkipReason": "a git commit ran but printed no commit line",
"lastWriteAt": null, "lastNotifiedAt": null, "lastEntity": null, "lastSkipReason": "a git commit ran but printed no commit line",
"dominantSkipReason": "a git commit ran but printed no commit line", "dominantSkipCount": 5,
"hosts": ["claude-code"], "silent": true
}
Expand All @@ -1483,6 +1485,9 @@ memory layer saved anything lately, and if not, why not". `memesh doctor --json`
trigger did not apply (post-commit on a Bash call that is not a git commit).
`silent` is true only for post-commit, session-summary and pre-compact, when
`triggeredRuns` is at least 5 and `writes` is 0.
`notifies` counts runs that told someone something and stored nothing, so
`runs` is not `writes + skips + errors`. It does not rescue a hook from `silent`
either, and none of the three hooks that `silent` applies to ever notifies.
- `types` — auto-capture entities per type, this week (`last7`) against the
week before (`prev7`); `stopped` means the type wrote last week and nothing
this week.
Expand All @@ -1495,8 +1500,10 @@ stopped type, or heartbeats with no outcome record at all past the grace

The figures come from `hook-outcomes.jsonl` beside the database (the directory
of `MEMESH_DB_PATH`, `~/.memesh` by default): every capture hook appends one
JSON line per run — `hook`, `at`, `host`, `outcome` (`wrote` / `skipped` /
`error`), and a `reason` or `entity` — on every exit path. An error records a
JSON line per run — `hook`, `at`, `host`, `outcome` (`wrote` / `notified` /
`skipped` / `error`), and a `reason` or `entity` — on every exit path. A run
that printed something for a person or model to read and stored nothing
records `notified`. An error records a
label — `uncaught <code or name>`, or a fixed literal such as `malformed stdin
JSON` — never the exception text. Records naming a hook
MeMesh does not ship are ignored, and reason text is stripped of control
Expand Down
8 changes: 4 additions & 4 deletions scripts/mcp-doc-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"schema_version": "mcp-doc-contract/v2",
"source_sha256": "ced0bbac77f4b05d9e74b3f8514d5ba925d3058a0c3dc5995ee7a24bc0264770",
"surfaces": {
"README.md": "a675c51ecc1220c4fd0b390068f317f27400fc5aa1a3a96171fc9261bc551e1e",
"README.zh-TW.md": "0a0e5677b18e7732e64d97cfe1d52570b07352113d994b42c0ba559b1df7e5f7",
"README.de.md": "31d9d8fb507cb876c71bf9a388379c23031c747771dab37e7dc7e83957c3fec0",
"README.md": "a0aa3dbb0bab4a02f611b146e1f55cf83d5f3f4e4bde49adc10a046d3d7e445e",
"README.zh-TW.md": "bd1b959c503a3c912f7fff0a012e1c626cb6639bcdd1d8c7150d21bb0fba4a63",
"README.de.md": "a077386d2a15a862b12700e40bfbf3d06797cbc3421e9d3b1905cf1d3d4a59ab",
"AGENTS.md": "fae5875686223faecbdf26a0092ae6682549a7c31c38ec496c54a6b9cab83e92",
"skills/memesh/SKILL.md": "ec91acad08e982776d0f499f41672217252e0848614d13b4c0602149028f85f8",
"docs/api/API_REFERENCE.md": "31b22679e97509722245b19bc95b139671382bee984755d8d791243f3c50637c",
"docs/api/API_REFERENCE.md": "6d5f087e7e828f85ded4a3609b4adf89a3dc4e266e1e59313b857e9bbb11d944",
"docs/ARCHITECTURE.md": "37b3ae97f6e242e7102cd7ee30876a36503e31400b8ac9e2325ac41d3b965bc2"
}
}
2 changes: 2 additions & 0 deletions skills/memesh/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ Call the `briefing` MCP tool or run `memesh briefing`. It returns the assembled
work topology: where the work was left off (goal / next / blocked / done),
decisions and direction, lessons not to repeat, what is known, recent activity.
One call is cheaper than re-exploring the repo to reconstruct the same picture.
`memesh briefing --index` returns only the index of durable memories — what is
known here, one line each, without the ranked sections.
Generic briefing and SessionStart context do not report unread durable messages:
they have no recipient identity. If you already know the exact logical
recipient, pass `recipient` with `project` (MCP) or use
Expand Down
Loading