From b81f87a45432d22170cac01acd16d50cb225f0e8 Mon Sep 17 00:00:00 2001 From: verify-sim Date: Sat, 12 Sep 2026 23:47:37 +0800 Subject: [PATCH 1/4] docs: bring the published docs up to what 4.10.0 actually does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit of the eight public documents against the 214 commits since v4.9.4 found twelve gaps. Two were wrong rather than merely missing, and one was the kind this project keeps writing rules about. Wrong: - CHANGELOG and API_REFERENCE both listed three hook outcomes. There are four — `notified` was added with the capture-liveness work and neither document followed. A reader counting runs would find that writes + skips + errors does not equal runs, with nothing to explain the difference. - The doctor `capture` example omitted `notifies` and `lastNotifiedAt`, which every `--json` response carries. Silent, and worth its own line: a derived observation over 10,000 characters is truncated with a trailing ellipsis and nothing says so — while a structured observation of the same length is rejected. API_REFERENCE documented the rejection and not the truncation, so the reasonable inference from it was wrong in the direction that loses data. Both the `note` field and the note-file path now say it. Missing: - `replace` on a memory archived with `forget` is refused; no document said so. - `replace` keeps the type the memory already has (landed today); every surface had followed except CHANGELOG. - Four behaviour changes since the 4.10.0 section was written had no entry at all: the receipt reporting the stored title, the over-cap message counting observations, the schemas declaring their forms through anyOf, and the JSON import path rejecting notes-only flags. - ARCHITECTURE's remember flow predated both `note` and `replace`, and its Session Summary section did not mention the two further writes a Stop now makes. - The three READMEs still described `remember` as observations-only, although the `briefing` row beside it had been updated for the index. - SKILL.md did not mention `briefing --index`. dist/skills-manifest.json is regenerated because it embeds SKILL.md. --- CHANGELOG.md | 24 ++++++++++++++++++++++-- README.de.md | 2 +- README.md | 2 +- README.zh-TW.md | 2 +- dist/skills-manifest.json | 4 ++-- docs/ARCHITECTURE.md | 13 ++++++++++++- docs/api/API_REFERENCE.md | 22 ++++++++++++++++------ skills/memesh/SKILL.md | 2 ++ 8 files changed, 57 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3057461..77c74650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,8 +47,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 @@ -98,6 +99,25 @@ 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 two 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 db2c70bd..1e8c380b 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..c57211ef 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -247,13 +247,23 @@ 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 + -> KnowledgeGraph.clearEntityData(name) + -> previous title/observations/tags -> metadata.replaced_history + -> stored type is kept unless a different `type` was passed -> KnowledgeGraph.createEntity(name, type, {observations, tags}) -> 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 +420,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` +- **Two further writes on the same Stop (#324)**: the project's own memory directory is ingested as note files (`src/core/note-ingest.ts`, tagged `source:note-file`), and a decision-shaped move since the last Stop prints a remember nudge (`src/core/turn-signal.ts`). The nudge only prints — it stores nothing, and records the `notified` outcome rather than `wrote`. Both writes respect `MEMESH_AUTO_CAPTURE=false`; the printed line does not. ### Pre-Compact (`scripts/hooks/pre-compact.js`) diff --git a/docs/api/API_REFERENCE.md b/docs/api/API_REFERENCE.md index aefa6360..0573f34e 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, `replace: true` simply creates the memory and reports `replaced: false`. A memory archived with `forget` refuses `replace` instead: remember it again without `replace` to bring it back, then replace it. `replace` with `note` requires an explicit `name`. **Input Schema**: @@ -1427,6 +1427,9 @@ 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 the same way a `note` string does, so a + single paragraph over 10,000 characters is silently truncated with a + trailing `…` here too. 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 +1468,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 } @@ -1484,6 +1487,9 @@ memory layer saved anything lately, and if not, why not". `memesh doctor --json` session-summary on a Stop after the session was already captured). `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`; a hook that only notifies still + has `writes` 0 and can be reported silent. - `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. @@ -1496,8 +1502,12 @@ 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`: it counts as triggered, but not as a write, so `runs` is +not `writes + skips + errors` and a hook that only ever notifies is not +silent. 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/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 From adc8299c323a839e83141f03439b080e832a5370 Mon Sep 17 00:00:00 2001 From: verify-sim Date: Sat, 12 Sep 2026 23:49:01 +0800 Subject: [PATCH 2/4] chore(docs): re-sign the four surfaces whose remember description changed The certified hash covers the extracted tool table, so the three READMEs and API_REFERENCE went stale when the `remember` row started mentioning `note` and `replace`. Reviewed before signing: the tool names are unchanged, and the new description matches what the code does. --- scripts/mcp-doc-contract.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mcp-doc-contract.json b/scripts/mcp-doc-contract.json index 0af19dbe..05984298 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": "7fe235ee67e76a6f777a10e93ac1379805b4026b513941061d7c4da6b4221618", "docs/ARCHITECTURE.md": "37b3ae97f6e242e7102cd7ee30876a36503e31400b8ac9e2325ac41d3b965bc2" } } From ddccbe95d58c8be2af01d9f7bbc837a223a05da2 Mon Sep 17 00:00:00 2001 From: verify-sim Date: Sun, 13 Sep 2026 00:27:56 +0800 Subject: [PATCH 3/4] docs: fix three defects an independent verifier found in the previous commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixes written to close twelve documentation gaps were themselves unreviewed code, and three of them were wrong. - API_REFERENCE said a notify-only hook "can be reported silent" eighteen lines above saying it is not silent. The code settles it: no hook that notifies is in SILENT_ELIGIBLE_HOOKS, which is post-commit, session-summary and pre-compact (capture-liveness.ts:293). The sentence now says that instead of contradicting its neighbour. - API_REFERENCE still said `replace: true` on an unknown name "simply creates the memory". It does not: with no stored type to inherit, operations.ts:233 rejects the call unless `type` was passed — which is what the CHANGELOG entry in the same commit said, so the two documents contradicted each other and the contradiction was introduced here. - ARCHITECTURE credited the remember nudge to src/core/turn-signal.ts. It lives in scripts/hooks/_stop-notes.js:384; turn-signal.ts is the Hermes chat-turn classifier, and it writes memories, so the wrong file was named and its real behaviour is the opposite of what the sentence claimed. The same sentence called both Stop actions "writes" while saying one stores nothing, and said both respect MEMESH_AUTO_CAPTURE=false when only the ingest is gated on it (_stop-notes.js:440; the nudge sits outside at :471). --- docs/ARCHITECTURE.md | 2 +- docs/api/API_REFERENCE.md | 8 +++++--- scripts/mcp-doc-contract.json | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c57211ef..2f4e0651 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -420,7 +420,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` -- **Two further writes on the same Stop (#324)**: the project's own memory directory is ingested as note files (`src/core/note-ingest.ts`, tagged `source:note-file`), and a decision-shaped move since the last Stop prints a remember nudge (`src/core/turn-signal.ts`). The nudge only prints — it stores nothing, and records the `notified` outcome rather than `wrote`. Both writes respect `MEMESH_AUTO_CAPTURE=false`; the printed line does not. +- **Two further things happen on the same Stop (#324)**, both in `scripts/hooks/_stop-notes.js`: the project's own memory directory is ingested as note files (`runNoteIngestion`, via `src/core/note-ingest.ts`, tagged `source:note-file`), and a decision-shaped move since the last Stop prints a remember nudge (`decideNudge`). Only the first is a write, and only the first is gated on `MEMESH_AUTO_CAPTURE=false`; the nudge sits outside that check, stores nothing, and records the `notified` outcome rather than `wrote`. `src/core/turn-signal.ts` is a different thing that sounds similar — the Hermes chat-turn classifier, which does write. ### Pre-Compact (`scripts/hooks/pre-compact.js`) diff --git a/docs/api/API_REFERENCE.md b/docs/api/API_REFERENCE.md index 0573f34e..a415ccd7 100644 --- a/docs/api/API_REFERENCE.md +++ b/docs/api/API_REFERENCE.md @@ -53,7 +53,7 @@ Two forms. **Structured**: `name` + `type`, with `title` / `observations`. **Not 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`. A memory archived with `forget` refuses `replace` instead: remember it again without `replace` to bring it back, then replace it. `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**: @@ -1488,8 +1488,10 @@ memory layer saved anything lately, and if not, why not". `memesh doctor --json` `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`; a hook that only notifies still - has `writes` 0 and can be reported silent. + `runs` is not `writes + skips + errors`. It does not rescue a hook from + `silent` either — but no hook that notifies is eligible to be called + silent in the first place: that list is post-commit, session-summary and + pre-compact. - `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. diff --git a/scripts/mcp-doc-contract.json b/scripts/mcp-doc-contract.json index 05984298..fb2c0827 100644 --- a/scripts/mcp-doc-contract.json +++ b/scripts/mcp-doc-contract.json @@ -7,7 +7,7 @@ "README.de.md": "a077386d2a15a862b12700e40bfbf3d06797cbc3421e9d3b1905cf1d3d4a59ab", "AGENTS.md": "fae5875686223faecbdf26a0092ae6682549a7c31c38ec496c54a6b9cab83e92", "skills/memesh/SKILL.md": "ec91acad08e982776d0f499f41672217252e0848614d13b4c0602149028f85f8", - "docs/api/API_REFERENCE.md": "7fe235ee67e76a6f777a10e93ac1379805b4026b513941061d7c4da6b4221618", + "docs/api/API_REFERENCE.md": "6d5f087e7e828f85ded4a3609b4adf89a3dc4e266e1e59313b857e9bbb11d944", "docs/ARCHITECTURE.md": "37b3ae97f6e242e7102cd7ee30876a36503e31400b8ac9e2325ac41d3b965bc2" } } From 799dc67162750c4c34c99603f9c12ff56292e305 Mon Sep 17 00:00:00 2001 From: verify-sim Date: Sun, 13 Sep 2026 01:11:30 +0800 Subject: [PATCH 4/4] docs: fix three more defects two reviews found in my own sentences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A fresh-eyes review and a simplification pass, both on the same diff, and both found things in prose I had written to close earlier findings. Six defects in about fifty lines now. - API_REFERENCE derived the opposite conclusion from its own premises: it said a notify-only hook "is not silent" because notifies count as triggered and not as writes — which are exactly the two conditions that make a hook silent (capture-liveness.ts:735-742). The sentence was also a second copy of what the same file says twenty lines earlier, and the copy is the one that was wrong. Deleted rather than corrected; the correct statement stays where it was, and the three-item hook list it repeated is no longer repeated either. - CHANGELOG said the schemas "state the two forms". There are three, and the same bullet listed all three: note, name + type, name + replace. The code comment at handlers.ts:150 says "Three complete forms". - ARCHITECTURE's replace flow had the order backwards. The previous version is snapshotted BEFORE clearEntityData, which has to be true because the clear deletes the observations being snapshotted; and replaced_history is written AFTER createEntity, not before (operations.ts:259-264, :331-337). Following the documented order would produce an empty history. Also drops a duplicated truncation note, the blank line that was making the 4.10.0 list render loose, and the aside naming turn-signal.ts — no document mentions that file, so the sentence created the confusion it was resolving. --- CHANGELOG.md | 3 +-- docs/ARCHITECTURE.md | 7 ++++--- docs/api/API_REFERENCE.md | 15 +++++---------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c74650..61e3206d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,13 +111,12 @@ All notable changes to MeMesh are documented here. - **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 two forms (#324).** `remember` +- **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/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2f4e0651..e9029e72 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -255,10 +255,11 @@ Tool call: remember({name, type, observations, tags, relations}) -> name from a slug of the title + a digest of the text -> replace: true only: -> refuse when the memory was archived with forget - -> KnowledgeGraph.clearEntityData(name) - -> previous title/observations/tags -> metadata.replaced_history + -> 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 @@ -420,7 +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` -- **Two further things happen on the same Stop (#324)**, both in `scripts/hooks/_stop-notes.js`: the project's own memory directory is ingested as note files (`runNoteIngestion`, via `src/core/note-ingest.ts`, tagged `source:note-file`), and a decision-shaped move since the last Stop prints a remember nudge (`decideNudge`). Only the first is a write, and only the first is gated on `MEMESH_AUTO_CAPTURE=false`; the nudge sits outside that check, stores nothing, and records the `notified` outcome rather than `wrote`. `src/core/turn-signal.ts` is a different thing that sounds similar — the Hermes chat-turn classifier, which does write. +- **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 a415ccd7..cf6ad3ac 100644 --- a/docs/api/API_REFERENCE.md +++ b/docs/api/API_REFERENCE.md @@ -1427,9 +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 the same way a `note` string does, so a - single paragraph over 10,000 characters is silently truncated with a - trailing `…` here too. + 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 @@ -1488,10 +1487,8 @@ memory layer saved anything lately, and if not, why not". `memesh doctor --json` `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 — but no hook that notifies is eligible to be called - silent in the first place: that list is post-commit, session-summary and - pre-compact. + `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. @@ -1507,9 +1504,7 @@ of `MEMESH_DB_PATH`, `~/.memesh` by default): every capture hook appends one 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`: it counts as triggered, but not as a write, so `runs` is -not `writes + skips + errors` and a hook that only ever notifies is not -silent. An error records a +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