L2 v0.8.0 source

memory-save

Proje başına bir MEMORY.md dosyasını, stop'u engellemeden güncel tutan ve session'a yükleyen bir Claude Code Mod'u. Her ana döngü turundan sonra session'ın tool'suz bir fork'una projenin neyi hatırlaması gerektiğini sorar ve cevabı kendisi yazar. Ana konuşma bir memory edit'i hiç görmez: engellenen bir stop yok, MEMORY.md üzerinde bir Read ya da Edit yok, fazladan bir tur yok.

Ne yapar

Memory'yi yükler

Başlangıçta, resume'da, /clear ve compaction'da classic.SessionStart hook'u tek bir context bloğu ekler:

[PROJECT MEMORY: <project>]
Answer in the language of the user's own messages, in every reply and every progress line, also at the end of a long turn whose context (tool output, docs, this memory) is in another language. Keep technical terms and identifiers as they are.

<bütün MEMORY.md>

Topic files in ~/.cli-tweaks/memory/<project>: history.md

Dil satırı orada, çünkü context'i çoğunlukla İngilizce olan uzun turlar başka bir dildeki prompt'lara İngilizce cevaplarla bitiyordu (ölçüldü). Topic satırı yalnız topic dosyaları varken bulunur. MEMORY.md olmayan bir proje hiçbir blok almaz. Blok, dosyayı yazma talimatı taşımaz, çünkü dosyayı mod yazar. Memory bu olaylar arasında tekrarlanmaz, yani context'i tur tur büyütmez.

Memory'yi kaydeder

Bir cevapla ya da bir kesintiyle biten her ana döngü turundan sonra:

  1. ~/.cli-tweaks/memory/<project>/MEMORY.md dosyasını, varsa, okur.
  2. $.model.fork'a tek bir mesaj gönderir. Fork bütün session transcript'ini görür ve prompt cache'ini paylaşır, ama tool'u yoktur. Mesaj mevcut dosyayı, yazma kurallarını ve cevap biçimini taşır. Yazma kuralları, template ve MIGRATION, OFFLOAD ve BULLET SPLIT notları klasik memory-save Stop hook'unun metinleridir, kelimesi kelimesine; yalnız durdurmaya dair kısımlar dışarıda bırakılır, çünkü fork durdurmaz.
  3. Fork JSON ile cevap verir: eklenecek, kaldırılacak ya da değiştirilecek madde'ler ve history.md gibi topic dosyalarına eklenecek metin.
  4. Mod cevabı uygular, sonucu kontrol eder ve dosyaları yazar.

Kayıt arka planda çalışır. Fork çalışırken sonraki prompt beklemez. Aynı anda bir kayıt çalışır; bir kayıt sırasında biten bir tur, ondan sonra bir kayıt daha ister.

Proje adı, bir git worktree içinde de birincil repository adıdır; yoksa git top level, yoksa çalışma dizinidir.

Ne gösterir

Prompt'un altında bir status line, bir kayıt çalışırken ve sonrasında:

memory-save: saving… · 14:31
memory-save: +2 -1 · 14:32
memory-save: +3 -1 ~3 topic: history · 14:32
memory-save: no change · 14:32
memory-save: +2 1 refused · 14:32
memory-save: error: reply has no JSON object · 14:32

sidebar açıkken bu durum oraya gider, session boyunca duran ve her kayıtta yeniden yazılan bir MEMORY.md section'ı olarak, ve status line boş kalır. Orada satır renklidir: yazılmış bir kayıt yeşil, bir kısmı atlayan ya da reddeden bir kayıt sarı, bir hata kırmızı, saving… ya da no change soluk. Sidebar kapalıyken ya da o mod kurulu değilken status line yukarıdaki gibi, engine'in kendi renginde çizilir.

Section, durumun altında ikinci, soluk bir satır taşır: son transcript satırı, başındaki MEMORY.md: olmadan. Durum kaydın nerede olduğunu söyler, ikinci satır ne yaptığını:

MEMORY.md
topic: history · 14:32
topic files only; appended to history.md

