From 77cad713807ab9bf4e7a7c5285aee4425467b8fa Mon Sep 17 00:00:00 2001 From: Hamza Ayed Date: Sun, 4 Oct 2026 19:17:28 +0300 Subject: [PATCH] surface agent stream audit IDs --- SovereignAI-Starter/ROADMAP.md | 1 + .../lib/core/network/api_repository.dart | 11 ++- .../test/agent_run_result_test.dart | 90 +++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/SovereignAI-Starter/ROADMAP.md b/SovereignAI-Starter/ROADMAP.md index 40f235b..17b068a 100644 --- a/SovereignAI-Starter/ROADMAP.md +++ b/SovereignAI-Starter/ROADMAP.md @@ -5,6 +5,7 @@ ## الحالة الحالية — 2026-10-04 - 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 على المهمة متعددة الأهداف كان ناقصًا وهو خلل جودة منفصل. +- 2026-10-04 — ربط أخطاء بث الوكيل بسجل التدقيق في Flutter: يرفق العميل `X-Agent-Audit-ID` برسالة الخطأ عندما ينتهي SSE قبل `done` أو يصل حدث `error`، ليستطيع المستخدم مطابقة المشكلة مع سجل الخادم دون عرض محتوى خاص. اختبارات Flutter الكاملة 27/27، بينها حالتا انقطاع البث وخطأ SSE، و`flutter analyze --no-pub` بلا ملاحظات. ما زال التحقق المرئي من رسالة الخطأ داخل نافذة Windows مطلوبًا. - 2026-10-04 — إصلاح إجراء المشروع غير المستجيب على Flutter Web: مراجعة `file_selector_web 0.9.4+2` المثبت محليًا أكدت أن `getDirectoryPath()` يعيد `null` على الويب؛ لذلك لم يكن زر تسجيل مجلد كامل قادرًا على فتح المنتقي. حُوّل الإجراء الرئيسي في الويب إلى اختيار ملفات كود/PDF للتحليل، وأُخفي إجراء تسجيل المجلد من قائمة «مشاريعي» عندما لا يوجد مشروع مسجل، مع رسالة تشرح خيار الملفات والمشاريع المسجلة. أزرار تسجيل المجلد بقيت لسطح المكتب، وقائمة المشاريع المسجلة سابقًا تظل قابلة للاختيار. اختبار Flutter الكامل 25/25، `flutter analyze --no-pub` بلا ملاحظات، وبناء Web نجح خلال 171.3 ثانية. لم يتم بعد اختبار إرسال ملف فعلي من نافذة المتصفح ولا التحقق المرئي من Windows؛ لا تُعد هذه الخطوة تسجيل مشروع كاملًا في الويب. diff --git a/SovereignAI-Starter/flutter_app/lib/core/network/api_repository.dart b/SovereignAI-Starter/flutter_app/lib/core/network/api_repository.dart index 72df7bf..67acce5 100644 --- a/SovereignAI-Starter/flutter_app/lib/core/network/api_repository.dart +++ b/SovereignAI-Starter/flutter_app/lib/core/network/api_repository.dart @@ -567,6 +567,11 @@ class ApiRepository { final streamed = await client .send(request) .timeout(const Duration(minutes: 10)); + final auditId = streamed.headers['x-agent-audit-id']; + String withAuditId(String message) => + auditId == null || auditId.isEmpty + ? message + : '$message\nمعرّف تتبع الطلب: $auditId'; if (streamed.statusCode < 200 || streamed.statusCode >= 300) { final response = await http.Response.fromStream(streamed); _checkStatus(response); @@ -601,9 +606,11 @@ class ApiRepository { eventName = null; } } - if (streamError != null) throw Exception(streamError); + if (streamError != null) throw Exception(withAuditId(streamError)); final data = resultData; - if (data == null) throw Exception('انقطع اتصال البث قبل اكتمال الوكيل.'); + if (data == null) { + throw Exception(withAuditId('انقطع اتصال البث قبل اكتمال الوكيل.')); + } return AgentRunResult.fromApiData(data); } finally { if (identical(_activeRequestClient, client)) _activeRequestClient = null; diff --git a/SovereignAI-Starter/flutter_app/test/agent_run_result_test.dart b/SovereignAI-Starter/flutter_app/test/agent_run_result_test.dart index 5246846..7f5b7d3 100644 --- a/SovereignAI-Starter/flutter_app/test/agent_run_result_test.dart +++ b/SovereignAI-Starter/flutter_app/test/agent_run_result_test.dart @@ -8,6 +8,96 @@ import 'package:flutter_app/core/network/api_repository.dart'; void main() { group('ApiRepository.runAgent SSE stream', () { + test('includes the audit ID when the server closes before done', () async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + server.listen((request) async { + await utf8.decoder.bind(request).join(); + request.response.headers.contentType = ContentType.json; + if (request.uri.path == '/v1/auth/local-session') { + request.response.write( + jsonEncode({ + 'access_token': 'test-session', + 'user': {'mode': 'local'}, + }), + ); + } else if (request.uri.path == '/v1/agent/run/stream') { + request.response.statusCode = HttpStatus.ok; + request.response.headers.set('x-agent-audit-id', 'trace-closed-42'); + request.response.headers.contentType = ContentType( + 'text', + 'event-stream', + charset: 'utf-8', + ); + request.response.write( + 'event: progress\ndata: {"message":"بدأ الوكيل"}\n\n', + ); + } else { + request.response.statusCode = HttpStatus.notFound; + } + await request.response.close(); + }); + + try { + final repository = ApiRepository( + baseUrl: 'http://${server.address.address}:${server.port}', + sessionTokenStore: EphemeralSessionTokenStore(), + ); + await expectLater( + repository.runAgent('اختبار انقطاع البث'), + throwsA( + predicate( + (error) => error.toString().contains('trace-closed-42'), + ), + ), + ); + } finally { + await server.close(force: true); + } + }); + + test('includes the audit ID for an SSE error event', () async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + server.listen((request) async { + await utf8.decoder.bind(request).join(); + request.response.statusCode = HttpStatus.ok; + if (request.uri.path == '/v1/auth/local-session') { + request.response.headers.contentType = ContentType.json; + request.response.write( + jsonEncode({ + 'access_token': 'test-session', + 'user': {'mode': 'local'}, + }), + ); + } else if (request.uri.path == '/v1/agent/run/stream') { + request.response.headers.set('x-agent-audit-id', 'trace-error-17'); + request.response.headers.contentType = ContentType( + 'text', + 'event-stream', + charset: 'utf-8', + ); + request.response.write( + 'event: error\ndata: {"message":"تعذر إكمال المهمة"}\n\n', + ); + } + await request.response.close(); + }); + + try { + final repository = ApiRepository( + baseUrl: 'http://${server.address.address}:${server.port}', + sessionTokenStore: EphemeralSessionTokenStore(), + ); + await expectLater( + repository.runAgent('اختبار خطأ البث'), + throwsA( + predicate((error) => error.toString().contains('trace-error-17')), + ), + ); + } finally { + await server.close(force: true); + } + }); + test( 'keeps progress through heartbeat and returns the done result', () async {