From 46da7f2df7cbcfa234ee29b9455a66654406211f Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:18:42 -0400 Subject: [PATCH 1/4] feat(memory): per-file entry-policy + Agents directives template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Memory files gain a machine-readable entry-policy frontmatter property: append-only (the default; absent means this) or living (a current-state file whose expired entries are pruned rather than left as history). - vault_list_memory_files surfaces entry_policy per file (unrecognized values resolve to append-only — the safe reading) - vault_update_memory / vault_delete_memory descriptions state the default and the living exception; vault_delete_memory now names the case where deletion is intended maintenance - memory-review prompt labels each file's policy in its structural overview and gains a living-files-only reflection step for expired entries; append-only files keep the never-prune contract - New Agents template (6th memory file): directives for how agents communicate, work, and verify — split from facts about the user; templates README documents the who-is-the-subject routing test - Routines template becomes the living example, restructured to Active commitments / Upcoming / Daily-weekly rhythm / Recent past - All templates (in-code specs + templates/memory/) declare their policy, enumerate their sections in the scope callout, and cross-reference Agents; shrink-guard floor recalibrated to sit above the new largest empty template (Routines 1228 B) Co-Authored-By: Claude Fable 5 --- .devin/wiki.json | 2 +- AGENTS.md | 6 +- ARCHITECTURE.md | 2 +- deploy/local/README.md | 8 +- deploy/remote/README.md | 8 +- .../__tests__/memory-review-prompt.test.ts | 25 ++++- .../__tests__/tool-definitions.test.ts | 15 ++- .../mcp-core/prompts/memory-review-prompt.ts | 12 ++- src/vault-mcp/mcp-core/tools/memory-tools.ts | 12 +-- .../__tests__/memory-store.test.ts | 91 ++++++++++++++++- .../vault-operations/memory-store.ts | 97 ++++++++++++++----- templates/memory/Agents.md | 28 ++++++ templates/memory/Me.md | 8 +- templates/memory/Opinions.md | 8 +- templates/memory/Principles.md | 8 +- templates/memory/README.md | 52 +++++++++- templates/memory/Routines.md | 18 ++-- 17 files changed, 334 insertions(+), 66 deletions(-) create mode 100644 templates/memory/Agents.md diff --git a/.devin/wiki.json b/.devin/wiki.json index 9b90ed544..e30ec624d 100644 --- a/.devin/wiki.json +++ b/.devin/wiki.json @@ -74,7 +74,7 @@ }, { "title": "Prompt Reference", - "purpose": "The 3 MCP prompts, organized in group modules under mcp-core/prompts/ (mirroring the tools/ pattern) with prompt-definitions.ts as the orchestrator — each queries the search index, link graph, and memory layer at invocation time, assembling live vault data with guided instructions. vault-orientation (zero-arg: vault stats with total notes/untagged/broken links, folder note counts, property adoption rates with low-adoption flags, orphan detection via findOrphans, tag cloud, recent notes, memory outline, and contextual tool suggestions based on findings); memory-review (structural overview from listMemoryFiles showing scope callouts, section entry counts, and file sizes before the raw content; reads memory as an append-only evolution; guided reflection adds scope-fit against declared scopes and coverage analysis for gaps); daily-review (outgoing links via getOutgoingLinks with broken-link flags, backlinks via getBacklinks, date-specific activity via modifiedOnDate instead of global recentNotes so past-date reviews show that date's work; guides reconciliation, link following, and pattern recognition). Prompts degrade gracefully — an empty memory layer or missing daily note returns a valid message with getting-started guidance, never a hard failure. Autocomplete is supported on the memory-review file argument.", + "purpose": "The 3 MCP prompts, organized in group modules under mcp-core/prompts/ (mirroring the tools/ pattern) with prompt-definitions.ts as the orchestrator — each queries the search index, link graph, and memory layer at invocation time, assembling live vault data with guided instructions. vault-orientation (zero-arg: vault stats with total notes/untagged/broken links, folder note counts, property adoption rates with low-adoption flags, orphan detection via findOrphans, tag cloud, recent notes, memory outline, and contextual tool suggestions based on findings); memory-review (structural overview from listMemoryFiles showing scope callouts, section entry counts, and file sizes before the raw content; reads memory as an append-only evolution (files marked entry-policy: living are current-state snapshots whose expired entries may be pruned); guided reflection adds scope-fit against declared scopes and coverage analysis for gaps); daily-review (outgoing links via getOutgoingLinks with broken-link flags, backlinks via getBacklinks, date-specific activity via modifiedOnDate instead of global recentNotes so past-date reviews show that date's work; guides reconciliation, link following, and pattern recognition). Prompts degrade gracefully — an empty memory layer or missing daily note returns a valid message with getting-started guidance, never a hard failure. Autocomplete is supported on the memory-review file argument.", "parent": "MCP Interface" }, { diff --git a/AGENTS.md b/AGENTS.md index 6892903b7..cc1ffd4aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -480,7 +480,11 @@ from tools: returns a valid fallback message; a prompt must not hard-fail the client. - **`memory-review` is append-only by design** — it reads the memory layer as a dated **evolution** (never "newest supersedes older"), proposes only - append updates, and never prunes "stale" entries. + append updates, and never prunes "stale" entries. The one exception: a + memory file whose frontmatter declares `entry-policy: living` is a + current-state snapshot, and the review may propose pruning its expired + entries (the policy is surfaced by `vault_list_memory_files`; absent + means append-only). ### MCP naming conventions diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a4850c405..73109dcf9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -392,7 +392,7 @@ Alongside tools, the server registers MCP **prompts** (`prompts/list` / `prompts | `memory-review` | `file?`, `max_chars?` | Structural overview (scope callouts from `listMemoryFiles`, section entry counts) + dated content as a timeline. Guided reflection: evolution narrative, scope-fit against declared scopes, backfill gaps, coverage analysis. Append-only by design. | | `daily-review` | `date?`, `max_chars?` | Reconciles a day — daily note content + outgoing links (via `getOutgoingLinks`, with broken-link flags) + backlinks (via `getBacklinks`) + date-specific activity (via `modifiedOnDate`) + vault-wide task status (due/overdue, scheduled, daily-note-scoped via `listTasks`). Surfaces what happened, what's open, and what needs follow-up. | -Each handler degrades to a valid message rather than throwing, so a prompt never hard-fails the client. `memory-review` is deliberately append-only: it reads dated entries as a timeline (each entry true when written), never as "newest supersedes older," and never prunes "stale" entries — matching the memory layer's design. `daily-review` uses `modifiedOnDate` instead of `recentNotes`, so past-date reviews show activity from _that_ date — not today's globally recent notes. +Each handler degrades to a valid message rather than throwing, so a prompt never hard-fails the client. `memory-review` is deliberately append-only: it reads dated entries as a timeline (each entry true when written), never as "newest supersedes older," and never prunes "stale" entries — matching the memory layer's default. The exception is a memory file whose frontmatter declares `entry-policy: living` (a current-state snapshot, e.g. the Routines template): `vault_list_memory_files` surfaces the policy as `entry_policy`, and the review may propose pruning expired entries there. `daily-review` uses `modifiedOnDate` instead of `recentNotes`, so past-date reviews show activity from _that_ date — not today's globally recent notes. ## Infrastructure diff --git a/deploy/local/README.md b/deploy/local/README.md index f28e1916b..c99ef2f22 100644 --- a/deploy/local/README.md +++ b/deploy/local/README.md @@ -196,8 +196,12 @@ File Explorer at `\\wsl$\Ubuntu\home\you\vaults\MyVault`. On first startup, if your vault doesn't already have a memory folder (default: `About Me/`), the server creates one with template files (Me.md, Opinions.md, -Principles.md). Agents can also create new memory files and sections on the fly -via `vault_update_memory` — no manual setup needed. +Principles.md, Routines.md, Agents.md). Agents can also create new memory files +and sections on the fly via `vault_update_memory` — no manual setup needed. +Memory files are append-only by default; a file can declare +`entry-policy: living` in frontmatter for current-state content whose expired +entries get pruned (the Routines template ships this way) — see +[templates/memory](../../templates/memory/README.md) for the full convention. ## Configuration diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 75175ccc2..273a74297 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -264,8 +264,12 @@ docker compose down -v On first startup, if your vault doesn't already have a memory folder (default: `About Me/`), the server creates one with template files (Me.md, Opinions.md, -Principles.md). Agents can also create new memory files and sections on the fly -via `vault_update_memory` — no manual setup needed. +Principles.md, Routines.md, Agents.md). Agents can also create new memory files +and sections on the fly via `vault_update_memory` — no manual setup needed. +Memory files are append-only by default; a file can declare +`entry-policy: living` in frontmatter for current-state content whose expired +entries get pruned (the Routines template ships this way) — see +[templates/memory](../../templates/memory/README.md) for the full convention. ## Configuration diff --git a/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts b/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts index 8b5f2b2f6..4b0d97a58 100644 --- a/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts @@ -353,7 +353,7 @@ describe("memory-review full prompt output", () => { "", "1 memory file in About Me/:", "", - "- **Mem** (98 bytes)", + "- **Mem** (98 bytes, append-only)", " - Notes (newest first) (1 entries)", "", "## Current memory", @@ -372,9 +372,30 @@ describe("memory-review full prompt output", () => { "3. **Backfill gaps.** Point out durable facts that are implied but not yet captured, and propose them as dated append entries (bullet + target file + section).", "4. **Corrections (rare, separate).** Only a fact that is mis-recorded or now genuinely incorrect — not one that simply changed over time — warrants a fix. Prefer an appended dated correction that preserves the old entry (history matters); reserve vault_delete_memory for genuinely wrong facts.", "5. **Coverage analysis.** What areas of the user's life, work, or preferences are NOT yet represented? Use the file scopes and section names above to identify gaps worth filling.", + "6. **Expired current-state entries (living files only).** A file marked `living` in the Structure section is a current-state snapshot, not a history ledger — flag entries whose date or commitment has passed and propose pruning them (vault_delete_memory), with the outcome appended to a history section when worth keeping. Never propose this for append-only files.", "", - "Propose every change as an explicit vault_update_memory call (newest-first; the server stamps the date) and **confirm with me before writing anything**. Never delete an entry just for being old.", + "Propose every change as an explicit vault_update_memory call (newest-first; the server stamps the date) and **confirm with me before writing anything**. Never delete an entry just for being old from an append-only file.", ].join("\n"), ) }) + + it("labels a living file's entry policy in the structural overview", async () => { + const vault = await mkdtemp(join(tmpdir(), "prompt-living-")) + onTestFinished(async () => { + await rm(vault, { recursive: true, force: true }) + }) + await mkdir(join(vault, "About Me"), { recursive: true }) + await writeFile( + join(vault, "About Me", "Routines.md"), + "---\ntitle: Routines\ntype: profile\nentry-policy: living\n---\n\n# Routines\n\n## Upcoming (newest first)\n- **2026-07-11**: a plan\n", + "utf8", + ) + const calls = registerWithSearch(vault, {} as SearchIndex) + const handler = findCall(calls, PROMPT_NAMES.MEMORY_REVIEW)[2] + + const text = textOf(await handler({ file: "Routines" }, fakeExtra)) + // The policy must reach the overview line — this fails if the frontmatter + // property stops flowing through listMemoryFiles into the prompt. + expect(text).toContain("bytes, living)") + }) }) diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index eb722fde8..1ff1ee6ef 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -165,7 +165,20 @@ describe("registerTools", () => { // Assert the full contract fragment — a bare "idempotent" check would // also pass on a reworded "not idempotent" description. expect(config.description).toContain( - "idempotent — an exact duplicate (same date + text in the same section) is a no-op", + "Idempotent — an exact duplicate (same date + text in the same section) is a no-op", + ) + }) + + it("memory tool descriptions document the entry-policy contract", () => { + // Append-only is the default; the living opt-in must be discoverable from + // the tools that write, delete, and list memory — not only from templates. + const [, updateConfig] = requireCall(TOOL_NAMES.VAULT_UPDATE_MEMORY) + expect(updateConfig.description).toContain("entry-policy: living") + const [, deleteConfig] = requireCall(TOOL_NAMES.VAULT_DELETE_MEMORY) + expect(deleteConfig.description).toContain("entry-policy: living") + const [, listConfig] = requireCall(TOOL_NAMES.VAULT_LIST_MEMORY_FILES) + expect(listConfig.description).toContain( + 'entry_policy is "append-only" (the default', ) }) diff --git a/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts b/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts index 84c95ea7c..f23d9af51 100644 --- a/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts @@ -3,7 +3,10 @@ * The memory layer is append-with-dates, read as an EVOLUTION — never a * "newest supersedes older" record. This prompt narrates the trajectory and * proposes append-only changes; it deliberately does not hunt for "stale" - * entries to prune or frame evolving beliefs as contradictions to reconcile. */ + * entries to prune or frame evolving beliefs as contradictions to reconcile. + * The one exception is a file whose frontmatter declares `entry-policy: + * living` (a current-state snapshot, e.g. a Routines file): there, expired + * entries are maintenance debt, and the review may propose pruning them. */ import { completable } from "@modelcontextprotocol/sdk/server/completable.js" import { z } from "zod" @@ -26,7 +29,7 @@ export { PROMPT_NAMES as MEMORY_REVIEW_PROMPT_NAMES } /** Formats a single memory file outline as a bullet with scope and section details. */ const formatFileOutline = (outline: MemoryFileOutline): string => { - const titleLine = `- **${outline.file}** (${outline.bytes} bytes)` + const titleLine = `- **${outline.file}** (${outline.bytes} bytes, ${outline.entry_policy})` const scopeLines = (outline.leading_callout?.body ?? "") .split("\n") @@ -70,7 +73,7 @@ export const registerMemoryReviewPrompt = ({ PROMPT_NAMES.MEMORY_REVIEW, { title: "Reflect on memory (read as an evolution)", - description: `Reflect on the ${config.memoryDir}/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old.`, + description: `Reflect on the ${config.memoryDir}/ memory layer — review its structure and scopes, read dated entries as a timeline, surface scope-fit issues and coverage gaps, and propose append-only updates. Never prunes entries for being old, except expired entries in files marked entry-policy: living.`, argsSchema: { file: completable( z @@ -200,8 +203,9 @@ export const registerMemoryReviewPrompt = ({ "3. **Backfill gaps.** Point out durable facts that are implied but not yet captured, and propose them as dated append entries (bullet + target file + section).", `4. **Corrections (rare, separate).** Only a fact that is mis-recorded or now genuinely incorrect — not one that simply changed over time — warrants a fix. Prefer an appended dated correction that preserves the old entry (history matters); reserve vault_delete_memory for genuinely wrong facts.`, "5. **Coverage analysis.** What areas of the user's life, work, or preferences are NOT yet represented? Use the file scopes and section names above to identify gaps worth filling.", + "6. **Expired current-state entries (living files only).** A file marked `living` in the Structure section is a current-state snapshot, not a history ledger — flag entries whose date or commitment has passed and propose pruning them (vault_delete_memory), with the outcome appended to a history section when worth keeping. Never propose this for append-only files.", "", - "Propose every change as an explicit vault_update_memory call (newest-first; the server stamps the date) and **confirm with me before writing anything**. Never delete an entry just for being old.", + "Propose every change as an explicit vault_update_memory call (newest-first; the server stamps the date) and **confirm with me before writing anything**. Never delete an entry just for being old from an append-only file.", ].join("\n") reqLogger.info("prompt_result", { outcome: "ok", diff --git a/src/vault-mcp/mcp-core/tools/memory-tools.ts b/src/vault-mcp/mcp-core/tools/memory-tools.ts index e6a89c1c9..d0b50b23c 100644 --- a/src/vault-mcp/mcp-core/tools/memory-tools.ts +++ b/src/vault-mcp/mcp-core/tools/memory-tools.ts @@ -94,7 +94,7 @@ Returns: Raw markdown text.`, TOOL_NAMES.VAULT_UPDATE_MEMORY, { title: "Update Memory", - description: `Append a dated entry to a section of a ${config.memoryDir}/ memory file. The server prefixes the date automatically ("- **YYYY-MM-DD**: entry text") and inserts newest-first by default. Append-only and idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe; when a preference changes, append the new state (newest wins) rather than deleting the old one. + description: `Append a dated entry to a section of a ${config.memoryDir}/ memory file. The server prefixes the date automatically ("- **YYYY-MM-DD**: entry text") and inserts newest-first by default. Idempotent — an exact duplicate (same date + text in the same section) is a no-op, so retrying a timed-out call is safe. Memory files are append-only by default: when a preference changes, append the new state (newest wins) rather than deleting the old one. A file may declare \`entry-policy: living\` in frontmatter (surfaced by vault_list_memory_files) — a current-state file where pruning expired entries is expected maintenance rather than a violation. Example: vault_update_memory({ file: "Opinions", section: "Code patterns (newest first)", entry: "Prefer immutable data structures" }) @@ -201,16 +201,16 @@ Returns: Confirmation message (notes when an identical entry already existed and TOOL_NAMES.VAULT_LIST_MEMORY_FILES, { title: "List Memory Files", - description: `Discovery tool — lists ${config.memoryDir}/ memory files with their H1/H2 heading structure, per-section entry counts, and each file's leading callout (by convention a "Scope of this file" block describing what belongs in it). Does NOT return actual entries. + description: `Discovery tool — lists ${config.memoryDir}/ memory files with their H1/H2 heading structure, per-section entry counts, entry policy, and each file's leading callout (by convention a "Scope of this file" block describing what belongs in it). Does NOT return actual entries. -Example: vault_list_memory_files() returns file outlines with headings like "Decision heuristics (newest first)", entry counts, and the file's scope callout. +Example: vault_list_memory_files() returns file outlines with headings like "Decision heuristics (newest first)", entry counts, each file's entry policy, and its scope callout. -When to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names. +When to use: Discovering what memory files and sections exist — and what each file is for — BEFORE calling vault_get_memory, vault_update_memory, or vault_delete_memory. Always call this first to get valid file and section names, and to check a file's entry policy before pruning entries. Errors: - An empty or nonexistent memory folder returns an empty array, not an error. -Returns: JSON array of file outlines, each { file, title, bytes, leading_callout, headings } — bytes is the on-disk file size; leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a "Scope of this file" block, or null.`, +Returns: JSON array of file outlines, each { file, title, bytes, entry_policy, leading_callout, headings } — bytes is the on-disk file size; entry_policy is "append-only" (the default — entries are never edited or deleted) or "living" (a current-state file whose expired entries may be pruned; declared via \`entry-policy\` frontmatter); leading_callout is the file's top-of-file callout ({ type, title, body }), by convention a "Scope of this file" block, or null.`, inputSchema: {}, annotations: { readOnlyHint: true, @@ -244,7 +244,7 @@ Returns: JSON array of file outlines, each { file, title, bytes, leading_callout Example: vault_delete_memory({ file: "Opinions", section: "AI tooling & memory (newest first)", date: "2026-05-01", entry: "Prefer X over Y" }) -When to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory is append-only by design, so do NOT delete to reflect a change: when a preference or fact has since evolved, append the new state via vault_update_memory (newest-first naturally supersedes). Call vault_get_memory(file, section) first to see exact entry text for matching. +When to use: Removing an entry that was wrong when it was written — a mistake, a misattribution, or something never true. Memory files are append-only by default, so do NOT delete to reflect a change: when a preference or fact has since evolved, append the new state via vault_update_memory (newest-first naturally supersedes). The exception is a file whose frontmatter declares \`entry-policy: living\` (check via vault_list_memory_files) — a current-state file where deleting an expired entry is the intended maintenance. Call vault_get_memory(file, section) first to see exact entry text for matching. Prefer vault_update_memory to supersede a changed entry; prefer vault_delete_note for deleting entire non-protected notes. Parameters: diff --git a/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts b/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts index 1c35368d7..c1fbc14d8 100644 --- a/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts +++ b/src/vault-mcp/vault-operations/__tests__/memory-store.test.ts @@ -1120,6 +1120,56 @@ describe("listMemoryFiles", () => { expect(noTitle?.title).toBe("NoTitle") }) + it("defaults entry_policy to append-only when the property is absent", async () => { + // The base fixtures (Principles, Opinions) declare no entry-policy. + const outlines = await listMemoryFiles({ vaultPath: vault }, logger) + const principles = outlines.find((outline) => outline.file === "Principles") + expect(principles?.entry_policy).toBe("append-only") + }) + + it("surfaces entry_policy living when declared in frontmatter", async () => { + await writeFile( + join(vault, "About Me/Living.md"), + [ + "---", + "title: Living", + "type: profile", + "entry-policy: living", + "---", + "", + "# Living", + "", + "## Upcoming (newest first)", + "- **2026-07-11**: a current-state entry", + ].join("\n"), + "utf8", + ) + const outlines = await listMemoryFiles({ vaultPath: vault }, logger) + const living = outlines.find((outline) => outline.file === "Living") + expect(living?.entry_policy).toBe("living") + }) + + it("treats an unrecognized entry-policy value as append-only", async () => { + // Only the explicit "living" opt-in relaxes append-only; a typo must not + // silently authorize destructive maintenance. + await writeFile( + join(vault, "About Me/Typo.md"), + [ + "---", + "title: Typo", + "type: profile", + "entry-policy: sometimes", + "---", + "", + "# Typo", + ].join("\n"), + "utf8", + ) + const outlines = await listMemoryFiles({ vaultPath: vault }, logger) + const typo = outlines.find((outline) => outline.file === "Typo") + expect(typo?.entry_policy).toBe("append-only") + }) + it("does not treat a heading-looking line inside a code fence as a section", async () => { // The shared heading parser is fence-aware, so a "## ..."-looking line inside // a code block is not surfaced as a section. The prior memory-local parser was @@ -1279,8 +1329,9 @@ describe("bootstrapMemoryDir", () => { const emptyVault = await mkdtemp(join(tmpdir(), "bootstrap-")) await bootstrapMemoryDir({ vaultPath: emptyVault }, logger) const outlines = await listMemoryFiles({ vaultPath: emptyVault }, logger) - expect(outlines).toHaveLength(4) + expect(outlines).toHaveLength(5) expect(outlines.map((outline) => outline.file).sort()).toEqual([ + "Agents", "Me", "Opinions", "Principles", @@ -1299,11 +1350,49 @@ describe("bootstrapMemoryDir", () => { const parsed = parseNote(raw) expect(parsed.data.title).toBe("Principles") expect(parsed.data.type).toBe("profile") + expect(parsed.data["entry-policy"]).toBe("append-only") expect(parsed.data.tags).toEqual(["memory", "principles"]) expect(parsed.data.created).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/) expect(parsed.data.related).toEqual([ "[[About Me/Opinions]]", "[[About Me/Me]]", + "[[About Me/Agents]]", + ]) + await rm(emptyVault, { recursive: true }) + }) + + it("bootstraps the Agents template with directive sections", async () => { + const emptyVault = await mkdtemp(join(tmpdir(), "bootstrap-agents-")) + await bootstrapMemoryDir({ vaultPath: emptyVault }, logger) + const outlines = await listMemoryFiles({ vaultPath: emptyVault }, logger) + const agents = outlines.find((outline) => outline.file === "Agents") + expect(agents).toBeDefined() + expect(agents?.entry_policy).toBe("append-only") + const sectionNames = agents?.headings + .filter((heading) => heading.level === 2) + .map((heading) => heading.text) + expect(sectionNames).toEqual([ + "Communication (newest first)", + "Working style (newest first)", + "Verification & scope (newest first)", + ]) + await rm(emptyVault, { recursive: true }) + }) + + it("bootstraps the Routines template as a living current-state file", async () => { + const emptyVault = await mkdtemp(join(tmpdir(), "bootstrap-living-")) + await bootstrapMemoryDir({ vaultPath: emptyVault }, logger) + const outlines = await listMemoryFiles({ vaultPath: emptyVault }, logger) + const routines = outlines.find((outline) => outline.file === "Routines") + expect(routines?.entry_policy).toBe("living") + const sectionNames = routines?.headings + .filter((heading) => heading.level === 2) + .map((heading) => heading.text) + expect(sectionNames).toEqual([ + "Active commitments (newest first)", + "Upcoming (newest first)", + "Daily/weekly rhythm (newest first)", + "Recent past (newest first)", ]) await rm(emptyVault, { recursive: true }) }) diff --git a/src/vault-mcp/vault-operations/memory-store.ts b/src/vault-mcp/vault-operations/memory-store.ts index ec1bf7d56..9815b0e68 100644 --- a/src/vault-mcp/vault-operations/memory-store.ts +++ b/src/vault-mcp/vault-operations/memory-store.ts @@ -18,11 +18,12 @@ import type { Logger } from "../../logger.js" // Refuse a memory write that would remove more than half of an existing file's // bytes — a catastrophic shrink almost always means the on-disk copy diverged // (e.g. a skeleton template clobbering real content) rather than a legitimate -// single-entry edit. The 200-byte floor sits just -// above the largest empty memory template (Me 152 B, Principles 193 B, -// Opinions 197 B — frontmatter + headings, no entries), so a file with no real -// content is never guarded, while a file with even one dated entry (~240 B+) is. -const SHRINK_FLOOR_BYTES = 200 +// single-entry edit. The 1300-byte floor sits just above the largest empty +// memory template (Routines 1228 B; Agents 1081 B; Me/Opinions/Principles +// ~900 B — frontmatter + scope callout + headings, no entries), so a file with +// no real content is never guarded, while a file that has accumulated real +// entries beyond the skeleton is. +const SHRINK_FLOOR_BYTES = 1300 const SHRINK_RATIO = 0.5 const guardAgainstShrink = ( beforeBytes: number, @@ -86,10 +87,26 @@ type MemoryHeading = Readonly<{ entryCount?: number }> +/** How a memory file's entries may be maintained. Append-only is the layer's + * default: entries are never edited or deleted, and corrections arrive as new + * dated entries. A file declares `entry-policy: living` in frontmatter when it + * is a current-state snapshot (e.g. a Routines file) — there, expired entries + * are pruned rather than left as history, so agents must not assume its + * timeline is complete. */ +export type MemoryEntryPolicy = "append-only" | "living" + +/** Resolves a frontmatter `entry-policy` value to a policy, treating anything + * other than the explicit "living" opt-in (missing, misspelled, wrong type) + * as the append-only default — the safe reading, since append-only forbids + * destructive maintenance. */ +const entryPolicyFromFrontmatter = (value: unknown): MemoryEntryPolicy => + value === "living" ? "living" : "append-only" + export type MemoryFileOutline = Readonly<{ file: string title: string bytes: number + entry_policy: MemoryEntryPolicy leading_callout: LeadingCallout | null headings: MemoryHeading[] }> @@ -197,6 +214,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { fileName: string title: string tag: string + entryPolicy: MemoryEntryPolicy related: string[] scope: string sections: string[] @@ -207,13 +225,14 @@ export const createMemoryStore = (options: { memoryDir: string }) => { fileName: "Me", title: "Me", tag: "identity", - related: ["Opinions", "Principles", "Routines"], + entryPolicy: "append-only", + related: ["Opinions", "Principles", "Routines", "Agents"], scope: [ "> [!info] Scope of this file", "> **Contains:** Identity, interests, and durable context about the user — who they are, what they're into, situational facts.", - "> **Does NOT contain:** Opinions or preferences (→ Opinions), guiding principles (→ Principles), recurring routines (→ Routines).", - '> **Section structure:** H2 sections grouped by theme, each suffixed "(newest first)".', - "> **Convention:** append newest first; never overwrite dated entries; ISO dates only.", + "> **Does NOT contain:** Opinions or preferences (→ Opinions), guiding principles (→ Principles), recurring routines (→ Routines), directives for AI agents (→ Agents).", + '> **Section structure:** Identity, Interests, Context — each suffixed "(newest first)".', + "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ "Identity (newest first)", @@ -225,13 +244,14 @@ export const createMemoryStore = (options: { memoryDir: string }) => { fileName: "Opinions", title: "Opinions", tag: "opinions", - related: ["Principles", "Me"], + entryPolicy: "append-only", + related: ["Principles", "Me", "Agents"], scope: [ "> [!info] Scope of this file", "> **Contains:** Evolving views on tools, patterns, methods, and processes — stances that may shift over time.", - "> **Does NOT contain:** Stable values or decision heuristics (→ Principles), identity or interests (→ Me).", - '> **Section structure:** H2 sections by topic, each suffixed "(newest first)".', - "> **Convention:** append newest first; never overwrite dated entries; ISO dates only.", + "> **Does NOT contain:** Stable values or decision heuristics (→ Principles), identity or interests (→ Me), directives for AI agents (→ Agents).", + '> **Section structure:** Tools and workflows, Code patterns, Communication preferences — each suffixed "(newest first)".', + "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ "Tools and workflows (newest first)", @@ -243,13 +263,14 @@ export const createMemoryStore = (options: { memoryDir: string }) => { fileName: "Principles", title: "Principles", tag: "principles", - related: ["Opinions", "Me"], + entryPolicy: "append-only", + related: ["Opinions", "Me", "Agents"], scope: [ "> [!info] Scope of this file", "> **Contains:** Stable values, decision heuristics, and non-negotiables — how the user thinks and what they hold firm.", - "> **Does NOT contain:** Evolving opinions on tools or methods (→ Opinions), identity facts (→ Me).", - '> **Section structure:** H2 sections by theme, each suffixed "(newest first)".', - "> **Convention:** append newest first; never overwrite dated entries; ISO dates only.", + "> **Does NOT contain:** Evolving opinions on tools or methods (→ Opinions), identity facts (→ Me), directives for AI agents (→ Agents).", + '> **Section structure:** Decision heuristics, Working style, Non-negotiables — each suffixed "(newest first)".', + "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ "Decision heuristics (newest first)", @@ -261,18 +282,39 @@ export const createMemoryStore = (options: { memoryDir: string }) => { fileName: "Routines", title: "Routines", tag: "routines", - related: ["Me"], + entryPolicy: "living", + related: ["Me", "Agents"], scope: [ "> [!info] Scope of this file", - "> **Contains:** Recurring routines, cadences, and practiced habits — what the user actually does on a regular rhythm.", - "> **Does NOT contain:** One-off events or plans, identity facts (→ Me), principles (→ Principles).", - '> **Section structure:** H2 sections by cadence, each suffixed "(newest first)".', - "> **Convention:** append newest first; never overwrite dated entries; ISO dates only.", + "> **Contains:** Active commitments, upcoming plans, recurring rhythms, and recent-past events kept for context — the time-sensitive logistics of the user's current life. A **current-state snapshot**, not a history ledger.", + "> **Does NOT contain:** One-off events or reference material, identity facts (→ Me), principles (→ Principles), directives for AI agents (→ Agents).", + '> **Section structure:** Active commitments, Upcoming, Daily/weekly rhythm, Recent past — each suffixed "(newest first)".', + "> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active-commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned.", ].join("\n"), sections: [ - "Daily (newest first)", - "Weekly (newest first)", - "Commitments (newest first)", + "Active commitments (newest first)", + "Upcoming (newest first)", + "Daily/weekly rhythm (newest first)", + "Recent past (newest first)", + ], + }, + { + fileName: "Agents", + title: "Agents", + tag: "agents", + entryPolicy: "append-only", + related: ["Me", "Principles", "Opinions", "Routines"], + scope: [ + "> [!info] Scope of this file", + "> **Contains:** Directives for AI agents working with the user — how to communicate, how to run work, and how to verify and scope changes. The subject of every entry is *agent behavior*; if an entry states a fact about the user, it belongs in another memory file.", + "> **Does NOT contain:** Identity facts (→ Me), the user's values (→ Principles), their evolving stances on tools (→ Opinions), current-life logistics (→ Routines).", + '> **Section structure:** Communication, Working style, Verification & scope — each suffixed "(newest first)".', + "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", + ].join("\n"), + sections: [ + "Communication (newest first)", + "Working style (newest first)", + "Verification & scope (newest first)", ], }, ] @@ -289,6 +331,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { `title: ${spec.title}`, `created: ${created}`, "type: profile", + `entry-policy: ${spec.entryPolicy}`, "tags:", " - memory", ` - ${spec.tag}`, @@ -335,6 +378,9 @@ export const createMemoryStore = (options: { memoryDir: string }) => { const frontmatter = { title: params.fileName, type: "profile", + // Programmatically-created files get the safe default; a user opts a + // file into "living" by editing the property deliberately. + "entry-policy": "append-only", tags: ["memory", toKebabCase(params.fileName)], created: DateTime.now().toISO(), } @@ -639,6 +685,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { file: name, title, bytes, + entry_policy: entryPolicyFromFrontmatter(parsed.data["entry-policy"]), leading_callout: leadingCallout, headings, } diff --git a/templates/memory/Agents.md b/templates/memory/Agents.md new file mode 100644 index 000000000..e9dc9fa46 --- /dev/null +++ b/templates/memory/Agents.md @@ -0,0 +1,28 @@ +--- +title: Agents +created: 2025-01-01T00:00:00-05:00 # replace with your current timestamp +type: profile +entry-policy: append-only +tags: + - memory + - agents +related: + - "[[About Me/Me]]" + - "[[About Me/Principles]]" + - "[[About Me/Opinions]]" + - "[[About Me/Routines]]" +--- + +# Agents + +> [!info] Scope of this file +> **Contains:** Directives for AI agents working with the user — how to communicate, how to run work, and how to verify and scope changes. The subject of every entry is _agent behavior_; if an entry states a fact about the user, it belongs in another memory file. +> **Does NOT contain:** Identity facts (→ Me), the user's values (→ Principles), their evolving stances on tools (→ Opinions), current-life logistics (→ Routines). +> **Section structure:** Communication, Working style, Verification & scope — each suffixed "(newest first)". +> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). + +## Communication (newest first) + +## Working style (newest first) + +## Verification & scope (newest first) diff --git a/templates/memory/Me.md b/templates/memory/Me.md index 69afbc013..ba2981f50 100644 --- a/templates/memory/Me.md +++ b/templates/memory/Me.md @@ -2,6 +2,7 @@ title: Me created: 2025-01-01T00:00:00-05:00 # replace with your current timestamp type: profile +entry-policy: append-only tags: - memory - identity @@ -9,15 +10,16 @@ related: - "[[About Me/Opinions]]" - "[[About Me/Principles]]" - "[[About Me/Routines]]" + - "[[About Me/Agents]]" --- # Me > [!info] Scope of this file > **Contains:** Identity, interests, and durable context about the user — who they are, what they're into, situational facts. -> **Does NOT contain:** Opinions or preferences (→ Opinions), guiding principles (→ Principles), recurring routines (→ Routines). -> **Section structure:** H2 sections grouped by theme, each suffixed "(newest first)". -> **Convention:** append newest first; never overwrite dated entries; ISO dates only. +> **Does NOT contain:** Opinions or preferences (→ Opinions), guiding principles (→ Principles), recurring routines (→ Routines), directives for AI agents (→ Agents). +> **Section structure:** Identity, Interests, Context — each suffixed "(newest first)". +> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Identity (newest first) diff --git a/templates/memory/Opinions.md b/templates/memory/Opinions.md index 9e98bc39f..0026b5f4f 100644 --- a/templates/memory/Opinions.md +++ b/templates/memory/Opinions.md @@ -2,21 +2,23 @@ title: Opinions created: 2025-01-01T00:00:00-05:00 # replace with your current timestamp type: profile +entry-policy: append-only tags: - memory - opinions related: - "[[About Me/Principles]]" - "[[About Me/Me]]" + - "[[About Me/Agents]]" --- # Opinions > [!info] Scope of this file > **Contains:** Evolving views on tools, patterns, methods, and processes — stances that may shift over time. -> **Does NOT contain:** Stable values or decision heuristics (→ Principles), identity or interests (→ Me). -> **Section structure:** H2 sections by topic, each suffixed "(newest first)". -> **Convention:** append newest first; never overwrite dated entries; ISO dates only. +> **Does NOT contain:** Stable values or decision heuristics (→ Principles), identity or interests (→ Me), directives for AI agents (→ Agents). +> **Section structure:** Tools and workflows, Code patterns, Communication preferences — each suffixed "(newest first)". +> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Tools and workflows (newest first) diff --git a/templates/memory/Principles.md b/templates/memory/Principles.md index aa58c31ca..c998cf8d6 100644 --- a/templates/memory/Principles.md +++ b/templates/memory/Principles.md @@ -2,21 +2,23 @@ title: Principles created: 2025-01-01T00:00:00-05:00 # replace with your current timestamp type: profile +entry-policy: append-only tags: - memory - principles related: - "[[About Me/Opinions]]" - "[[About Me/Me]]" + - "[[About Me/Agents]]" --- # Principles > [!info] Scope of this file > **Contains:** Stable values, decision heuristics, and non-negotiables — how the user thinks and what they hold firm. -> **Does NOT contain:** Evolving opinions on tools or methods (→ Opinions), identity facts (→ Me). -> **Section structure:** H2 sections by theme, each suffixed "(newest first)". -> **Convention:** append newest first; never overwrite dated entries; ISO dates only. +> **Does NOT contain:** Evolving opinions on tools or methods (→ Opinions), identity facts (→ Me), directives for AI agents (→ Agents). +> **Section structure:** Decision heuristics, Working style, Non-negotiables — each suffixed "(newest first)". +> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Decision heuristics (newest first) diff --git a/templates/memory/README.md b/templates/memory/README.md index 96c41db2d..f00797df2 100644 --- a/templates/memory/README.md +++ b/templates/memory/README.md @@ -15,6 +15,7 @@ templates: cp templates/memory/Me.md ~/your-vault/About\ Me/ cp templates/memory/Principles.md ~/your-vault/About\ Me/ cp templates/memory/Opinions.md ~/your-vault/About\ Me/ +cp templates/memory/Agents.md ~/your-vault/About\ Me/ cp templates/memory/Routines.md ~/your-vault/About\ Me/ # optional ``` @@ -38,6 +39,46 @@ Memory files use a specific format that Vault Cortex tools understand: `vault_patch_note` (operation `prepend`, no heading). - **H2 headings**: sections (topics, categories) - **Dated bullets**: entries under each section, in `- **YYYY-MM-DD**: text` format +- **`entry-policy` frontmatter** (recommended): how the file's entries may be + maintained — see [Entry policy](#entry-policy) below + +## Which file gets an entry? + +One split matters more than the others: **facts about the user vs directives +for agents**. The test — _who is the subject of the entry?_ + +- An imperative addressed to agents ("verify before claiming success", + "answer every question explicitly") → **Agents.md** +- A fact or preference about the user ("prefers written docs over video", + "meeting-free Wednesdays") → **Me / Principles / Opinions / Routines**, + per each file's scope callout + +Mixing the two is the most common drift in a lived-in memory layer: directives +accumulate inside Principles and Me because they _feel_ like values. Route them +to Agents.md from the start — it doubles as the highest-value always-read for +any agent session. + +## Entry policy + +Memory files are **append-only by default**: entries are never edited or +deleted, and corrections arrive as new dated entries. A file can opt out by +declaring the policy in frontmatter: + +```yaml +entry-policy: append-only # the default — may be omitted +entry-policy: living # current-state snapshot — expired entries are pruned +``` + +`living` is for files that describe _what's current_ rather than _what has been +true_ — the Routines template ships this way. When an entry there expires (the +date passes, the commitment ends), delete it and, if the outcome is worth +keeping, append it to a history section (Recent past). Without pruning, a +current-state file accumulates expired plans that mislead every agent reading +it. + +`vault_list_memory_files` surfaces each file's policy as `entry_policy` +(defaulting to `append-only` when the property is absent), so agents can check +it before pruning anything. The `vault_update_memory` tool appends dated entries automatically. The `vault_get_memory` tool reads them back, either a full file, a single section, or all files concatenated. @@ -67,7 +108,10 @@ convention: semantic/vector search, enabling temporal queries ("how has the user's stance on X evolved?", "what did they believe about Y last month?") -This is append-only by design. Entries are never overwritten — new entries are -added at the top (newest first), and the full history is preserved. Agents -retrieve context by reading the most recent entries, but the timeline remains -available for deeper understanding. +This is append-only by design (for files with the default `entry-policy`). +Entries are never overwritten — new entries are added at the top (newest +first), and the full history is preserved. Agents retrieve context by reading +the most recent entries, but the timeline remains available for deeper +understanding. Files marked `entry-policy: living` trade the complete timeline +for an accurate current state — their dated entries record when something was +captured, not a full history. diff --git a/templates/memory/Routines.md b/templates/memory/Routines.md index 7fb7d5353..64a8c5cda 100644 --- a/templates/memory/Routines.md +++ b/templates/memory/Routines.md @@ -2,23 +2,27 @@ title: Routines created: 2025-01-01T00:00:00-05:00 # replace with your current timestamp type: profile +entry-policy: living tags: - memory - routines related: - "[[About Me/Me]]" + - "[[About Me/Agents]]" --- # Routines > [!info] Scope of this file -> **Contains:** Recurring routines, cadences, and practiced habits — what the user actually does on a regular rhythm. -> **Does NOT contain:** One-off events or plans, identity facts (→ Me), principles (→ Principles). -> **Section structure:** H2 sections by cadence, each suffixed "(newest first)". -> **Convention:** append newest first; never overwrite dated entries; ISO dates only. +> **Contains:** Active commitments, upcoming plans, recurring rhythms, and recent-past events kept for context — the time-sensitive logistics of the user's current life. A **current-state snapshot**, not a history ledger. +> **Does NOT contain:** One-off events or reference material, identity facts (→ Me), principles (→ Principles), directives for AI agents (→ Agents). +> **Section structure:** Active commitments, Upcoming, Daily/weekly rhythm, Recent past — each suffixed "(newest first)". +> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active-commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned. -## Daily (newest first) +## Active commitments (newest first) -## Weekly (newest first) +## Upcoming (newest first) -## Commitments (newest first) +## Daily/weekly rhythm (newest first) + +## Recent past (newest first) From 3feabb0a8c3685d960cd78dd2c146513d2fa1989 Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:40:01 -0400 Subject: [PATCH 2/4] fix: address bot review findings on entry-policy PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lower SHRINK_FLOOR_BYTES from 1300 to 1250 so single-entry Routines files (~1270 B) are guarded by the catastrophic-shrink check - Add typeof guard to entryPolicyFromFrontmatter for defense-in-depth - Fix memory-review prompt to mention vault_delete_memory alongside vault_update_memory (step 6 introduces deletion but the final instruction only mentioned updates) - Fix "Active-commitments" → "Active commitments" hyphenation to match section heading (template + MEMORY_TEMPLATE_SPECS) - Fix "highest-value always-read" → "always-read file" (missing noun) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../mcp-core/__tests__/memory-review-prompt.test.ts | 2 +- .../mcp-core/prompts/memory-review-prompt.ts | 2 +- src/vault-mcp/vault-operations/memory-store.ts | 12 ++++++------ templates/memory/README.md | 4 ++-- templates/memory/Routines.md | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts b/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts index 4b0d97a58..ec6e6142c 100644 --- a/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/memory-review-prompt.test.ts @@ -374,7 +374,7 @@ describe("memory-review full prompt output", () => { "5. **Coverage analysis.** What areas of the user's life, work, or preferences are NOT yet represented? Use the file scopes and section names above to identify gaps worth filling.", "6. **Expired current-state entries (living files only).** A file marked `living` in the Structure section is a current-state snapshot, not a history ledger — flag entries whose date or commitment has passed and propose pruning them (vault_delete_memory), with the outcome appended to a history section when worth keeping. Never propose this for append-only files.", "", - "Propose every change as an explicit vault_update_memory call (newest-first; the server stamps the date) and **confirm with me before writing anything**. Never delete an entry just for being old from an append-only file.", + "Propose updates as explicit vault_update_memory calls and deletions as explicit vault_delete_memory calls; for living-file pruning, append any worthwhile outcome to the appropriate history section first. The server stamps update dates. **Confirm with me before writing or deleting anything**. Never delete an entry just for being old from an append-only file.", ].join("\n"), ) }) diff --git a/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts b/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts index f23d9af51..699226c0b 100644 --- a/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/memory-review-prompt.ts @@ -205,7 +205,7 @@ export const registerMemoryReviewPrompt = ({ "5. **Coverage analysis.** What areas of the user's life, work, or preferences are NOT yet represented? Use the file scopes and section names above to identify gaps worth filling.", "6. **Expired current-state entries (living files only).** A file marked `living` in the Structure section is a current-state snapshot, not a history ledger — flag entries whose date or commitment has passed and propose pruning them (vault_delete_memory), with the outcome appended to a history section when worth keeping. Never propose this for append-only files.", "", - "Propose every change as an explicit vault_update_memory call (newest-first; the server stamps the date) and **confirm with me before writing anything**. Never delete an entry just for being old from an append-only file.", + "Propose updates as explicit vault_update_memory calls and deletions as explicit vault_delete_memory calls; for living-file pruning, append any worthwhile outcome to the appropriate history section first. The server stamps update dates. **Confirm with me before writing or deleting anything**. Never delete an entry just for being old from an append-only file.", ].join("\n") reqLogger.info("prompt_result", { outcome: "ok", diff --git a/src/vault-mcp/vault-operations/memory-store.ts b/src/vault-mcp/vault-operations/memory-store.ts index 9815b0e68..d325c84ee 100644 --- a/src/vault-mcp/vault-operations/memory-store.ts +++ b/src/vault-mcp/vault-operations/memory-store.ts @@ -18,12 +18,12 @@ import type { Logger } from "../../logger.js" // Refuse a memory write that would remove more than half of an existing file's // bytes — a catastrophic shrink almost always means the on-disk copy diverged // (e.g. a skeleton template clobbering real content) rather than a legitimate -// single-entry edit. The 1300-byte floor sits just above the largest empty +// single-entry edit. The 1250-byte floor sits just above the largest empty // memory template (Routines 1228 B; Agents 1081 B; Me/Opinions/Principles // ~900 B — frontmatter + scope callout + headings, no entries), so a file with -// no real content is never guarded, while a file that has accumulated real -// entries beyond the skeleton is. -const SHRINK_FLOOR_BYTES = 1300 +// no real content is never guarded, while a file with even one dated entry +// (~1270 B+) is. +const SHRINK_FLOOR_BYTES = 1250 const SHRINK_RATIO = 0.5 const guardAgainstShrink = ( beforeBytes: number, @@ -100,7 +100,7 @@ export type MemoryEntryPolicy = "append-only" | "living" * as the append-only default — the safe reading, since append-only forbids * destructive maintenance. */ const entryPolicyFromFrontmatter = (value: unknown): MemoryEntryPolicy => - value === "living" ? "living" : "append-only" + typeof value === "string" && value === "living" ? "living" : "append-only" export type MemoryFileOutline = Readonly<{ file: string @@ -289,7 +289,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { "> **Contains:** Active commitments, upcoming plans, recurring rhythms, and recent-past events kept for context — the time-sensitive logistics of the user's current life. A **current-state snapshot**, not a history ledger.", "> **Does NOT contain:** One-off events or reference material, identity facts (→ Me), principles (→ Principles), directives for AI agents (→ Agents).", '> **Section structure:** Active commitments, Upcoming, Daily/weekly rhythm, Recent past — each suffixed "(newest first)".', - "> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active-commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned.", + "> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned.", ].join("\n"), sections: [ "Active commitments (newest first)", diff --git a/templates/memory/README.md b/templates/memory/README.md index f00797df2..7a068599a 100644 --- a/templates/memory/README.md +++ b/templates/memory/README.md @@ -55,8 +55,8 @@ for agents**. The test — _who is the subject of the entry?_ Mixing the two is the most common drift in a lived-in memory layer: directives accumulate inside Principles and Me because they _feel_ like values. Route them -to Agents.md from the start — it doubles as the highest-value always-read for -any agent session. +to Agents.md from the start — it doubles as the highest-value always-read file +for any agent session. ## Entry policy diff --git a/templates/memory/Routines.md b/templates/memory/Routines.md index 64a8c5cda..153261329 100644 --- a/templates/memory/Routines.md +++ b/templates/memory/Routines.md @@ -17,7 +17,7 @@ related: > **Contains:** Active commitments, upcoming plans, recurring rhythms, and recent-past events kept for context — the time-sensitive logistics of the user's current life. A **current-state snapshot**, not a history ledger. > **Does NOT contain:** One-off events or reference material, identity facts (→ Me), principles (→ Principles), directives for AI agents (→ Agents). > **Section structure:** Active commitments, Upcoming, Daily/weekly rhythm, Recent past — each suffixed "(newest first)". -> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active-commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned. +> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned. ## Active commitments (newest first) From d0b1a8ffb9c1186242ae7848104588bb836f48ab Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:03:38 -0400 Subject: [PATCH 3/4] fix(memory): remove redundant "not an error" bullet from vault_update_memory Errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The duplicate no-op contract is already documented in the opening paragraph ("Idempotent — an exact duplicate is a no-op"), the Returns section, and the idempotentHint annotation. The Errors bullet was triple-redundancy placed in a misleading section — a "not an error" note inside "Errors:" contradicts its own framing and contributes to the Conciseness (Co=4) TDQS ding. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/vault-mcp/mcp-core/tools/memory-tools.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vault-mcp/mcp-core/tools/memory-tools.ts b/src/vault-mcp/mcp-core/tools/memory-tools.ts index d0b50b23c..69677de56 100644 --- a/src/vault-mcp/mcp-core/tools/memory-tools.ts +++ b/src/vault-mcp/mcp-core/tools/memory-tools.ts @@ -112,7 +112,6 @@ Errors: - "entry must be a single line" — memory entries are single dated bullets; collapse newlines or append multiple entries. - "section must be a single line" — section names become H2 headings; remove line breaks. - "date must be a real ISO calendar date" — options.date only accepts an existing calendar date in bare YYYY-MM-DD form (e.g. "2026-07-02"), not a timestamp. -- An exact duplicate entry is not an error — the call succeeds and reports that the entry already exists, without writing. Returns: Confirmation message (notes when an identical entry already existed and nothing was written).`, inputSchema: { From 70d5db51efc00319c50d7ddd964126c6c0b145ef Mon Sep 17 00:00:00 2001 From: Tanisha Aberdeen <32620895+aliasunder@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:11:03 -0400 Subject: [PATCH 4/4] fix(memory): restore generic Section structure lines in scope callouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PR replaced the generic "H2 sections by theme/topic/cadence" lines with specific heading names (e.g. "Identity, Interests, Context"). These will go stale as agents add new H2 sections — vault_list_memory_files already surfaces the live heading structure, so the scope callout should describe the pattern, not enumerate the headings. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/vault-mcp/vault-operations/memory-store.ts | 10 +++++----- templates/memory/Agents.md | 2 +- templates/memory/Me.md | 2 +- templates/memory/Opinions.md | 2 +- templates/memory/Principles.md | 2 +- templates/memory/Routines.md | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vault-mcp/vault-operations/memory-store.ts b/src/vault-mcp/vault-operations/memory-store.ts index d325c84ee..6a8e4a1cc 100644 --- a/src/vault-mcp/vault-operations/memory-store.ts +++ b/src/vault-mcp/vault-operations/memory-store.ts @@ -231,7 +231,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { "> [!info] Scope of this file", "> **Contains:** Identity, interests, and durable context about the user — who they are, what they're into, situational facts.", "> **Does NOT contain:** Opinions or preferences (→ Opinions), guiding principles (→ Principles), recurring routines (→ Routines), directives for AI agents (→ Agents).", - '> **Section structure:** Identity, Interests, Context — each suffixed "(newest first)".', + '> **Section structure:** H2 sections grouped by theme, each suffixed "(newest first)".', "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ @@ -250,7 +250,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { "> [!info] Scope of this file", "> **Contains:** Evolving views on tools, patterns, methods, and processes — stances that may shift over time.", "> **Does NOT contain:** Stable values or decision heuristics (→ Principles), identity or interests (→ Me), directives for AI agents (→ Agents).", - '> **Section structure:** Tools and workflows, Code patterns, Communication preferences — each suffixed "(newest first)".', + '> **Section structure:** H2 sections by topic, each suffixed "(newest first)".', "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ @@ -269,7 +269,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { "> [!info] Scope of this file", "> **Contains:** Stable values, decision heuristics, and non-negotiables — how the user thinks and what they hold firm.", "> **Does NOT contain:** Evolving opinions on tools or methods (→ Opinions), identity facts (→ Me), directives for AI agents (→ Agents).", - '> **Section structure:** Decision heuristics, Working style, Non-negotiables — each suffixed "(newest first)".', + '> **Section structure:** H2 sections by theme, each suffixed "(newest first)".', "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ @@ -288,7 +288,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { "> [!info] Scope of this file", "> **Contains:** Active commitments, upcoming plans, recurring rhythms, and recent-past events kept for context — the time-sensitive logistics of the user's current life. A **current-state snapshot**, not a history ledger.", "> **Does NOT contain:** One-off events or reference material, identity facts (→ Me), principles (→ Principles), directives for AI agents (→ Agents).", - '> **Section structure:** Active commitments, Upcoming, Daily/weekly rhythm, Recent past — each suffixed "(newest first)".', + '> **Section structure:** H2 sections by cadence, each suffixed "(newest first)".', "> **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned.", ].join("\n"), sections: [ @@ -308,7 +308,7 @@ export const createMemoryStore = (options: { memoryDir: string }) => { "> [!info] Scope of this file", "> **Contains:** Directives for AI agents working with the user — how to communicate, how to run work, and how to verify and scope changes. The subject of every entry is *agent behavior*; if an entry states a fact about the user, it belongs in another memory file.", "> **Does NOT contain:** Identity facts (→ Me), the user's values (→ Principles), their evolving stances on tools (→ Opinions), current-life logistics (→ Routines).", - '> **Section structure:** Communication, Working style, Verification & scope — each suffixed "(newest first)".', + '> **Section structure:** H2 sections by domain, each suffixed "(newest first)".', "> **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter).", ].join("\n"), sections: [ diff --git a/templates/memory/Agents.md b/templates/memory/Agents.md index e9dc9fa46..4cf222e49 100644 --- a/templates/memory/Agents.md +++ b/templates/memory/Agents.md @@ -18,7 +18,7 @@ related: > [!info] Scope of this file > **Contains:** Directives for AI agents working with the user — how to communicate, how to run work, and how to verify and scope changes. The subject of every entry is _agent behavior_; if an entry states a fact about the user, it belongs in another memory file. > **Does NOT contain:** Identity facts (→ Me), the user's values (→ Principles), their evolving stances on tools (→ Opinions), current-life logistics (→ Routines). -> **Section structure:** Communication, Working style, Verification & scope — each suffixed "(newest first)". +> **Section structure:** H2 sections by domain, each suffixed "(newest first)". > **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Communication (newest first) diff --git a/templates/memory/Me.md b/templates/memory/Me.md index ba2981f50..0522cadfd 100644 --- a/templates/memory/Me.md +++ b/templates/memory/Me.md @@ -18,7 +18,7 @@ related: > [!info] Scope of this file > **Contains:** Identity, interests, and durable context about the user — who they are, what they're into, situational facts. > **Does NOT contain:** Opinions or preferences (→ Opinions), guiding principles (→ Principles), recurring routines (→ Routines), directives for AI agents (→ Agents). -> **Section structure:** Identity, Interests, Context — each suffixed "(newest first)". +> **Section structure:** H2 sections grouped by theme, each suffixed "(newest first)". > **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Identity (newest first) diff --git a/templates/memory/Opinions.md b/templates/memory/Opinions.md index 0026b5f4f..86d816661 100644 --- a/templates/memory/Opinions.md +++ b/templates/memory/Opinions.md @@ -17,7 +17,7 @@ related: > [!info] Scope of this file > **Contains:** Evolving views on tools, patterns, methods, and processes — stances that may shift over time. > **Does NOT contain:** Stable values or decision heuristics (→ Principles), identity or interests (→ Me), directives for AI agents (→ Agents). -> **Section structure:** Tools and workflows, Code patterns, Communication preferences — each suffixed "(newest first)". +> **Section structure:** H2 sections by topic, each suffixed "(newest first)". > **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Tools and workflows (newest first) diff --git a/templates/memory/Principles.md b/templates/memory/Principles.md index c998cf8d6..04fd0fbf6 100644 --- a/templates/memory/Principles.md +++ b/templates/memory/Principles.md @@ -17,7 +17,7 @@ related: > [!info] Scope of this file > **Contains:** Stable values, decision heuristics, and non-negotiables — how the user thinks and what they hold firm. > **Does NOT contain:** Evolving opinions on tools or methods (→ Opinions), identity facts (→ Me), directives for AI agents (→ Agents). -> **Section structure:** Decision heuristics, Working style, Non-negotiables — each suffixed "(newest first)". +> **Section structure:** H2 sections by theme, each suffixed "(newest first)". > **Convention:** append newest first; never overwrite dated entries; ISO dates only. Entry policy: append-only (declared in frontmatter). ## Decision heuristics (newest first) diff --git a/templates/memory/Routines.md b/templates/memory/Routines.md index 153261329..444189274 100644 --- a/templates/memory/Routines.md +++ b/templates/memory/Routines.md @@ -16,7 +16,7 @@ related: > [!info] Scope of this file > **Contains:** Active commitments, upcoming plans, recurring rhythms, and recent-past events kept for context — the time-sensitive logistics of the user's current life. A **current-state snapshot**, not a history ledger. > **Does NOT contain:** One-off events or reference material, identity facts (→ Me), principles (→ Principles), directives for AI agents (→ Agents). -> **Section structure:** Active commitments, Upcoming, Daily/weekly rhythm, Recent past — each suffixed "(newest first)". +> **Section structure:** H2 sections by cadence, each suffixed "(newest first)". > **Convention:** append newest first; ISO dates only. Entry policy: **living** (declared in frontmatter) — a deliberate exception to the memory layer's append-only default. When an Upcoming or Active commitments entry expires, delete it and, if the outcome is worth keeping, append it to Recent past. Recent past entries are dated history and are not pruned. ## Active commitments (newest first)