Skip to main content
العودة إلى المدونة
Claude CodeAIAutomationMonitoringlaunchdResilience

system-droid: حارس يفحص أنظمتي ويصلحها بنفسه

كانت خدماتي المحلية تموت دائمًا بصمت: الموجز الصباحي لا يصل، والرموز تنتهي صلاحيتها، والنسخ الاحتياطي يتجمد — وأنا آخر من يعلم. فبنيت درويدًا على launchd يفحص كل شيء كل ساعتين، ويصلح ما يؤمَن إصلاحه عبر وكيل Claude، ولا يراسلني إلا حين يلزم إنسان فعلًا.

نُشر 17 أغسطس 202610 دقيقة قراءة

TL;DR

system-droid برنامج صغير بلغة Go يشغّله launchd كل ساعتين وبعد كل استيقاظ للـ Mac من السكون. يمرّ على قائمة فحوصات لجميع خدماتي المحلية، ويلزم الصمت ما دام كل شيء يعمل، ولا يكتب إلى Telegram إلا حين يتعطّل شيء فعلًا. ومرة واحدة يوميًا يرسل ملخصًا قصيرًا «كل شيء يعمل»، حتى لا يعني الصمت أبدًا «مجهول». وحين يتعطّل شيء، يحاول أولًا إصلاحه بنفسه: وكيل Claude ‏(Opus) مستقل يتعقّب السبب ويعالجه — ولا يستدعيني إلا إذا أخفق.

◆ Усі системи в нормі · 10/10 · 2026-08-17 09:00

— OpenClaw / Resonance
◆ styletts2-ua (tts) — локальний український TTS :8123
◆ openclaw gateway (:18789) — ядро агента Resonance (launchd + порт)
◆ openclaw hooks — hooks-ендпоінт увімкнено

— Інфраструктура
◆ cli-proxy Claude auth — робочий Claude-токен (валідність, авто-оновлення)
◆ system-droid — сам вартовий (надсилає ці звіти)
◆ otel-block — корпоративна телеметрія заблокована у firewall
◆ vault git backup — git-копія Obsidian-сховища на GitHub

لماذا وُجد أصلًا

خلال الأشهر الماضية تراكم عندي، دون أن أنتبه، أسطول كامل من الأتمتة المحلية: وكيل OpenClaw ‏(«Resonance»)، وخادم صوت أوكراني StyleTTS2، ووسيط LLM أمام Claude، ومولّدات ملاحظات يومية لـ Obsidian، ونسخة git من خزنتي، وقاعدة جدار ناري تحجب القياس عن بُعد الخاص بالشركات. كل واحدة من هذه الأشياء مفيدة ما دامت تعمل فقط. وكانت تتعطّل دائمًا بالطريقة نفسها — بصمت.

الموجز الصباحي ببساطة لا يصل، ولا ألاحظ ذلك إلا مساءً. المجدوِل يقتل عملية، ومع ذلك يبدو آخر تشغيل لها «ناجحًا». رمز مصادقة تنتهي صلاحيته، فيعيش التوليد أسابيع على نصّ طوارئ. لا يعلن أيٌّ من هذه الأعطال عن نفسه: الخدمة لا تشتكي — بل تختفي فحسب. كنت بحاجة إلى مَن يلاحظ ذلك بدلًا مني.

كيف يعمل

النظام كله ملف ثنائي واحد وملف config.json فيه نوعان من الفحوصات: HTTP (الخدمة تجيب على /health) وأوامر (سكربت ينتهي بالرمز 0). يطلق launchd الدرويد وفق الجدول؛ فيجري الفحوصات بالتوازي ويقرر إن كان يلزم إزعاجي أصلًا. وضع نظام جديد تحت المراقبة يعني سطرًا واحدًا في الإعدادات — دون إعادة ترجمة للبرنامج.

المبدأ الجوهري: الاقتصاد في الانتباه. لا لوحات يجب فتحها، ولا سيل «كل شيء بخير» كل ساعتين. توجد ثلاثة أنواع من الرسائل بالضبط: الملخص اليومي بأن كل شيء أخضر؛ و«تعطّل — أصلحته بنفسي»؛ و«تعطّل — أحتاجك». وما عدا ذلك صمت.

الإصلاح الذاتي

حين يفشل فحص، لا يهرع الدرويد إليّ فورًا. بل يطلق أولًا وكيل Claude بمجموعة أدوات مقيّدة وصلاحية كتابة في مجلدات محددة فقط. وقبل كل محاولة تُلتقط لقطة git لجميع مجلدات العمل، فأي تغيير يجريه الوكيل يمكن التراجع عنه بأمر واحد — وتصل قيم الـ hash لتلك اللقطات في الرسالة نفسها:

