Diagnose interrupted agent SSE streams
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
- 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 الموجه بقي بلا إخراج وتوقف بعد التحقق من أن استدعاء الاختبار نفسه عالق؛ لم أوقف أي تطبيق. ما زال يلزم اختبار طلب طويل تفاعليًا من نافذة النسخة الحديثة لتحديد سبب بلاغ المستخدم؛ لا يوجد إصلاح جديد مثبت حتى الآن.
|
||||
- 2026-10-07 — تحسين تشخيص انقطاع SSE في عميل Flutter: عند استثناء أثناء قراءة البث أو انتهائه قبل `done`، تعرض الرسالة مدة الاتصال وآخر نوع حدث وعدد heartbeats ومعرّف التدقيق من ترويسة الخادم. أضيف تأكيد لهذه البيانات إلى اختبار الإغلاق المبكر. اجتاز `agent_run_result_test.dart` المجموعة 5/5، واجتاز `dart analyze` الملف والاختبار المستهدفين بلا ملاحظات بعد حذف علامة null assertion الزائدة. أُعيد بناء نسخة Windows خلال 87.6 ثانية وشُغّل PID `12352`؛ سجل API أكد اتصالها بمسار مهارات الوكيل (200). هذا يحسن تشخيص الانقطاع التالي ولا يثبت زوال سببه؛ ما زال اختبار مهمة طويلة من واجهة Windows مطلوبًا، ولا يوجد طلب تشغيل فاشل حديث في سجل التدقيق.
|
||||
- 2026-10-04 — تجربة حية لدورة عمل الوكيل مع مشروع مؤقت: سجلت مجلدًا تجريبيًا محليًا عبر `/v1/agent/projects`، ثم أرسل Flutter-equivalent request إلى `/v1/agent/run/stream` مع `README.md` محددًا. قرأ Gemma 4 الملف، ثم استدعى `propose_file_change` وأعاد diff لتحديثه خلال 77 ثانية مع heartbeat وHTTP 200؛ بقي الملف دون تطبيق التعديل. أُلغي تسجيل المشروع وحُذف المجلد المؤقت بعد التحقق. هذا يثبت مسار API من تسجيل المشروع حتى معاينة التغيير، ولا يغلق اختبار اختيار المجلد وعرض diff داخل نافذة Windows.
|
||||
- 2026-10-04 — متابعة انقطاع الوكيل بعد 4–5 دقائق: أُعيد تشغيل API التطبيق على `127.0.0.1:8000` مع قاعدة البيانات الحالية، وأكد `/health` أن Gemma 4 سليمة. طلب بحث حي عبر `/v1/agent/run/stream` اكتمل بـHTTP 200 وحدث `done` بعد 387 ثانية، مع 15 heartbeat؛ لم ينقطع SSE خلال أكثر من 6 دقائق. مهلة عميل Flutter للبث هي 10 دقائق، والـheartbeat يعيد ضبط مهلة خمول تدفق الأسطر، لذا لا تفسر مدة 4–5 دقائق وحدها الانقطاع. اجتازت مجموعة اختبارات الوكيل/البث/المهل 32/32، ونجح `compileall` و`git diff --check`. أُضيف fallback يعرض مقتطفًا لكل هدف بحث صريح إذا توقف Gemma قبل إكمال تنسيق الجواب، مع اختبار انحدار ناجح. بقي التحقق اليدوي في نافذة Windows غير ممكن هذه الجولة: أداة التحكم المرئي انتهت مهلتها بعد محاولات الاستعادة، والنوافذ المكتشفة كانت من مسار بناء قديم؛ لم يُرسل أي نقر إليها. نتيجة البث الحي تثبت صمود API في هذا الطلب، لكنها لا تعزل سبب كل انقطاع سابق قد يكون من العميل/النافذة أو من تغير تشغيل الخدمة.
|
||||
- 2026-10-04 — متابعة فقد اتصال الوكيل بعد 4–5 دقائق: أُعيد اختبار `/v1/agent/run/stream` بطلب بحث فعلي متعدد الأهداف. على الخدمة الموجودة في 8000 اكتمل طلب خلال 163.6 ثانية مع 12 heartbeat وحدث `done`، ولم ينقطع النقل؛ لكن Gemma أعادت ملخصًا ناقصًا، وهذا قصور جودة منفصل. كشف سجل التدقيق القديم أن مدد البث تُحفظ عند وصول الترويسة قبل انتهاء جسم SSE، فلا تمثل عمر الطلب ولا تكشف قطع العميل. عُدّل middleware لقياس المدة حتى اكتمال البث، وتسجيل `499` داخليًا عند إلغاء/إغلاق العميل و`500` عند استثناء أثناء الإرسال، من دون تسجيل محتوى السؤال أو الإجابة. اختبارات بث الوكيل والإلغاء والمهارات نجحت 31/31. شُغلت نسخة من الشفرة الحالية على 18813 بقاعدة اختبار معزولة؛ طلب Gemma حي استمر 219.6 ثانية، تلقى 12 heartbeat وانتهى بـ`done`، وسجل التدقيق 219,465ms وHTTP 200. بعدها أُعيد تشغيل API على 8000 (PID 41184) مع الاحتفاظ بقاعدة SQLite الحالية، وأعاد `/health` الحالة `ok`. اختبار الوكيل النهائي على 8000 انتهى بـ`done` بعد 23.98 ثانية مع heartbeat؛ سجل التدقيق الجديد طابق المدة 23,975ms وحالة 200. لم يتكرر فقد الاتصال في الاختبارات الحية؛ ما زال اختبار Flutter التفاعلي المطوّل مفتوحًا، وجواب Gemma على المهمة متعددة الأهداف كان ناقصًا وهو خلل جودة منفصل.
|
||||
|
||||
@@ -568,10 +568,16 @@ class ApiRepository {
|
||||
.send(request)
|
||||
.timeout(const Duration(minutes: 10));
|
||||
final auditId = streamed.headers['x-agent-audit-id'];
|
||||
final streamStopwatch = Stopwatch()..start();
|
||||
var heartbeatCount = 0;
|
||||
var lastEventName = 'بدء الاتصال';
|
||||
String withAuditId(String message) =>
|
||||
auditId == null || auditId.isEmpty
|
||||
? message
|
||||
: '$message\nمعرّف تتبع الطلب: $auditId';
|
||||
String streamDiagnostics() =>
|
||||
'بعد ${streamStopwatch.elapsed.inSeconds} ثانية؛ آخر حدث: '
|
||||
'$lastEventName؛ عدد النبضات: $heartbeatCount.';
|
||||
if (streamed.statusCode < 200 || streamed.statusCode >= 300) {
|
||||
final response = await http.Response.fromStream(streamed);
|
||||
_checkStatus(response);
|
||||
@@ -579,37 +585,50 @@ class ApiRepository {
|
||||
String? eventName;
|
||||
Map<String, dynamic>? resultData;
|
||||
String? streamError;
|
||||
await for (final line in streamed.stream
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.timeout(const Duration(minutes: 10))) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventName = line.substring(7).trim();
|
||||
} else if (line.startsWith('data: ')) {
|
||||
final data = jsonDecode(line.substring(6)) as Map<String, dynamic>;
|
||||
switch (eventName) {
|
||||
case 'progress':
|
||||
final message = data['message']?.toString();
|
||||
if (message != null) onProgress?.call(message);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
onProgress?.call('الوكيل ما زال يعمل، أنتظر النموذج المحلي...');
|
||||
break;
|
||||
case 'done':
|
||||
resultData = data;
|
||||
break;
|
||||
case 'error':
|
||||
streamError =
|
||||
data['message']?.toString() ?? 'تعذر تنفيذ مهمة الوكيل.';
|
||||
break;
|
||||
try {
|
||||
await for (final line in streamed.stream
|
||||
.transform(utf8.decoder)
|
||||
.transform(const LineSplitter())
|
||||
.timeout(const Duration(minutes: 10))) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventName = line.substring(7).trim();
|
||||
lastEventName = eventName;
|
||||
} else if (line.startsWith('data: ')) {
|
||||
final data = jsonDecode(line.substring(6)) as Map<String, dynamic>;
|
||||
switch (eventName) {
|
||||
case 'progress':
|
||||
final message = data['message']?.toString();
|
||||
if (message != null) onProgress?.call(message);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
heartbeatCount++;
|
||||
onProgress?.call('الوكيل ما زال يعمل، أنتظر النموذج المحلي...');
|
||||
break;
|
||||
case 'done':
|
||||
resultData = data;
|
||||
break;
|
||||
case 'error':
|
||||
streamError =
|
||||
data['message']?.toString() ?? 'تعذر تنفيذ مهمة الوكيل.';
|
||||
break;
|
||||
}
|
||||
eventName = null;
|
||||
}
|
||||
eventName = null;
|
||||
}
|
||||
} on Exception catch (error) {
|
||||
throw Exception(
|
||||
withAuditId(
|
||||
'انقطع اتصال بث الوكيل ${streamDiagnostics()} نوع الخطأ: '
|
||||
'${error.runtimeType}.',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (streamError != null) throw Exception(withAuditId(streamError));
|
||||
final data = resultData;
|
||||
if (data == null) {
|
||||
throw Exception(withAuditId('انقطع اتصال البث قبل اكتمال الوكيل.'));
|
||||
throw Exception(
|
||||
withAuditId('انتهى البث قبل اكتمال الوكيل ${streamDiagnostics()}'),
|
||||
);
|
||||
}
|
||||
return AgentRunResult.fromApiData(data);
|
||||
} finally {
|
||||
|
||||
@@ -45,8 +45,16 @@ void main() {
|
||||
await expectLater(
|
||||
repository.runAgent('اختبار انقطاع البث'),
|
||||
throwsA(
|
||||
predicate(
|
||||
(error) => error.toString().contains('trace-closed-42'),
|
||||
allOf(
|
||||
predicate(
|
||||
(error) => error.toString().contains('trace-closed-42'),
|
||||
),
|
||||
predicate(
|
||||
(error) => error.toString().contains('آخر حدث: progress'),
|
||||
),
|
||||
predicate(
|
||||
(error) => error.toString().contains('عدد النبضات: 0'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user