On ajan aynı tip kontrolünü aynı anda çalıştırdı. Çözüm bir klasördü.

Tek bir checkout içinde on yedi kod ajanı, aynı anda on tsc süreci, load average 37 ve 87 MB boş bellek. Doksan saniyelik bir tip kontrolü 7 dk 36 sürdü. İşte ölçüm, makinenin neden hesap yapmadığı ve sorunu çözen küçük paylaşımlı kilit. Her depoya kopyalanabilir.

7 Eylül'de geliştirme makinemiz düzgün yanıt vermeyi bıraktı. Çökme değil, donma değil. Sadece her şey on kat uzun sürmeye başladı, kodla hiçbir ilgisi olmayan şeyler dahil.

Bariz şüphelilerin hepsi yanlış çıktı. Dizüstü ısınmıyordu: kayıtlı throttling yok, batarya 30,6 C. Kaçak bir süreç CPU'yu yemiyordu. Hiçbir şey dağıtılmamıştı. Tek sıra dışı şey, aynı depoda on yedi ajan CLI'ının canlı olmasıydı ki bu bizde normal bir iş günü.

İşte gerçekte olan şey, tahmin edilerek değil ölçülerek.

Ölçüm

16 GB, 8 çekirdekli, beş buçuk saattir açık bir makine, on yedi ajan iş başında:

Ölçtüğümüz şeyDeğer
Canlı ajan CLI'ları17
İki dakika içinde görülen eşzamanlı tsc --noEmit süreçleri3, sonra 10
Load average37 ile 41 arası
Boş bellek / bellek sıkıştırıcı87 MB / 7,2 GB
Masaüstü uygulamasında bir tip kontrolü, doymuş makine26 sn CPU için duvar saatinde 7 dk 36
Aynı tip kontrolü, sakin makine33 sn

Belirleyici satır sondan bir öncekidir. Yedi buçuk dakikaya yayılmış yirmi altı saniye CPU, yüzde on kullanım demektir. Tip kontrolü hesap yapmıyordu. Bellek bekliyordu.

Ve bu süreçlerden biri işletim sistemi tarafından koşu ortasında öldürüldü. Öldürülen bir tsc, sıfırdan farklı bir kodla ve boş çıktıyla sonlanır; bu da gerçek bir tip hatasından ayırt edilemez. Yani makine yavaş olmakla kalmıyor, kimsenin güvenemeyeceği kararlar üretiyordu.

Kimse yanlış bir şey yapmadı

Üzerinde durmaya değen kısım burası, çünkü bu arızayı gelirken görmeyi bu kadar zorlaştıran da bu.

Bu ajanların her biri kurala uyuyordu. Her biri TypeScript düzenlemişti. Her birine, işi teslim etmeden önce tiplerini doğrulaması söylenmişti. Her biri tsc --noEmit çalıştırdı. Hiçbiri diğerlerini göremiyordu. Bir ajanın «şu anda pahalı işi yapıyorum, bekleyin» diye yazacağı ortak bir tahta yok.

Sonra kendi kendini besliyor. Makine doymuş olduğu için tip kontrolü yavaşlıyor. Onu izleyen ajan takıldığına karar veriyor. Öldürüp yenisini başlatıyor. Bu refleks tek başına doğru, grup halinde felaket ve bu, bir ay önce ajanlar arkalarında takılı kalmış arama süreçleri bıraktığında belgelediğimiz arıza ailesinin aynısı: Process Guard başlamış olanı bulan ağ, buradaki ise başlamasını engelleyen çözüm.

Almadığımız üç karar

Daha az ajan çalıştırmak. Bu belirtiyi yarıya indirir, hatayı olduğu gibi bırakır. Yüklü bir makinede aynı anda iki tip kontrolü yine tek bir kontrolden yavaştır ve filoyu kısmak, sorunun bedelini işi hızlı kılan şeyle ödemektir.

Sonda tek bir tip kontrolü. Cazip ve performansla hiçbir ilgisi olmayan bir nedenle yanlış. On bilet sonra bulunan bir tip hatası öksüzdür: onu yazan ajan kapanmıştır, bağlamı kaybolmuştur ve tek satırı düzeltmek için bir insanın bütün konuyu yeniden açması gerekir. Doğrulamayı ertelemek istemedik.

Artımlı derleme. Denendi ve bırakıldı. --noEmit kipinde kazanç şüpheli ve eşzamanlı süreçler paylaşılan .tsbuildinfo dosyasını bozuyor. Sorunun yarısını, diğer yarısını kötüleştirerek çözüyor.

Bunun yerine ne yaptık: tek kontrol, paylaşımlı

