diff --git a/CHANGELOG.md b/CHANGELOG.md
index 733fea71..bd23d5c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
@@ -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
`, and the Stop hook for the project's own memory directory, upsert
one memory per frontmatter note file, tagged `source:note-file`, with the
diff --git a/README.de.md b/README.de.md
index a28fdc50..fc1d568a 100644
--- a/README.de.md
+++ b/README.de.md
@@ -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 |
diff --git a/README.md b/README.md
index 0bb19893..9eebc48a 100644
--- a/README.md
+++ b/README.md
@@ -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 |
diff --git a/README.zh-TW.md b/README.zh-TW.md
index d8262011..07b9cfdb 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -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 備份、搬遷記憶,或在相容代理之間轉移 |
diff --git a/dist/skills-manifest.json b/dist/skills-manifest.json
index ae5f6fc9..8fef93fa 100644
--- a/dist/skills-manifest.json
+++ b/dist/skills-manifest.json
@@ -168,8 +168,8 @@
},
{
"path": "skills/memesh/SKILL.md",
- "sha256": "a4181c34f4e346911d3c92fe4274264e4649eb3a10f4be619ce3e1bbacf7213e",
- "bytes": 20532
+ "sha256": "c34a9740a5c6531dd4b600ae28cae9061497e126849f53c679827c8c60a69f73",
+ "bytes": 20669
}
]
}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index b424615c..e9029e72 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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, ...}
```
@@ -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`)
diff --git a/docs/api/API_REFERENCE.md b/docs/api/API_REFERENCE.md
index 36f1bf09..13103cf1 100644
--- a/docs/api/API_REFERENCE.md
+++ b/docs/api/API_REFERENCE.md
@@ -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**:
@@ -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
@@ -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
}
@@ -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.
@@ -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 `, 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
diff --git a/scripts/mcp-doc-contract.json b/scripts/mcp-doc-contract.json
index 0af19dbe..fb2c0827 100644
--- a/scripts/mcp-doc-contract.json
+++ b/scripts/mcp-doc-contract.json
@@ -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"
}
}
diff --git a/skills/memesh/SKILL.md b/skills/memesh/SKILL.md
index a8c98ebb..09c3c5d3 100644
--- a/skills/memesh/SKILL.md
+++ b/skills/memesh/SKILL.md
@@ -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