عشرة وكلاء شغّلوا نفس فحص الأنواع في اللحظة نفسها. الحل كان مجلدًا.
سبعة عشر وكيل برمجة في نفس النسخة من المستودع، عشر عمليات tsc في وقت واحد، load average عند 37 و87 ميغابايت من الذاكرة الحرة. فحص أنواع مدته تسعون ثانية استغرق 7 دقائق و36 ثانية. إليك القياس، وسبب أن الآلة لم تكن تحسب، والقفل المشترك الصغير الذي حل المشكلة. قابل للنسخ إلى أي مستودع.
في 7 سبتمبر، توقفت آلة التطوير لدينا عن الاستجابة كما ينبغي. لا انهيار ولا تجمد. ببساطة صار كل شيء يستغرق عشرة أضعاف الوقت، بما في ذلك ما لا علاقة له بالشيفرة.
المشتبه بهم البديهيون كانوا جميعًا خطأ. الحاسوب لم يكن يسخن: لا throttling مسجل، والبطارية عند 30.6 درجة مئوية. لا عملية مجنونة تلتهم المعالج. لا شيء نُشر. الشيء الوحيد غير المعتاد أن سبعة عشر واجهة CLI لوكلاء كانت حية في نفس المستودع، وهذا يوم عمل عادي عندنا.
إليك ما كان يحدث فعلًا، مقيسًا لا مخمَّنًا.
القياس
آلة بذاكرة 16 غيغابايت و8 أنوية، مشتغلة منذ خمس ساعات ونصف، وسبعة عشر وكيلًا في العمل:
| ما قسناه | القيمة |
|---|---|
| واجهات CLI الحية للوكلاء | 17 |
عمليات tsc --noEmit المتزامنة، خلال دقيقتين | 3، ثم 10 |
| Load average | 37 إلى 41 |
| الذاكرة الحرة / ضاغط الذاكرة | 87 ميغابايت / 7.2 غيغابايت |
| فحص أنواع واحد على تطبيق سطح المكتب، آلة مشبعة | 7 دقائق و36 ثانية زمن جدار مقابل 26 ثانية معالج |
| نفس فحص الأنواع، آلة هادئة | 33 ثانية |
السطر الحاسم هو ما قبل الأخير. ست وعشرون ثانية من المعالج موزعة على سبع دقائق ونصف تعني عشرة بالمئة من الاستغلال. فحص الأنواع لم يكن يحسب. كان ينتظر الذاكرة.
وواحدة من تلك العمليات قتلها نظام التشغيل في منتصف الطريق. عملية tsc مقتولة تخرج برمز غير صفري ومخرَج فارغ، وهو أمر لا يمكن تمييزه عن خطأ نوع حقيقي. فبالإضافة إلى بطئها، كانت الآلة تنتج أحكامًا لا يستطيع أحد الوثوق بها.
لم يرتكب أحد أي خطأ
هذا هو الجزء الذي يستحق التوقف عنده، لأنه ما يجعل العطل صعب التوقع إلى هذا الحد.
كل واحد من هؤلاء الوكلاء كان يتبع القاعدة. كل منهم حرر TypeScript. كل منهم كانت تعليماته أن يتحقق من أنواعه قبل تسليم العمل. كل منهم شغّل tsc --noEmit. ولم يكن أي منهم يرى الآخرين. لا توجد لوحة مشتركة يكتب عليها وكيل «أنا أقوم الآن بالشيء المكلف، انتظروا».
ثم تتغذى المشكلة على نفسها. فحص الأنواع يتباطأ لأن الآلة مشبعة. الوكيل الذي يراقبه يستنتج أنه عالق. فيقتله ويطلق واحدًا آخر. هذا الانعكاس صحيح بمفرده وكارثي ضمن مجموعة، وهو من عائلة الأعطال نفسها التي وثّقناها قبل شهر حين كان الوكلاء يتركون خلفهم عمليات بحث عالقة: Process Guard هو الشبكة التي تعثر على ما بدأ فعلًا، وهنا نمنع البدء أصلًا.
الحلول الثلاثة التي لم نأخذ بها
تشغيل عدد أقل من الوكلاء. هذا يقسم العرض ويبقي على الخلل. فحصا أنواع متزامنان على آلة محملة يبقيان أبطأ من فحص واحد، وتقليص الأسطول يعني دفع ثمن المشكلة من الشيء نفسه الذي يمنح العمل سرعته.
فحص أنواع واحد في النهاية. مغرٍ، وخاطئ لسبب لا علاقة له بالأداء. خطأ نوع يُكتشف بعد عشر مهام يصبح يتيمًا: الوكيل الذي كتبه أُغلق، وسياقه ضاع، وعلى إنسان أن يعيد فتح الموضوع كله ليصحح سطرًا واحدًا. لم نكن نريد تأجيل التحقق.
التصريف التدريجي. جُرّب واستُبعد. المكسب مشكوك فيه في وضع --noEmit، والعمليات المتزامنة تفسد ملف .tsbuildinfo المشترك. إنه يحل نصف المشكلة بتفاقم النصف الآخر.
ما فعلناه بدلًا من ذلك: فحص واحد، مشترك
القاعدة ليست «تحقق أقل كثيرًا». إنها فحص أنواع واحد في كل مرة، لكل مشروع، للجميع. سكربت غلاف يستبدل N تحققًا بواحد، ويجيب عن ثلاث حالات:
- لا شيء تغير منذ آخر تشغيل، فنعيد نتيجته.
- تشغيل جارٍ بالفعل، فـننتظره ونأخذ نتيجته.
- وإلا، نأخذ القفل ونكون عملية
tscالوحيدة على الآلة.
من وجهة نظر الوكيل لم يتغير شيء: يكتب yarn typecheck، ويحصل على أخطاء أنواعه. ولا ينتظر أطول من قبل أيضًا، لأن التشغيل الذي ينتظره هو تشغيل بدأ قبل تشغيله هو. الآلة تدفع ثمن واحد بدل عشرة.
هذه هي الفكرة كلها. المثير أن الآليتين اللتين تحتاجهما أصغر بكثير مما قد تتخيل.
القفل مجلد
لا ملف، لا قاعدة بيانات، لا خدمة خفية. مجلد.
try {
mkdirSync(lockDir); // نجح: القفل لنا
} catch (err) {
if (err.code === 'EEXIST') { /* شخص آخر يمسكه، ننتظر */ }
}
mkdir إما ينشئ المجلد أو يفشل بـ EEXIST، ويفعل ذلك بشكل ذري على macOS وWindows وLinux، بلا اعتمادية وبلا نداء أصلي. كتابة ملف ثم التحقق من وجوده كانت ستكون عمليتين، والعمليتان هما تحديدًا الموضع الذي ينزلق فيه وكيل ثانٍ.
داخل المجلد نضع ملف owner.json فيه الـ pid واسم المضيف ووقت البدء. هذا الملف للتشخيص ولكشف قفل ميت. ليس هو من يقصي أبدًا.
القفل الميت يُستعاد تلقائيًا في حالتين: العملية المالكة اختفت (يُتحقق من ذلك فقط إذا تطابق اسم المضيف، لأن الـ pid لا يعني شيئًا من آلة إلى أخرى)، أو مرّ على القفل أكثر من خمس عشرة دقيقة.
فخ كلّفنا خللًا. بين mkdir وكتابة owner.json توجد نافذة يكون فيها المالك غير مقروء. إعلان القفل ميتًا داخل تلك النافذة يعني سرقته ممن أخذه للتو، أي بالضبط السباق الذي وُجد الملف ليمنعه. لذلك حين لا يكون هناك مالك مقروء، نحكم بعمر المجلد، لا بالملف الغائب.
البصمة تاريخ وعدد
الحالة الأولى تحتاج أن تعرف هل تغير شيء منذ آخر تشغيل. الجواب البديهي هو حساب بصمة تجزئة لملفات المصدر. نحن لا نفعل ذلك.
البصمة هي <أحدث mtime>:<عدد الملفات> على الجذور المستنتجة من حقل include في الـ tsconfig، إضافة إلى الـ tsconfig نفسه.
على 2300 ملف، قراءة كل بايت تكلف أكثر من الفحص الذي نوفره. التاريخ وحده لا يرى الحذف. العدد وحده لا يرى التعديل. معًا يغطيان الحالتين. السلبية الكاذبة المقبولة هي تعديلان داخل الميلي ثانية نفسها يتركان العدد كما هو، وأسوأ حالة هناك نتيجة مخزنة متأخرة ببضع ثوانٍ، لا خطأ نوع صامت أبدًا، لأن التحقق الحاجب يبقى تحقق البناء.
قاعدة مكتوبة لم تكفِ، فأضفنا hook
التعليمة كانت في AGENTS.md منذ اليوم الأول: لا تشغّل tsc مباشرة أبدًا، دائمًا السكربت المشترك. لم يكفِ ذلك، ويستحق الأمر أن نقول بصدق لماذا.
التحقق من الأنواع بعد تعديل انعكاس راسخ في العمق. تحت الضغط، يكتب وكيل npx tsc --noEmit دون إعادة قراءة التعليمات. ويكفي وكيل واحد يخالف القاعدة ليعيد إنتاج التكدس الذي وُجد القفل ليمنعه. التعليمة قابلة للنقاش. الـ hook لا.
فوصلنا hook من نوع PreToolUse على أداة Bash، يرفض tsc المباشر ويسمي الأمر الصحيح في نص الرفض:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/scripts/hooks/block-direct-tsc.mjs\"" }
]
}
]
}
}
الـ hook يقرأ نداء الأداة كـ JSON على stdin، ويخرج بالرمز 2 مع السبب على stderr للرفض، وبالرمز 0 للسماح. تفصيلان يصنعان الفرق بين hook مفيد وآخر مزعج.
يتعرف على tsc في موضع الأمر، لا في أي مكان من السلسلة. البحث عن الأحرف الثلاثة في أي موضع كان سيرفض أيضًا grep -rn tsc AGENTS.md. لذلك يشترط النمط وجود tsc في بداية سطر أو بعد ; أو && أو || أو | أو (، مسبوقًا اختياريًا بمشغّل حزم وبمسار. ويسمح كذلك بمرور tsc --version: لا سبب لرفض خيار إخباري.
ويرفض شيئًا ثانيًا لم نكن نتوقعه. رأينا وكيلًا ينتظر خلف القفل، ثم يقرر بعد حين أنه لا بد ميت، فيحذف مجلد القفل ليفك حصار نفسه. هذا يطلق عملية ثقيلة ثانية إلى جانب الحية، أي الالتفاف المثالي على كل ما يحميه القفل. لذلك حذف مجلد القفل أو مجلد الذاكرة المؤقتة مرفوض هو الآخر، مع شرح أن القفل الميت يُستعاد وحده.
هذا الرفض الثاني ما كنا لنكتبه مسبقًا أبدًا. جاء من ملاحظة ما يفعله الوكلاء فعلًا حين يُحصرون، وهو مصدر لحواجز الأمان أفضل من تخيل ما قد يفعلونه.
أين تتوقف الفكرة
الـ hook خاص بـ Claude Code. بقية واجهات الوكلاء في الأسطول لا ترى سوى القاعدة المكتوبة. هذه ثغرة معروفة ونتحملها: حاجز أمان يغطي غالبية الأسطول أفضل من غياب أي حاجز في انتظار معيار hooks تقرأه كل الواجهات.
أما السكربت المشترك فهو محايد تجاه المزود، لأنه أمر كأي أمر آخر. أي CLI قادر على تشغيل yarn typecheck يستفيد من القفل، سواء أُجبر على ذلك أم لا.
ما يجب أن تخرج به
فحص الأنواع كان حالتنا الأكثر ضجيجًا، لا حالة خاصة. النمط ينطبق على كل أمر مكلف، عديم الأثر الجانبي ضمن نافذة قصيرة، ويطلقه كل وكيل للسبب الوجيه نفسه: تثبيت الاعتماديات، تشغيل مجموعة الاختبارات كاملة، البناء للإنتاج، تشغيل خادم تطوير على منفذ ثابت.
ثلاثة أسئلة، بهذا الترتيب، وتصبح لديك كل الفكرة:
- هل يمكنني إعادة استخدام نتيجة حديثة؟
- هل يمكنني الانضمام إلى التشغيل الجاري بالفعل؟
- وإلا، هل أنا من يبدأه، وحده؟
إذا كان عدة وكلاء يتقاسمون آلتك، فما يستحق القياس ليس كم منهم يعمل. بل كم منهم يطلق نفس الأمر في نفس الدقيقة. هذا الرقم هو ما تشعر به آلتك حقًا، وما دمت لا تنظر إليه، ستلوم الحرارة.
إذا أردت الصورة الأوسع عن طريقتنا في تشغيل عدة وكلاء على مستودع واحد دون أن يدوس بعضهم بعضًا، فهي في تشغيل وكلاء البرمجة بالتوازي، وشبكة الأمان للعمليات التي بدأت فعلًا هي Process Guard. السكربت المشترك والـ hook يعيشان كلاهما في مستودع AgentsRoom، أي حيث كان أولئك الوكلاء السبعة عشر يعملون ذلك المساء.
تحميل AgentsRoom
شغّل كل وكلاء الذكاء الاصطناعي لديك، في كل مشاريعك، من نافذة واحدة.
التطبيق المرافق: تابع وكلاءك أينما كنت
استخدم Claude أو Codex أو Antigravity CLI أو أي مزود AI آخر.
أرسل الأخطاء والطلبات مباشرة إلى قائمة المهام العامة.
لمحة عن AgentsRoom أثناء العمل.
تابع القراءة
30 حدث خطاف ينطلق في جلسة Claude Code. ثلاثة فقط يمكنها الرد.
القائمة الكاملة لأحداث خطافات Claude Code، ومتى ينطلق كل منها، وأي 15 منها يمكنه الحظر، وقاعدة stdout التي تبتلع بصمت مخرجات معظم الخطافات. مرجع ميداني بُني من تشغيل الخطافات في الإنتاج عبر آلاف جلسات الوكلاء.
اقرأ المقالمدرب الجري بالذكاء الاصطناعي عندي هو مستودع Git ووكيل Claude
أنهي حصتي، تتزامن ساعتي، وبعد ثلاث دقائق يكون التحليل مكتوبا في مستودعي، والأسبوع قد أعيد ضبطه، ومدربي قد ترك تعليقا تحت نشاط Strava. لا تطبيق مبرمج، ولا خادم مكتوب، ولا فاتورة API محسوبة على كل token: اشتراك Claude، وAgentsRoom، وملفات Markdown. هذا هو التركيب الكامل، قابل لإعادة الإنتاج.
اقرأ المقاللا يحتفظ Antigravity CLI إلا بتسجيل دخول Google واحد لكل جهاز، وإليك ما ينفع بدلًا منه
لماذا لا يمكنك التناوب بين اشتراكَي Google AI Pro في Antigravity CLI، وأين يخزّن تسجيل دخولك فعلًا، وما الذي تفعله مبدّلات الحسابات حقًا بسلسلة مفاتيح نظامك، ولماذا لا تضاعف خطة العائلة حصتك، والأسلوب الوحيد الذي يشغّل عدة حسابات بالتوازي فعلًا.
اقرأ المقال