🔧 system droid — полагоджено автоматично, 1 спроба (2026-08-02 07:41)

Було зламано:
• news digest freshness — застарілий дайджест: 2026-07-31 (2 дні тому)

✅ Зараз усі перевірки зелені.

↩️ Відкат змін агента (git reset --hard на знімок):
• ~/Developer/system-droid  @ f037cfb
• ~/Developer/obsidian-cron @ e98746c

وهو ليس لعبة: لقد أعاد الوكيل فعلًا توليد ملخص قديم بعد انقطاع شبكة ليلي، وأعاد بنفسه تحميل مهمة launchd عالقة، وشخّص بنفسه أن مشكلة رمزٍ ما لا تُحل من دوني. في الرسالة أرى ما الذي تعطّل، وما الذي فعله الوكيل، وكيف انتهى الأمر.

ما الذي اصطاده حتى الآن

في سبعة أسابيع حوّل الدرويد عدة أعطال صامتة — كان كل منها قادرًا على العيش أيامًا — إلى رسائل رأيتها فورًا:

  • مولّد موجزات مقتول. أطلقت نواة macOS النار على العملية مباشرة بعد إعادة بناء الملف الثنائي — ما كان الموجز الصباحي ليصدر أبدًا، وما كان أحد ليلاحظ.
  • رمز تحديث منتهي الصلاحية. توقف الوسيط عن تجديد رمز الوصول، وصار كل استدعاء LLM يعود بـ 401. قالها الدرويد صراحة: هذا لا يُحل إلا بتسجيل دخول جديد — أمر واحد، دقيقتان.
  • نصّ بديل مكان التحليل. كان التوليد ينتهي «بنجاح»، لكن الملاحظة كانت تحوي نصّ طوارئ بدل تحليل الذكاء الاصطناعي. فحصُ محتوى الملاحظة، لا رمز الخروج، اصطاد ذلك من اليوم الأول.
  • نسخة احتياطية متجمدة. ظل المكوّن الإضافي لـ git يواصل عمليات الـ commit محليًا بينما مات الدفع إلى GitHub بصمت — كانت النسخة ستتجمد، ليُكتشف ذلك في أسوأ لحظة ممكنة.

ما لا يصلحه — عن قصد

أهم قرار في هذا النظام ليس ما يُؤتمت، بل ما لا يُؤتمت. بعض الفحوصات موسومة «تنبيه فقط»: كل ما يمسّ المصادقة وأنظمة الآخرين وجلسات وكيلي نفسه. رمز OAuth منتهي الصلاحية لا «يُصلَح» بسكربت — بل يجب أن يعيد إصداره إنسان في المتصفح. والوكيل ممنوع صراحة من لمس الرموز، أو إعادة تشغيل خدماتي العاملة، أو ليّ فحصٍ حتى يصير أخضر.

وهناك حدّ مادي أيضًا: المُصلِح نفسه هو Claude CLI، وحين تسقط الشبكة أو المصادقة يسقط مع كل شيء آخر. ما يكسر النظام يكسر أيضًا اليدين اللتين تصلحانه. فالتصرف الأمين هناك ليس محاولات بطولية، بل «أحتاجك» واضحة — مع تعليمات دقيقة عمّا يجب فعله.

التفاصيل الصغيرة التي قام عليها كل شيء

ما استهلك معظم الوقت لم يكن الفحوصات، بل موثوقية الحارس نفسه. أولًا: إعادة بناء الملف الثنائي لـ Go في مكانه تغيّر توقيعه، وlaunchd يخزّن متطلبات التوقيع مؤقتًا — فيقتل النظام ببساطة التشغيل المجدول التالي. الآن ينتهي كل بناء دائمًا بإعادة تحميل المهمة:

deploy.sh
# Перезбирання бінарника на місці змінює його cdhash, а launchd кешує
# вимоги до підпису (LWCR) на момент bootstrap. Після збирання кеш
# застаріває — і наступний плановий запуск ядро вбиває (exit 9) або
# launchd відмовляється його запускати (exit 78 / EX_CONFIG).
# Тому збирання ЗАВЖДИ закінчується перезавантаженням задачі:
go build -o system-droid .
codesign --verify --strict system-droid
launchctl bootout  "gui/$(id -u)/com.oleksii.system-droid" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.oleksii.system-droid.plist
./check-self.sh   # свіжий запуск має бути зеленим

ثانيًا: في خدمات KeepAlive، رمزُ خروج آخر تشغيل مقياسٌ مضلِّل. بعد كل إعادة تشغيل يظهر هناك SIGTERM، فتبدو خدمة حية تمامًا وكأنها معطّلة. السؤال الصحيح ليس «كيف انتهت» بل «هل تعمل الآن»:

