Repository navigation
Conversation
…onflict isolation (avenoxai#205) Add append-only offline mutation queue (state/mutation-queue.jsonl) with anonymous persistent machine identity, idempotent replay against local sync engine, and conflict isolation via .sync-conflict-*.md copies adhering to multi-device file sync semantics. Register mutation-queue CLI command and doctor health diagnosis. Fixes avenoxai#205
tungarix
left a comment
There was a problem hiding this comment.
Teşekkürler @bakiabaci, emek verilmiş ve testli bir çalışma. Ancak bu haliyle birleştirmeye hazır görmüyorum; aşağıdaki maddeler çözülmeden almayalım.
Birleştirmeyi engelleyenler
1. note_update notu sessizce eziyor (veri kaybı)
SyncEngine'de note_update yok, dolayısıyla her zaman else dalı çalışıyor. Orada exp = payload.get("expected_hash", "") boş geldiğinde cur_hash.startswith("") her zaman True döner ve dosya write_text ile doğrudan üzerine yazılır. Yani expected_hash vermeyen bir mutasyon, araya giren her değişikliği çakışma üretmeden siler. Bu, PR'ın "orijinal dosya asla ezilmez" vaadine ters. expected_hash zorunlu olmalı (tam 64 karakter, prefix değil). Eşleşmezse de çakışma kopyasına gitmeli.
2. source doğrulanmıyor, vault dışına yazılabiliyor
target_path = vpath / source ve isolate_conflict içindeki yazma, beyin_v3_sync.py'deki yol korumalarını (is_absolute(), '..' in parts, is_relative_to(self.root)) atlıyor. source: "../../x.md" ya da mutlak bir yol, note_update yedek dalında ve çakışma kopyasında vault'un dışına dosya yazdırıyor. Doğrudan dosya yazan yerler kaldırılıp yazma motorun kendi yollarından yapılmalı ya da en azından aynı doğrulama enqueue sırasında uygulanmalı.
3. Eşzamanlı enqueue ile replay mutasyon kaybettiriyor
replay_mutations kuyruğu okuyor, işliyor, sonra _save_all_mutations ile tüm dosyayı os.replace ile yeniden yazıyor. Bu arada başka bir oturum enqueue ile satır eklerse o satır kayboluyor. #205'in çıkış noktası tam da "birden fazla cihaz / agent oturumu aynı anda", bu yüzden kilit (ya da ayrı bir durum dosyası, append-only durum olayları) gerekli.
4. "Fixes #205" issue'yu kapatır
Bakımcı #205'i olay kimliği/cihaz kaydı, taşıma, yetkilendirme ve çakışma politikası tasarımı için bilerek açık tutuyor. Bu PR o tasarımı çözmüyor: kuyruk state/ altında, yani makineye özel. Cihazlar arasında hiçbir şey taşınmıyor. Lütfen Refs #205 olarak değiştirelim.
Düzeltilmesi gerekenler
- Yinelenen dosya:
scripts/altında sadece CLI (beyin_v3.py) var; runtime modülleritemplate/.claude/scripts/altında ve_ensure_scripts_path()ile oradan yükleniyor.scripts/beyin_v3_mutation.pybirebir kopya, iki dosya zamanla ayrışır. Kaldırılmalı. - Çakışma kopyası üzerine yazılabiliyor: Ad saniye çözünürlüğünde. Aynı saniyede aynı kaynak için iki çakışma olursa ikincisi
write_textile ilkini eziyor. Var olan dosyayı kontrol edip son ek eklemek ya daopen(..., "x")kullanmak yeterli. doctorsalt okunur değil:get_machine_iddosya yoksadevice.jsonoluşturuyor.doctoriçinden çağrıldığında yazma yapılmamalı.mutation-queueher alt komutta yazma sayılıyor:SOURCE_WRITE_COMMANDS'a eklendiği için--listve tanı çağrıları damark_session_edittetikliyor. Sadece--replayyazma sayılmalı.failed/conflictkayıtlar için çıkış yok: Yeniden deneme ya da çözüldü işaretleme yolu yok.doctorkalıcı olarakneeds_attentiongösterir.task_updateçakışmasında kopya yok: Diğer eylemler çakışma kopyası açarken burada değişiklik sadece kuyruktaconflictolarak kalıyor. Davranış tutarlı olmalı ya da belgelenmeli.- Mutasyon kimliği idempotent değil:
mutation_idzaman damgası içerdiği için aynı mutasyonun iki kez kuyruğa girmesi iki ayrı kayıt üretiyor. Issue'daki "aynı olay ikinci kez gönderildiğinde veri çoğalmaz" hedefi için kimlik içerikten türetilmeli ya da çağıran taraf vermeli.
Testler
Eklenmesi iyi olur:
expected_hasholmadan ve yanlış hash ilenote_update(dosyanın değişmediğini doğrulayan)../ve mutlaksourceileenqueue/replayreddi- Aynı saniyede iki çakışma kopyası
Yazım sırasında Linux/macOS/Windows test matrisi henüz tamamlanmamıştı. Yeşile döndükten sonra yukarıdaki maddelerle birlikte tekrar bakabilirim.
|
@bakiabaci replay yolunda ölçtüklerim (dosya işlemleri bellekte taklit edildi, kuyruk/replay fonksiyonları gerçek):
Yeniden üretim (Windows 11, Python 3.14; dosya yazmaları bellekte tutuldu)import datetime, hashlib, io, sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
sys.path.insert(0, str(Path('template/.claude/scripts').resolve()))
import beyin_v3_mutation as m
from beyin_v3_sync import render
from beyin_v3_secrets import redact
v, s = Path('C:/review-memory/vault'), Path('C:/review-memory/state')
fs = {}
def key(p): return str(Path(p).resolve())
def put(p, text, **kw): fs[key(p)] = text; return len(text)
class Handle(io.StringIO):
def __init__(self, p, mode):
super().__init__(fs.get(key(p), '') if mode != 'w' else '')
self.p, self.mode = p, mode
if mode == 'a': self.seek(0, 2)
def fileno(self): return 0
def close(self):
if not self.closed and self.mode != 'r': put(self.p, self.getvalue())
super().close()
def fopen(p, mode='r', **kw): return Handle(p, mode)
def temporary(**kw): return 0, str(s/'temporary')
def replace(a, b): fs[key(b)] = fs.pop(key(a))
class Clock(datetime.datetime):
@classmethod
def now(cls, tz=None): return cls(2026,10,20,12,0,0,tzinfo=tz)
def enqueue(action, source, payload): return m.enqueue_mutation(s, action, source, payload)
def replay(): return m.replay_mutations(v, s, SimpleNamespace(sync=lambda:None))
print('existing filter:', redact('sk-'+'SyntheticOnly'*3, s)[0])
with patch.object(Path,'mkdir'), patch.object(Path,'is_file',lambda p:key(p) in fs), patch.object(Path,'read_text',lambda p,**kw:fs[key(p)]), patch.object(Path,'read_bytes',lambda p:fs[key(p)].encode()), patch.object(Path,'write_text',put), patch.object(m,'open',fopen,create=True), patch.object(m.os,'fdopen',lambda fd,*a,**kw:fopen(s/'temporary','w')), patch.object(m.os,'fsync'), patch.object(m.os,'replace',replace), patch.object(m.tempfile,'mkstemp',temporary), patch.object(m,'get_machine_id',return_value='a'*32), patch.object(m.datetime,'datetime',Clock):
put(v/'notes/a.md','newer device B\n')
enqueue('note_update','notes/a.md',{'text':'stale A'})
r = replay(); print('overwrite:',r['applied'],r['conflicts'],repr(fs[key(v/'notes/a.md')]))
put(v/'../victim.txt','victim\n')
enqueue('note_update','../victim.txt',{'text':'overwrite victim'})
replay(); print('escape:',key(v/'../victim.txt'),repr(fs[key(v/'../victim.txt')]))
token = 'sk-'+'SyntheticOnly'*3
enqueue('note_update','notes/a.md',{'text':token,'expected_hash':hashlib.sha256(b'stale A\n').hexdigest()})
replay(); print('update leaks token:',token in fs[key(v/'notes/a.md')])
enqueue('note_create','notes/conflict.md',{'text':token})
put(v/'notes/conflict.md','other\n')
replay(); print('conflict leaks token:',any('sync-conflict' in p and token in t for p,t in fs.items()))
for text in ['first offline edit','second offline edit']: enqueue('note_create','notes/collision.md',{'text':text})
put(v/'notes/collision.md','original\n')
r = replay(); copies = [t for p,t in fs.items() if 'collision.sync-conflict' in p]
print('conflict collision:',r['conflicts'],len(copies),repr(copies[0]))
put(v/'notes/retry.md',render({},'same\n'))
enqueue('note_create','notes/retry.md',{'text':'same'})
r = replay(); print('note retry:',r['applied'],r['conflicts'])
meta = {'id':'t','status':'inbox','owner':'x'}
put(v/'tasks/retry.md',render(dict(meta,kind='task',revision=1),'same\n'))
enqueue('task_create','tasks/retry.md',{'text':'same','metadata':meta})
r = replay(); print('task retry:',r['applied'],r['conflicts'])
fs.clear(); enqueue('note_create','notes/A.md',{'text':'A'})
def create(*args):
enqueue('note_create','notes/B.md',{'text':'B'})
print('queue after concurrent enqueue:',[x['source'] for x in m.list_mutations(s)])
return {'status':'succeeded'}
m.replay_mutations(v,s,SimpleNamespace(note_create=create,sync=lambda:None))
print('queue after replay save:',[x['source'] for x in m.list_mutations(s)]) |
|
Sekiz yeni test geçiyor ve gerçek CLI'da enqueue → replay → clear akışı çalışıyor. Ancak replay yazma yolu motorun korumalarını atlıyor (hepsi geçici kasada gerçek CLI ile üretildi):
Bugünkü main ile Ölçüm: Windows 11, Python 3.14.6, PR head |
|
Takip commit'ini eski sekiz repro ile yeniden ölçtüm; hash/TOCTOU, kasa dışına yazma, enqueue kaybı, secret filtresi, private metadata, çakışma kopyaları, create tekrarları ve doctor/list yan etkileri kapandı. 15 kuyruk testi ve önceki üç CI testi geçiyor; takip üretim yaması geri alınınca yeni yedi test ve üç CI testi düşüyor. Eşzamanlı enqueue kilitte bekliyor; timeout'ta açık hata veriyor, replay serbest bırakılınca başarılı enqueue kaydı korunup sonraki replay'de uygulanıyor. Bir yeni regresyon var: sıfır baytlık |
|
Katkı ve |
Özet ve ilgili çalışma
state/mutation-queue.jsonlüzerinde append-only biriktiren, bağlantı sağlandığında yerel senkronizasyon motoru üzerinde idempotent biçimde yeniden oynatan (replay) ve çakışma durumunda orijinal dosyayı asla ezmeyip mevcut dosya eşitleme sözleşmesine (_STAMPED_COPY/.sync-conflict-*.md) uygun izole kopyalar üreten çevrimdışı kuyruk katmanı eklendi.main@d9aa301Ne bozuktu?
parallel-presence-device.json) ile mutasyon takibi arasında ortak, gizlilik odaklı ve donanım/kullanıcı adı sızdırmayan anonimmachine_idstandardı tekilleştirilmemişti.Hatanın yeniden üretim sıralamaları
Senaryo: Çevrimdışı oturumda üretilen değişikliklerin çoklu cihazda çakışması
notes/plan.mdüzerinde düzenleme yapar.notes/plan.mddosyasını günceller ve git ile depoya yazar.lost update).Yapılan değişiklikler ve tasarım kararları
1. Anonim Cihaz Kimliği (
get_machine_id)state/parallel-presence-device.jsonveyastate/device.jsonüzerinden 32 karakterlik rastgele hex kimlik okunur; yoksauuid.uuid4().hexile atomik ve 0o600 izinleriyle oluşturulur.2. Çevrimdışı Mutasyon Kuyruğu (
template/.claude/scripts/beyin_v3_mutation.py&scripts/beyin_v3_mutation.py)enqueue_mutation:note_create,note_update,task_create,task_updatevereceipteylemlerinistate/mutation-queue.jsonldosyasına append-only biçimde yazar. Kuyruk dosyası vault dışında, makineye özelstate/dizininde tutulur.list_mutations: Kuyruktaki girdileri listeler (--statusfiltresi ile).replay_mutations: Bekleyen (pending) mutasyonları FIFO sırasıyla yerel senkronizasyon motorunda yürütür:applied: True, idempotent: True).isolate_conflict): Eğer diskteki dosya değişmişse veya çakışma varsa, orijinal dosya ASLA değiştirilmez;.sync-conflict-<YYYYMMDD>-<HHMMSS>-<machine_id[:8]>.mdkopyası oluşturulur. Bu desen doğrudanbeyin_v3_sync.pyiçindeki_STAMPED_COPYregex'i ile tam uyumludur;synckomutunda görünür uyarı (file sync conflict copy) üretir.clear_applied: Yalnızcaappliedstatüsündeki kayıtları temizler,pendingveconflictkayıtlarını kullanıcı denetimi için korur.3. CLI ve Doctor Entegrasyonu (
scripts/beyin_v3.py)beyin.py mutation-queue [--enqueue --file <json> | --list | --replay | --clear-applied | --status]komutu eklendi.doctorçıktısınamutation_queuebloğu (machine_id,pending_count,conflict_count,applied_count) eklendi.SOURCE_WRITE_COMMANDSkümesinemutation-queuedahil edilerek oturum makbuz hatırlatma penceresi korundu.Davranış örnekleri
state/mutation-queue.jsonliçinde append-only saklanır.sync-conflict-*.mdkopyası açılır,syncuyarıridempotent: Trueolarak tanınır ve no-op ile uygulanmış sayılırmutation_queuealanı ile bekleyen ve çakışan mutasyon sayıları raporlanırRegresyon ve negatif kontrol kapsamı (
RED → GREEN)RED):beyin.py mutation-queuekomutu vebeyin_v3_mutationmodülü bulunmuyordu.GREEN):tests/v3_mutation_queue_test.pyiçindeki 8 yeni test başarıyla geçti:test_machine_id_generation_and_persistencetest_enqueue_and_list_mutationstest_replay_mutations_applies_note_and_receipttest_replay_task_create_and_updatetest_conflict_isolation_preserves_original_and_creates_stamped_copytest_clear_applied_prunes_queuetest_doctor_diagnosticstest_cli_mutation_queue_commandsTest ve doğrulama sonuçları
main@d9aa301git diff --checktemizDeğişen dosyalar
template/.claude/scripts/beyin_v3_mutation.pyscripts/beyin_v3_mutation.pyscripts/beyin_v3.pySOURCE_WRITE_COMMANDStests/v3_mutation_queue_test.pyV3 mimari kısıt ve güvenlik kontrolü
pip) bağımlılığı eklenmedi.state/klasörüne yazılır; kullanıcı adı veya mutlak yol sızdırılmaz.template/.claude/scripts/vescripts/senkron tutuldu.os.replace), POSIX izinleri (0o600) ve UTF-8 satır sonları gözetildi.Yeniden çalıştırma
Maintenance verification — 2026-10-25
Repaired current-main conflicts by merging main into this PR branch, preserving all contributor commits; no PR was merged into main. Queue mutations now share a cooperative local-state lock; concurrent enqueue is preserved; malformed queue data fails without rewrite. Replay validates paths and full hashes, uses the ordinary note writer, preserves omitted private/project metadata, applies enabled secret filtering to updates and conflict copies, creates distinct conflict files exclusively, recognizes note/task-create retries, and leaves read-only identity/session diagnostics unchanged. Python 3.11: 15 focused queue tests passed; retrieval evaluation passed. Six regression cases fail before repair. Full suites/CI are separate. Remaining design boundary: this is a local queue, not device transport/authentication/acknowledgement or an exactly-once task-update transaction. An interruption after task revision advancement but before queue status persistence still needs review. Refs #205; the full issue is not resolved.
Maintainer follow-up — 2026-10-26
Allowed guarded updates to an existing empty note only with a matching source hash, retaining the zero-byte backup and writer checks. Real empty-note regression failed before the fix; 37 focused tests ran on each Python 3.11/3.14 with one skip. Transport, authentication, conflict policy and broader sync acceptance remain unresolved.
These are focused results, not a full-suite pass. Fresh exact-head CI is pending. Contributor commits are preserved; the follow-up is append-only.