Transcript'te bir satır, bir kayıt bir dosyayı değiştirdiğinde. Bu satır modele gönderilmez:

memory-save: MEMORY.md: 2 added, 1 removed; appended to history.md

Dosya biçimi

MEMORY.md tam olarak şu section'ları, bu sırada taşır: ## CRITICAL RULES, ## Architecture & Config Facts, ## Active Warnings, ## Topic Files. Yeni bir dosya bu iskeletten başlar.

Template hiçbir zaman bozulmaz. Her yüklemede (başlangıç, resume, /clear, compaction) ve her kayıttan önce, tam olarak bu dört section'ı bu sırada taşımayan bir dosya, fork olmadan mod tarafından template'e konur: section'lar sıraya girer, tekrarlanan bir section tek bir section'da birleşir, eksik bir section - None yet. olarak eklenir ve diğer her ## section'ı ## Architecture & Config Facts sonuna bir ### Unsorted: <heading> satırı altında taşınır. Hiçbir satır düşmez. Sonraki kayıt fork'a, sıralanmamış her maddeyi ait olduğu section'a ya da history'yi bir topic dosyasına taşımasını ve boşaldığında ### Unsorted: satırını kaldırmasını söyler. Eski dosya yanında MEMORY.pre-migration.md olarak tutulur (sonraki bir onarım o kopyayı değiştirir) ve transcript'te bir satır bunu söyler:

memory-save: MEMORY.md: put into the four sections (old copy: MEMORY.pre-migration.md)

Sonucu template'te olmayan bir kayıt hiçbir zaman yazılmaz.

Bir yazma öncesinde ne kontrol edilir

  • Cevap tek bir JSON object'idir. Parser'ın okuyamadığı bir cevap sonraki tura bırakılır: hiçbir şey yazılmaz, cevap kanıt olarak tutulur ve bir transcript satırı bunu status line olmadan söyler. Başka biçimde bir op ya da topic tek başına reddedilir, cevabın geri kalanı yazılır, status line onu sayar (+2 1 refused), transcript satırı adlandırır ve sonraki kayıt fork'a neyi reddettiğini söyler. Tek bir bozuk op artık bütün kaydı kaybetmez.
  • Bir add, dosyanın zaten taşıdığı bir heading'i adlandırır: dört section'dan biri ya da onun bir ### alt başlığı, büyük küçük harf fark etmez. Bir madde o heading'in kendi bloğunun sonuna gider, yani bir section'a yapılan ekleme ilk alt başlığından önce iner. Heading'i dosyada olmayan bir ekleme tek başına reddedilir.
  • Kaldırılan ya da değiştirilen bir satır dosyada tam olarak bulunur, ya da yalnız baştaki bir liste işaretiyle tek bir satırdan ayrılır. Fork her girdiyi madde olarak yazar, dosyada düz bir paragraf satırı olarak duranı da. Satırı dosyada olmayan bir remove ya da replace atlanır ve diğer op'lar yazılır: status line onu sayar (+1 1 skipped), transcript satırı adlandırır ve sonraki kayıt fork'a böyle bir satırı markup'ı ile birlikte tam kopyalamasını söyler. Yanlış alıntılanmış bir satır (fork birinin etrafına ** eklemişse mesela) eskiden bütün kaydı durduruyordu.
  • Bir topic dosya adı küçük harftir, .md ile biter, dizin kısmı taşımaz ve memory.md değildir.
  • Sonuç dört section'ı sırada, 200 satırdan az ve 50000 karakterden az taşır. Zaten bir sınırda ya da üstünde olan bir dosya (bu kontrollerden önce yazılmış biri mesela) istisnadır: onu iki ölçüde de küçülten bir kayıt yazılır, böylece dosya her kaydın başarısız olması yerine adım adım sınırların altına iner. Bir sınırı aşan bir sonuç da kaybolmaz: eklemeler ve topic append'leri düşer, yalnız kaldırmalar yazılır ve düşen kısım reddedilmiş sayılır. Başlığı ya da dört ## section satırından birini alacak bir remove ya da replace tek başına reddedilir, böylece template bozulamaz; bir ### alt başlığı yine gidebilir.
  • 160 satırdan ya da 42000 karakterden itibaren fork'a bu kaydın kaç satır ve karakter kaldırması gerektiği söylenir. Bir sınırın üstünde not, yalnız küçültme kaydına döner: yeni madde ekleme, yalnız girdileri bir topic dosyasına taşı.
  • Hiçbir yeni madde 600 karakterden uzun değildir. Maddesi daha uzun olan bir add ya da replace tek başına reddedilir: diğer op'lar yazılır, status line onu sayar (+1 1 refused), transcript satırı adlandırır ve sonraki kayıt fork'a böyle bir maddeyi bölmesini ya da detayını bir topic dosyasına taşımasını söyler.
  • Fork çalışırken MEMORY.md değişmedi.