Kural «daha seyrek kontrol et» değil. Kural şu: proje başına, herkes için, aynı anda tek bir tip kontrolü. Bir sarmalayıcı betik N kontrolü tek kontrolle değiştiriyor ve üç duruma cevap veriyor:

  1. Son koşudan bu yana hiçbir şey değişmedi, onun sonucunu geri veriyoruz.
  2. Bir koşu zaten sürüyor, onu bekliyoruz ve sonucunu alıyoruz.
  3. Aksi halde kilidi alıyoruz ve makinedeki tek tsc oluyoruz.

Ajanın bakış açısından hiçbir şey değişmedi: yarn typecheck yazıyor, tip hatalarını alıyor. Eskisinden daha uzun da beklemiyor, çünkü arkasında beklediği koşu kendisininkinden önce başlamış bir koşudur. Makine onun yerine birinin bedelini ödüyor.

Fikrin tamamı bu. İlginç olan, ihtiyaç duyduğu iki mekanizmanın da sanılandan çok daha küçük olması.

Kilit bir klasördür

Dosya değil, veritabanı değil, arka plan servisi değil. Bir klasör.

try {
  mkdirSync(lockDir);       // başardı: kilit bizim
} catch (err) {
  if (err.code === 'EEXIST') { /* başkası tutuyor, bekliyoruz */ }
}

mkdir ya klasörü oluşturur ya da EEXIST ile başarısız olur ve bunu macOS, Windows ve Linux üzerinde, bağımlılık ve yerel çağrı olmadan atomik biçimde yapar. Bir dosya yazıp sonra var olup olmadığına bakmak iki işlem olurdu ve iki işlem, tam olarak ikinci bir ajanın araya sızdığı yerdir.

Klasörün içine pid, makine adı ve başlangıç saatini taşıyan bir owner.json bırakıyoruz. O dosya tanı içindir ve ölü bir kilidi saptamak içindir. Dışlamayı yapan asla o değildir.

Ölü bir kilit iki durumda kendiliğinden geri alınır: sahip süreç yok olmuşsa (yalnızca makine adı eşleştiğinde bakılır, çünkü bir pid makineden makineye hiçbir şey ifade etmez) ya da kilit on beş dakikadan eskiyse.

Bize bir hataya mal olan tuzak. mkdir ile owner.json yazımı arasında, sahibin okunamadığı bir pencere var. Kilidi bu pencerede ölü ilan etmek, onu az önce alan süreçten çalmak demektir; yani dosyanın önlemek için var olduğu yarışın ta kendisi. Bu yüzden okunabilir bir sahip yoksa, eksik dosyaya değil klasörün yaşına bakarak karar veriyoruz.

Parmak izi bir tarih ve bir sayıdır

Durum 1'in, son koşudan bu yana bir şey değişip değişmediğini bilmesi gerekiyor. Bariz cevap kaynak dosyaları hash'lemek olurdu. Bunu yapmıyoruz.

Parmak izi, tsconfig içindeki include alanından türetilen kökler üzerinde <en yeni mtime>:<dosya sayısı> ve buna ek olarak tsconfig'in kendisi.

2.300 dosyada her baytı okumak, tasarruf ettiği kontrolden pahalıya geliyor. Tarih tek başına bir silmeyi kaçırır. Sayı tek başına bir düzenlemeyi kaçırır. Birlikte ikisini de kapsarlar. Kabul edilen yanlış negatif, sayıyı değiştirmeyen ve aynı milisaniye içinde yapılan iki düzenlemedir; oradaki en kötü durum birkaç saniye bayatlamış bir önbellek sonucudur, asla sessiz bir tip hatası değil, çünkü engelleyici doğrulama hâlâ derlemededir.

Yazılı kural yetmedi, bir hook ekledik

Talimat ilk günden beri AGENTS.md içindeydi: asla doğrudan tsc değil, hep paylaşımlı betik. Yetmedi ve bunun nedenini dürüstçe söylemeye değer.

Bir düzenlemeden sonra tipleri doğrulamak derinlemesine yerleşmiş bir reflekstir. Baskı altında bir ajan, talimatları yeniden okumadan npx tsc --noEmit yazar. Ve kilidin önlemek için var olduğu yığılmayı yeniden yaratmaya tek bir ajanın kuralı atlaması yeter. Talimat pazarlığa açıktır. Hook değildir.

Bu yüzden Bash aracına, doğrudan bir tsc çağrısını reddeden ve reddederken doğru komutu adıyla söyleyen bir PreToolUse hook'u bağladık:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
        ]
      }
    ]
  }
}