check-launchd-running.sh
# У KeepAlive-демона LastExitStatus після КОЖНОГО перезапуску
# ненульовий (15/SIGTERM) — тож перевірка "останній запуск успішний"
# хибно валить цілком живий сервіс. Правильне питання інше:
# чи працює він ПРЯМО ЗАРАЗ?
info=$(launchctl list "$1" 2>/dev/null) || { echo "not loaded"; exit 1; }
pid=$(printf '%s\n' "$info" | awk -F'= ' '/"PID"/{gsub(/[ ;]/,"",$2);print $2}')
[ -n "$pid" ] && [ "$pid" != "0" ] && { echo "running (pid $pid)"; exit 0; }
echo "loaded but not running"; exit 1

ثالثًا: افحص الشيءَ بعينه الذي يعتمد عليه النظام. كان أول فحص للرمز عندي يحسب «تاريخ الإصدار زائد سنة» — ويلمع أخضر بينما الرمز الحقيقي يعيد 401 منذ يوم كامل:

# Календарна перевірка сяяла зеленим — річному токену ще жити й жити:
claude oauth token — OK: valid, 365d left

# А справжній робочий токен тим часом помер, і логи проксі це знали:
token refresh failed: {"error":"invalid_grant","error_description":"Refresh token expired"}
POST /v1/chat/completions → 401 "OAuth access token has expired. Re-authenticate."

ورابعًا: انقطاع شبكة لدورة واحدة في الثالثة فجرًا ليس سببًا لإيقاظ إنسان. إنذار «مطلوب تدخّل» لا يُرسَل الآن إلا إذا صمدت المشكلة عبر تشغيلين متتاليين. الأعطال العابرة تتلاشى وحدها؛ والحقيقية تصل على أي حال — متأخرة ساعتين فقط:

main.go
// Тривога "потрібне втручання" — лише якщо перевірка провалилася
// два запуски поспіль. Нічний обрив мережі, демон посеред перезапуску,
// пробудження зі сну — все це минає само до наступного циклу,
// і будити людину через таке не можна. Успішне лікування,
// навпаки, повідомляється одразу — це добра новина, а не шум.
counts := loadHealCounts()          // ~/.system-droid-heal-state.json
for _, r := range stillFailing {
    counts[r.Name]++
}
var persistent []Result
for _, r := range stillFailing {
    if counts[r.Name] >= 2 {        // поріг ескалації
        persistent = append(persistent, r)
    }
}
// сповіщення — лише якщо len(persistent) > 0

عيون لوكيل آخر

إلى جانب Telegram، يعمل الدرويد أيضًا كخادم MCP. أي أن وكيلي المحلي يستطيع سؤاله مباشرة عن حالة الأنظمة: بدل قراءة التقارير أقول «تحقق أن كل شيء يعمل» — فيستدعي الوكيل get_health، ويجري كل فحص مباشرة، ويجيب. توقفت المراقبة عن كونها صفحة تنظر إليها، وصارت خدمة تستشيرها.

# Дроїд працює і як MCP-сервер — локальний агент питає його сам:
Resonance › виклич get_health у system-droid і скажи, чи все зелене

get_health →
overall: 🟢 OK — 10/10 green (2026-08-17 09:00)
🟢 openclaw gateway (:18789) — ядро агента Resonance (launchd + порт)
🟢 cli-proxy Claude auth — робочий Claude-токен (валідність, авто-оновлення)
...

الخلاصة

تقنيًا لا شيء معقد هنا: Go وlaunchd وبضعة سكربتات shell وبوت Telegram. لكن هذه الحزمة البسيطة بالذات غيّرت الإحساس بأسطول الأتمتة كله: لم أعد أحمل في رأسي قائمة أشياء «يجب ألا أنسى فحصها». صار ذلك عمل الدرويد.

الصيغة قصيرة: افحص السلوك الحقيقي لا مؤشراته البديلة؛ وأصلح تلقائيًا ما يؤمَن إصلاحه؛ واستدعِ الإنسان بصدق حيث لا يجدي سواه؛ والزم الصمت بقية الوقت.

منذ سبعة أسابيع وهو صامت معظم الوقت. كل رسالة أرسلها كانت إما خبرًا سارًّا عن شيء أُصلح بالفعل، وإما إشارة دقيقة إلى الموضع الذي تلزم فيه دقيقتاي. هذا هو معنى الراحة: نظام يحرس الأنظمة الأخرى — ويحترم صمتك.

لاحظت خطأً؟

معلومة خاطئة، ترجمة ركيكة، أو شيء يبدو غير صحيح في هذه المقالة؟ راسلني — بلغتك أنت.