Hiçbir şey yazmayan ve status line'da bir hata gösteren iki durum kalır: fork hiç cevap vermedi (soğuk bir snapshot ya da bir API hatası) ve fork çalışırken MEMORY.md değişti.

Cevabın JSON object'i son } işaretinden geriye, adlandırılmış alanlardan oluşan ve parse edilen ilk { işaretine kadar okunur. JSON'dan önce bir cümle yazan bir cevap yine okunur, kendi parantezlerini taşıyan bir cümleli olanı da, mesela bir { tool: 'Edit' } matcher'ı.

Bu biçimde bir JSON object'i olmayan bir cevap memory dizinindeki memory-save.failed-reply.txt dosyasına yazılır ve bir transcript satırı sebebini ve output token sayısını adlandırır:

memory-save: MEMORY.md: this turn's reply was not read (reply is not valid JSON (JSON Parse error: Expected '}')); 1840 output tokens, kept in memory-save.failed-reply.txt

Böyle her cevap dosyayı değiştirir, yani dosya sonuncusunu tutar. Cevabın kesilip kesilmediğini ya da bozuk JSON taşıdığını görmek için onu okuyun. Fork cevabının stop reason'ı bir mod'a ulaşmaz, bu yüzden mod ikisini kendisi ayırt edemez.

Kurulum

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install memory-save@kilimcininkoroglu-mods

Function hook'lar early access. Flag olmadan hiçbir şey yüklenmez:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude

Tek bir session için yerel bir checkout'tan yükleyin:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir plugins/memory-save

Flag'i kalıcı yapmak için ~/.claude/settings.json dosyasına ekleyin:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

Kurulumdan sonra

  1. Modele MEMORY.md dosyasını edit etmesini söyleyen diğer her hook'u, CLAUDE.md satırını ya da skill'i kaldırın. Dosyayı mod yazar ve fork çalışırken bir model edit'i o kaydı bir hata ile durdurur.
  2. Elinizde olan bir memory dosyasını korumak için onu ~/.cli-tweaks/memory/<project>/MEMORY.md yoluna kopyalayın. Sonraki yüklemede mod onu dört section'a koyar ve eski kopyayı MEMORY.pre-migration.md olarak tutar. Dosyası olmayan bir proje ilk kaydından sonra bir tane alır; dizini mod oluşturur.
  3. Claude Code'u yeniden başlatın. Memory başlangıçta, resume'da, /clear ve compaction'da yüklenir.

Mod'un komutu yoktur. Kayıtları durdurmak için onu devre dışı bırakın: claude plugin disable memory-save@kilimcininkoroglu-mods.

Nereye uzanır

Claude Code 2.1.278 üzerinde claude plugin validate ile doğrulandı:

❯ ./register.ts hooks: session.start, classic.SessionStart, turn.complete
❯ ./register.ts calls: $.clock.now (via report), $.env.get (via locate), $.fs.exists (via readFile), $.fs.list (via memoryContext), $.fs.read (via readFile), $.fs.write (via ask, save, templated, writeTopics), $.model.fork (via ask), $.process.run (via git), $.sidebar.clear (via clearReport), $.sidebar.set (via report), $.ui.log (via ask, git, logEvent), $.ui.status (via clearReport, report)
❯ ./register.ts env writes: nothing
❯ ./register.ts env reads: HOME