Hook, araç çağrısını stdin üzerinden JSON olarak okur; reddetmek için gerekçeyi stderr'e yazıp 2 koduyla, izin vermek için 0 koduyla çıkar. İki ayrıntı, işe yarar bir hook ile can sıkıcı bir hook arasındaki farkı yaratıyor.

tsc ifadesini komut konumunda tanıyor, dizenin herhangi bir yerinde değil. Bu üç harfi her yerde aramak grep -rn tsc AGENTS.md komutunu da reddederdi. Bu yüzden desen, tsc ifadesinin satır başında ya da ;, &&, ||, | veya ( sonrasında olmasını istiyor, önünde isteğe bağlı bir paket çalıştırıcı ve bir yol bulunabiliyor. Ayrıca tsc --version çağrısını geçiriyor: bilgi veren bir bayrağı reddetmek için sebep yok.

Öngörmediğimiz ikinci bir şeyi de reddediyor. Bir ajanın kilidin arkasında beklediğini, bir süre sonra kilidin ölmüş olması gerektiğine karar verdiğini ve kendini kurtarmak için kilit klasörünü sildiğini gördük. Bu, canlı olanın yanında ikinci bir ağır süreç başlatır; yani kilidin koruduğu her şeyin kusursuz biçimde atlanmasıdır. Bu yüzden kilit ya da önbellek klasörünü silmek de reddediliyor, ölü bir kilidin kendiliğinden geri alındığı açıklamasıyla.

O ikinci reddi önceden asla yazamazdık. Ajanların tıkandıklarında gerçekte ne yaptığını izlemekten çıktı ve bu, ne yapabileceklerini hayal etmekten daha iyi bir korkuluk kaynağı.

Nerede duruyor

Hook Claude Code'a özgü. Filodaki diğer ajan CLI'ları yalnızca yazılı kuralı görüyor. Bu bilinen bir boşluk ve kabul ettik: her CLI'ın okuduğu bir hook standardı gelene kadar, filonun çoğunu kapsayan bir korkuluk hiç korkuluk olmamasından iyidir.

Paylaşımlı betiğin kendisi sağlayıcıdan bağımsız, çünkü sonuçta sıradan bir komut. yarn typecheck çalıştırabilen her CLI, buna zorlansın ya da zorlanmasın kilitten yararlanır.

Buradan ne çıkarmalı

Tip kontrolü en gürültülü durumumuzdu, özel bir durum değil. Desen; pahalı olan, kısa bir pencerede idempotent olan ve her ajanın aynı iyi nedenle başlattığı her komut için geçerli: bağımlılıkları kurmak, tam test takımını koşturmak, üretim için derlemek, sabit bir portta dev sunucusu başlatmak.

Bu sırayla üç soru ve tasarımın tamamı elinde:

  1. Yakın tarihli bir sonucu yeniden kullanabilir miyim?
  2. Zaten süren koşuya katılabilir miyim?
  3. Aksi halde onu tek başıma başlatan ben miyim?

Makineni birden fazla ajan paylaşıyorsa, ölçmeye değen şey kaç tanesinin çalıştığı değil. Kaç tanesinin aynı dakika içinde aynı komutu başlattığı. Makinenin gerçekten hissettiği sayı budur ve sen ona bakmadıkça suçu ısıya atarsın.

Birden fazla ajanı tek bir depoda birbirlerinin ayağına basmadan nasıl çalıştırdığımıza dair geniş resmi istiyorsan, o Kod ajanlarını paralel çalıştırmak yazısında; başlamayı başaran süreçler için güvenlik ağı ise Process Guard. Paylaşımlı betik de hook da AgentsRoom deposunda yaşıyor, yani o öğleden sonra bu on yedi ajanın çalıştığı yerde.

AgentsRoom'u İndirin

Tüm yapay zeka ajanlarınızı, tüm projelerinizde, tek bir pencereden çalıştırın.

ÜcretsizAgentsRoom'u İndir

Yardımcı uygulama: hareket halindeyken ajanlarınızı izleyin

Claude, Codex, Antigravity CLI veya başka bir AI sağlayıcı kullan.

Uzantıyı yükleyin
Chrome Web Store

Hataları ve istekleri doğrudan genel backlogunuza gönderin.

AgentsRoom'a kısa bir bakış.

Çoklu proje
Çoklu sağlayıcı
Çoklu ajan
Canlı durum
Diff ve commit
Mobil uygulama
Canlı önizleme
Ajan ekipleri
Tarayıcı otomasyonu
Backlog odaklı dev
Prompt kütüphanesi
Beceri kütüphanesi
Tüm özellikleri gör

Okumaya devam et