Retry agent skill loading after transient errors
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
## الحالة الحالية — 2026-10-07
|
||||
|
||||
- 2026-10-07 — تحسين تحمل انقطاع تحميل مهارات الوكيل: فحص واجهة الويب على `127.0.0.1:5303` كشف أن أول تحميل عرض خطأً مؤقتًا، ثم عادت الواجهة بعد إعادة التحميل واتصلت بـGemma 4 وأظهرت المحادثات المحفوظة. تحققت خدمة API بـ200، وأعادت `/v1/agent/skills` المهارات الثلاث مع CORS صحيحًا؛ أُلغيت جلسة الفحص المحلية. أضيفت ثلاث محاولات تحميل إجمالًا مع تراجع 300/750ms ومنع الطلبات المتزامنة؛ يزول خطأ المهارات عند النجاح ويبقى ظاهرًا إذا فشلت المحاولات كلها. اختبارات Cubit المستهدفة 10/10 وتحليل Dart للملفين بلا ملاحظات. بقي التحقق اليدوي من تسجيل مجلد وقراءة ملف ومعاينة diff من نافذة Windows؛ هذا لا يغلقه.
|
||||
- 2026-10-07 — استكمال جرد ملفات التشغيل لتقييم الإصدار التجاري: وسّع `scripts/generate_component_inventory.py` ليضيف binary PDFium وأدلة إشعاراته، وأوزان EasyOCR الموجودة كـCycloneDX components مع SHA-256 ومصدر/معرّف upstream وحالة الترخيص. ولّد SBOM الحالي 157 مكوّنًا: 50 Python و104 Flutter وPDFium واحد ووزنا OCR؛ تطابق ملفا OCR مع MD5 من إعداد EasyOCR 1.7.2، وسُجل غياب `english_g2.pth` المتوقع، مع تجزئة 16 إشعار بناء PDFium. اجتازت اختبارات المولّد 6/6 و`compileall` و`git diff --check`. أُحدّثت `sbom/README.md` و`LICENSE_REVIEW_2026-10.md`. بقي تصنيف 101 إشعار Flutter، تراخيص OCR/PDFium والنماذج، التحقق من مخطط CycloneDX، ومطابقة محتويات إصدار تجاري نهائي؛ هذا تقدم في الجرد وليس موافقة توزيع.
|
||||
- 2026-10-07 — إعادة فحص نافذة Windows: API على `127.0.0.1:8000` ما زالت سليمة (`/health` يعرض Gemma 4 وSQLite والوكيل)، لكن Flutter Web على 5301 متوقف. أداة Computer Use البديلة أعادت قائمة بلا نوافذ Mithqal؛ فحص العمليات وجد PID `55860` من بناء `ui-verify-20261004` مستجيبًا وله HWND، إلى جانب ست عمليات Debug أخرى. طلب فتح الملف التنفيذي الحالي أعاد `launched app did not expose a targetable window: Mithqal AI`؛ لذلك لم أرسل أي نقر أو كتابة إلى نافذة غير مستهدفة. محاولة `flutter --version` بقيت معلقة مع وجود Dart من SDK المعزول (PID 44892)، فأوقفت أمر الفحص وحده ولم أوقف عمليات التطبيق أو Dart. لا تزال تجربة Windows اليدوية غير منجزة؛ يلزم حل تعارض إتاحة النوافذ/قفل SDK أو تشغيل جلسة Debug واحدة معزولة ثم إعادة التحقق.
|
||||
- 2026-10-07 — إعادة فحص بلاغ انقطاع الوكيل بعد 4–5 دقائق: مراجعة الشفرة لم تجد مهلة خمس دقائق على مسار الوكيل؛ Flutter ينتظر حتى 10 دقائق بين أسطر SSE، والخادم يرسل heartbeat كل 15 ثانية، وكل استدعاء نموذج للوكيل محدود بـ600 ثانية. سجل الخدمة يحتوي طلب SSE سابقًا اكتمل خلال 386,998ms بحالة 200. اختبار حي جديد اكتمل بـ`done` خلال 28.3 ثانية مع heartbeat واحد وسجل تدقيق 28,278ms/200، لكنه لا يعيد إنتاج مدة البلاغ. اكتُشف أن PID `14848` كان يشغّل نسخة من مجلد تجربة 2026-09-30 تستخدم مسار `/v1/agent/workspace` القديم، مع وجود نوافذ Debug متعددة. بُني مصدر Flutter الحالي بنجاح خلال 166 ثانية في `%LOCALAPPDATA%\SovereignAI-Starter\windows-dev\flutter_app` وشُغّل PID `18428`؛ سجل الخادم أكد اتصال هذه النسخة بـ`/v1/agent/skills` (200). اختبارات Python المستهدفة 8/8. اختبار Flutter الموجه بقي بلا إخراج وتوقف بعد التحقق من أن استدعاء الاختبار نفسه عالق؛ لم أوقف أي تطبيق. ما زال يلزم اختبار طلب طويل تفاعليًا من نافذة النسخة الحديثة لتحديد سبب بلاغ المستخدم؛ لا يوجد إصلاح جديد مثبت حتى الآن.
|
||||
|
||||
+38
-7
@@ -19,6 +19,7 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
final LocalSettingsStore _settingsStore;
|
||||
Timer? _elapsedTimer;
|
||||
Stopwatch? _requestStopwatch;
|
||||
Future<void>? _loadingAgentSkills;
|
||||
int _requestGeneration = 0;
|
||||
List<ChatMessage> _messagesBeforeRequest = const [];
|
||||
bool _requestAppendedUserMessage = true;
|
||||
@@ -196,14 +197,44 @@ class ChatCubit extends Cubit<ChatState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAgentSkills() async {
|
||||
try {
|
||||
final available = await _api.getAgentSkills();
|
||||
if (!isClosed) emit(state.copyWith(availableSkills: available));
|
||||
} catch (error) {
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(error: 'تعذر تحميل مهارات الوكيل: $error'));
|
||||
Future<void> _loadAgentSkills() {
|
||||
final pending = _loadingAgentSkills;
|
||||
if (pending != null) return pending;
|
||||
final loading = _loadAgentSkillsWithRetry();
|
||||
_loadingAgentSkills = loading;
|
||||
return loading.whenComplete(() {
|
||||
if (identical(_loadingAgentSkills, loading)) {
|
||||
_loadingAgentSkills = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadAgentSkillsWithRetry() async {
|
||||
const delays = [Duration(milliseconds: 300), Duration(milliseconds: 750)];
|
||||
Object? lastError;
|
||||
for (var attempt = 0; attempt <= delays.length; attempt++) {
|
||||
try {
|
||||
final available = await _api.getAgentSkills();
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
availableSkills: available,
|
||||
clearError:
|
||||
state.error?.startsWith('تعذر تحميل مهارات الوكيل:') ?? false,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if (attempt < delays.length) {
|
||||
await Future<void>.delayed(delays[attempt]);
|
||||
if (isClosed) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isClosed) {
|
||||
emit(state.copyWith(error: 'تعذر تحميل مهارات الوكيل: $lastError'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ class _VersionTestApi extends ApiRepository {
|
||||
List<String> deletedFiles = const [];
|
||||
String? registeredProject;
|
||||
String? unregisteredProject;
|
||||
int skillFailuresRemaining = 0;
|
||||
int skillLoadCount = 0;
|
||||
|
||||
@override
|
||||
Future<String> getModelName() async => 'gemma4:e2b';
|
||||
@@ -40,23 +42,30 @@ class _VersionTestApi extends ApiRepository {
|
||||
];
|
||||
|
||||
@override
|
||||
Future<List<AgentSkillDescriptor>> getAgentSkills() async => const [
|
||||
AgentSkillDescriptor(
|
||||
id: 'code_explain',
|
||||
name: 'شرح الكود',
|
||||
description: 'شرح المصدر.',
|
||||
),
|
||||
AgentSkillDescriptor(
|
||||
id: 'code_review',
|
||||
name: 'مراجعة الكود',
|
||||
description: 'مراجعة المصدر.',
|
||||
),
|
||||
AgentSkillDescriptor(
|
||||
id: 'test_plan',
|
||||
name: 'خطة اختبارات',
|
||||
description: 'اقتراح الاختبارات.',
|
||||
),
|
||||
];
|
||||
Future<List<AgentSkillDescriptor>> getAgentSkills() async {
|
||||
skillLoadCount++;
|
||||
if (skillFailuresRemaining > 0) {
|
||||
skillFailuresRemaining--;
|
||||
throw StateError('temporary API failure');
|
||||
}
|
||||
return const [
|
||||
AgentSkillDescriptor(
|
||||
id: 'code_explain',
|
||||
name: 'شرح الكود',
|
||||
description: 'شرح المصدر.',
|
||||
),
|
||||
AgentSkillDescriptor(
|
||||
id: 'code_review',
|
||||
name: 'مراجعة الكود',
|
||||
description: 'مراجعة المصدر.',
|
||||
),
|
||||
AgentSkillDescriptor(
|
||||
id: 'test_plan',
|
||||
name: 'خطة اختبارات',
|
||||
description: 'اقتراح الاختبارات.',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SavedConversation>> listConversations() async => [];
|
||||
@@ -164,6 +173,36 @@ class _MemorySettingsStore extends LocalSettingsStore {
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('agent skills retry a transient API failure during startup', () async {
|
||||
final api = _VersionTestApi()..skillFailuresRemaining = 1;
|
||||
final cubit = ChatCubit(api, settingsStore: LocalSettingsStore.inMemory());
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 450));
|
||||
|
||||
expect(api.skillLoadCount, 2);
|
||||
expect(cubit.state.availableSkills, hasLength(3));
|
||||
expect(cubit.state.error, isNull);
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test(
|
||||
'agent skills preserve an error after all startup retries fail',
|
||||
() async {
|
||||
final api = _VersionTestApi()..skillFailuresRemaining = 3;
|
||||
final cubit = ChatCubit(
|
||||
api,
|
||||
settingsStore: LocalSettingsStore.inMemory(),
|
||||
);
|
||||
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1200));
|
||||
|
||||
expect(api.skillLoadCount, 3);
|
||||
expect(cubit.state.availableSkills, isEmpty);
|
||||
expect(cubit.state.error, contains('تعذر تحميل مهارات الوكيل'));
|
||||
await cubit.close();
|
||||
},
|
||||
);
|
||||
|
||||
test('embedding-only models are not selectable for chat', () async {
|
||||
final api = _VersionTestApi();
|
||||
final cubit = ChatCubit(api, settingsStore: LocalSettingsStore.inMemory());
|
||||
@@ -197,14 +236,8 @@ void main() {
|
||||
]);
|
||||
expect(message, contains('فُهرست 2 ملفًا'));
|
||||
expect(message, contains('4 مقطعًا'));
|
||||
expect(
|
||||
progressUpdates,
|
||||
contains('يفهرس الملف 1/2: README.md…'),
|
||||
);
|
||||
expect(
|
||||
progressUpdates,
|
||||
contains('يفهرس الملف 2/2: main.dart…'),
|
||||
);
|
||||
expect(progressUpdates, contains('يفهرس الملف 1/2: README.md…'));
|
||||
expect(progressUpdates, contains('يفهرس الملف 2/2: main.dart…'));
|
||||
expect(cubit.state.agentProgress, isNull);
|
||||
await subscription.cancel();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user