Reach L2, dosya yazar, git çalıştırır ve Claude'u sürer.

1. Okur:     HOME; ~/.cli-tweaks/memory/<project>/ altındaki MEMORY.md, topic dosyalarını ve dizin listesini; fork üzerinden session transcript'ini
2. Çalıştırır: git rev-parse, session başına iki kere, projeyi adlandırmak için; ana döngü turu başına bir tool'suz $.model.fork
3. Gönderir: MEMORY.md dosyasını başlangıçta, resume'da, /clear ve compaction'da session context'i olarak; fork mesajını (yazma kuralları ve mevcut MEMORY.md) session'ın kendi API client'ına, session'ın transcript'i üzerine
4. Saklar:   ~/.cli-tweaks/memory/<project>/ altında MEMORY.md, MEMORY.pre-migration.md, topic dosyalarını ve okunamayan son fork cevabını (memory-save.failed-reply.txt)
5. Düşman girdi: fork'un cevabı güvenilmez metindir; yalnız belgelenmiş JSON biçimi uygulanır, topic dosya adları kontrol edilir ve sonuç bir yazmadan önce her kontrolü geçmek zorundadır

Sınırlar

  • Fork'un tool'u yoktur. Yalnız transcript'i ve mevcut dosyayı bilir.
  • Session ortasında yüklenen bir mod (/reload-plugins, bir enable) memory'yi sonraki /clear, compaction ya da session'da yükler.
  • claude plugin test test engine'i classic.SessionStart olayını raise edemez. Yükleme, metninin unit test'leri ve canlı bir session kontrolü ile kapsanır.
  • Her ana döngü turu bir fork'a mal olur: cache okuması olarak transcript, input olarak mevcut dosya ve kurallar, output olarak cevap.
  • Başarısız bir kayıt tekrar denenmez. Sonraki tur yeniden kaydeder.
  • Bir fork soğuk bir cache snapshot'ında ya da bir API hatasında boş dönebilir. Status line o zaman hatayı gösterir.

Geliştirme

make install     # eslint, typescript-eslint, typescript
make lint        # complexity limiti 10, üstünde build'i düşürür
make typecheck   # /plugin-types ile üretilen .claude/types/ gerekir
make validate
make test        # claude plugin test

memory-save

A Claude Code Mod that keeps a per-project MEMORY.md up to date without blocking the stop, and loads it into the session. After every main-loop turn it asks a tool-less fork of the session what the project must remember, and writes the answer itself. The main conversation never sees a memory edit: no blocked stop, no Read or Edit of MEMORY.md, no extra turn.

What it does

Loads the memory

At startup, resume, /clear and compaction, the classic.SessionStart hook adds one context block:

[PROJECT MEMORY: <project>]
Answer in the language of the user's own messages, in every reply and every progress line, also at the end of a long turn whose context (tool output, docs, this memory) is in another language. Keep technical terms and identifiers as they are.

<the whole MEMORY.md>

Topic files in ~/.cli-tweaks/memory/<project>: history.md

The language line is there because long turns whose context was mostly English ended with English replies to prompts in another language (measured). The topic line is present only when topic files exist. A project without MEMORY.md gets no block. The block holds no instruction to write the file, because the mod writes it. The memory is not repeated between these events, so it does not grow the context turn by turn.

Saves the memory

After every main-loop turn that ended with an answer or an interruption:

  1. It reads ~/.cli-tweaks/memory/<project>/MEMORY.md, when the file exists.
  2. It sends one message to $.model.fork. The fork sees the whole session transcript and shares its prompt cache, but it has no tools. The message carries the current file, the writing rules and the reply format. The writing rules, the template and the MIGRATION, OFFLOAD and BULLET SPLIT notes are the texts of the classic memory-save Stop hook, word for word; only the parts about stopping are left out, because the fork does not stop.
  3. The fork answers with JSON: bullets to add, remove or replace, and text to append to topic files such as history.md.
  4. The mod applies the answer, checks the result and writes the files.

The save runs in the background. The next prompt is not held while the fork runs. One save runs at a time; a turn that ends during a save asks for one more save after it.

The project name is the primary repository name, also inside a git worktree, else the git top level, else the working directory.

What it shows

A status line under the prompt while a save runs, and after it:

memory-save: saving… · 14:31
memory-save: +2 -1 · 14:32
memory-save: +3 -1 ~3 topic: history · 14:32
memory-save: no change · 14:32
memory-save: +2 1 refused · 14:32
memory-save: error: reply has no JSON object · 14:32

While the sidebar is open, that state goes there instead, as a MEMORY.md section that stays for the session and is rewritten at each save, and the status line stays clear. There the line is coloured: a written save green, a save that skipped or refused a part yellow, an error red, and saving… or no change faint. With the sidebar closed, or without that mod installed, the status line is drawn as above, in the engine's own colour.

The section holds a second, faint line under the state: the last transcript line, without the MEMORY.md: front. The state says where the save stands, the second line says what it did:

MEMORY.md
topic: history · 14:32
topic files only; appended to history.md

One line in the transcript when a save changed a file. The line is not sent to the model:

memory-save: MEMORY.md: 2 added, 1 removed; appended to history.md

The file format

MEMORY.md has exactly these sections, in this order: ## CRITICAL RULES, ## Architecture & Config Facts, ## Active Warnings, ## Topic Files. A new file starts from this skeleton.

The template is never broken. At every load (startup, resume, /clear, compaction) and before every save, a file without exactly these four sections in this order is put into the template by the mod itself, without the fork: the sections go in order, a repeated section is merged into one, a missing section is added as - None yet., and every other ## section moves to the end of ## Architecture & Config Facts under a ### Unsorted: <heading> line. No line is dropped. The next save tells the fork to move each unsorted bullet to the section it belongs in, or history to a topic file, and to remove the ### Unsorted: line once it is empty. The old file is kept as MEMORY.pre-migration.md next to it (a later repair replaces that copy), and one line in the transcript says so:

memory-save: MEMORY.md: put into the four sections (old copy: MEMORY.pre-migration.md)

A save whose result is not in the template is never written.

What is checked before a write

  • The reply is one JSON object. A reply the parser cannot read is left to the next turn: nothing is written, the reply is kept as evidence, and one transcript line says so without a status line. An op or a topic of another shape is refused alone, the rest of the reply is written, the status line counts it (+2 1 refused), the transcript line names it, and the next save tells the fork what it refused. One bad op no longer loses the whole save.
  • An add names a heading the file already has: one of the four sections, or a ### subheading of it, whatever its case. A bullet goes at the end of that heading's own block, so an add to a section lands before its first subheading. An add whose heading the file lacks is refused alone.
  • A removed or replaced line exists in the file exactly, or it differs only by a leading list marker from exactly one line. The fork writes every entry as a bullet, also one that stands in the file as a plain paragraph line. A remove or replace whose line the file does not have is skipped, and the other ops are written: the status line counts it (+1 1 skipped), the transcript line names it, and the next save tells the fork to copy such a line exactly, with its markup. A misquoted line (the fork added ** around one, for example) used to stop the whole save.
  • A topic file name is lowercase, ends in .md, has no directory part and is not memory.md.
  • The result has the four sections in order, fewer than 200 lines and fewer than 50000 characters. A file that is already at or over a cap (one written before these checks, for example) is the exception: a save that makes it smaller in both measures is written, so the file comes back under the caps in steps instead of every save failing. A result over a cap is not lost either: the adds and the topic appends are dropped, the removes alone are written, and the dropped part is counted as refused. A remove or replace that would take the title or one of the four ## section lines is refused alone, so the template cannot break; a ### subheading may still go.
  • From 160 lines or 42000 characters the fork is told how many lines and characters this save must remove. Over a cap the note becomes a shrink-only save: add no new bullet, only move entries to a topic file.
  • No new bullet is longer than 600 characters. An add or replace whose bullet is longer is refused alone: the other ops are written, the status line counts it (+1 1 refused), the transcript line names it, and the next save tells the fork to split such a bullet or move its detail to a topic file.
  • MEMORY.md did not change while the fork ran.

Two cases are left that write nothing and show an error on the status line: the fork gave no reply at all (a cold snapshot or an API error), and MEMORY.md changed while the fork ran.

The reply's JSON object is read from the last } back to the first { that opens an object of named fields and parses. A reply that writes a sentence before the JSON is still read, also one whose sentence holds braces of its own, for example a { tool: 'Edit' } matcher.

A reply that is not a JSON object of that shape is written to memory-save.failed-reply.txt in the memory directory, and one transcript line names its cause and its output tokens:

memory-save: MEMORY.md: this turn's reply was not read (reply is not valid JSON (JSON Parse error: Expected '}')); 1840 output tokens, kept in memory-save.failed-reply.txt

Each such reply replaces the file, so it holds the last one. Read it to see whether the reply was cut short or held broken JSON. The fork reply's stop reason is not available to a mod, so the mod cannot tell the two apart itself.

Install

claude plugin marketplace add KilimcininKorOglu/claude-code-mods
claude plugin install memory-save@kilimcininkoroglu-mods

Function hooks are early access. Nothing loads without the flag:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude

Load it from a local checkout for one session:

CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir plugins/memory-save

To keep the flag on, add this to ~/.claude/settings.json:

{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }

After installing

  1. Remove every other hook, CLAUDE.md line or skill that tells the model to edit MEMORY.md. The mod writes the file, and a model edit while the fork runs makes that save stop with an error.
  2. To keep a memory file you already have, copy it to ~/.cli-tweaks/memory/<project>/MEMORY.md. At the next load the mod puts it into the four sections and keeps the old copy as MEMORY.pre-migration.md. A project without the file gets one after its first save; the mod creates the directory.
  3. Restart Claude Code. The memory loads at startup, resume, /clear and compaction.

The mod has no command. To stop the saves, disable it: claude plugin disable memory-save@kilimcininkoroglu-mods.

What it can reach

Validated with claude plugin validate on Claude Code 2.1.278:

❯ ./register.ts hooks: session.start, classic.SessionStart, turn.complete
❯ ./register.ts calls: $.clock.now (via report), $.env.get (via locate), $.fs.exists (via readFile), $.fs.list (via memoryContext), $.fs.read (via readFile), $.fs.write (via ask, save, templated, writeTopics), $.model.fork (via ask), $.process.run (via git), $.sidebar.clear (via clearReport), $.sidebar.set (via report), $.ui.log (via ask, git, logEvent), $.ui.status (via clearReport, report)
❯ ./register.ts env writes: nothing
❯ ./register.ts env reads: HOME

Reach L2, writes files, runs git and drives Claude.

1. Reads:    HOME; MEMORY.md, its topic files and the directory listing under ~/.cli-tweaks/memory/<project>/; the session transcript, through the fork
2. Runs:     git rev-parse, twice per session, to name the project; one tool-less $.model.fork per main-loop turn
3. Sends:    MEMORY.md as session context at startup, resume, /clear and compaction; the fork message (the writing rules and the current MEMORY.md) to the session's own API client, on top of the session's transcript
4. Persists: MEMORY.md, MEMORY.pre-migration.md, topic files and the last unreadable fork reply (memory-save.failed-reply.txt) under ~/.cli-tweaks/memory/<project>/
5. Hostile input: the fork's reply is untrusted text; only the documented JSON shape is applied, topic file names are checked, and the result must pass every check before a write

Limits

  • The fork has no tools. It knows only the transcript and the current file.
  • A mod loaded in the middle of a session (/reload-plugins, an enable) loads the memory at the next /clear, compaction or session.
  • The test engine of claude plugin test cannot raise classic.SessionStart. The load is covered by unit tests of its text and by a live session check.
  • Every main-loop turn costs one fork: the transcript as cache reads, the current file and the rules as input, and the answer as output.
  • A save that fails is not retried. The next turn saves again.
  • A fork can come back empty on a cold cache snapshot or an API error. The status line then shows the error.

Development

make install     # eslint, typescript-eslint, typescript
make lint        # complexity limit 10, fails the build above it
make typecheck   # needs .claude/types/ from /plugin-types
make validate
make test        # claude plugin test