# عدم التكرار

> أعد أي طلب POST أو DELETE بأمان عبر Idempotency-Key، وأعد إرسال رسائل المحادثة بأمان عبر client_message_id.

الشبكات تتعطل. قد ينتهي وقت الطلب بعد أن ينجز الخادم العمل فعلًا، فتنشئ إعادة المحاولة العادية جلسة ثانية أو رسالة ثانية أو إجابة ثانية. يمنحك K-Agent أداتين لجعل إعادة المحاولة آمنة.

## `Idempotency-Key`

أرسل مفتاحًا فريدًا مع أي طلب `POST` أو `DELETE`. وإذا أعدت المحاولة بالمفتاح نفسه تحصل على النتيجة المحفوظة بدل تنفيذ ثانٍ:

**curl**

```bash
curl https://api.k-agent.kerneltics.com/v1/sessions \
  -H "Authorization: Bearer $KAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a0e-8d3b-4c55-9a77-1e2f3d4c5b6a" \
  -d '{"agent": "store-assistant", "external_id": "order-8812", "input": "وين طلبي؟"}'
```

**JavaScript**

```js
const key = crypto.randomUUID(); // create once, reuse for every retry of this request
const RETRY_409 = new Set(['idempotency_in_progress', 'session_busy', 'session_queue_full']);

async function createSessionWithRetry(body, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch('https://api.k-agent.kerneltics.com/v1/sessions', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.KAGENT_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': key,
        },
        body: JSON.stringify(body),
      });
      if (res.status === 409) {
        const { error } = await res.clone().json();
        if (!RETRY_409.has(error.code)) return res;
      } else if (res.status < 500 && res.status !== 429) {
        return res;
      }
    } catch {
      // network error: retry with the same key
    }
    await new Promise((r) => setTimeout(r, 500 * 2 ** i));
  }
  throw new Error('Gave up after retries');
}
```

**Python**

```python
import os, time, uuid, requests

key = str(uuid.uuid4())  # create once, reuse for every retry of this request
RETRY_409 = {"idempotency_in_progress", "session_busy", "session_queue_full"}

def create_session_with_retry(body: dict, attempts: int = 4) -> requests.Response:
    for i in range(attempts):
        try:
            r = requests.post(
                "https://api.k-agent.kerneltics.com/v1/sessions",
                headers={"Authorization": f"Bearer {os.environ['KAGENT_API_KEY']}",
                         "Idempotency-Key": key},
                json=body, timeout=120,
            )
            if r.status_code == 409:
                if r.json()["error"]["code"] not in RETRY_409:
                    return r
            elif r.status_code < 500 and r.status_code != 429:
                return r
        except requests.ConnectionError:
            pass  # network error: retry with the same key
        time.sleep(0.5 * 2 ** i)
    raise RuntimeError("Gave up after retries")
```

### القواعد

- **النطاق.** المفتاح محصور في مشروعك، وبيانات الاعتماد التي أرسلته، والمسار (الطريقة ونمط المسار). والمفتاح نفسه على مسار آخر مفتاح مختلف.
- **الطول.** من 1 إلى 255 حرفًا (وإلا `400 invalid_idempotency_key`). ومعرّف UUID v4 مثالي.
- **مدة الحفظ.** تُحفظ النتائج **24 ساعة**. وبعدها يمكن استخدام المفتاح من جديد.
- **الإعادة** ترجع الحالة والجسم الأصليين، مع الترويسة `Idempotent-Replayed: true`.
- **المفتاح نفسه مع طلب مختلف** — جسم آخر أو معرّف آخر في المسار — يعيد `422 idempotency_key_reused`. وتعتمد المقارنة على الجلسة بعد تحديدها، فمخاطبتها بمعرّف `sess_…` أو بـ `external_id` تُعدّ الطلب نفسه.
- **ما زال قيد التنفيذ.** إذا لم ينتهِ الطلب الأول تعيد إعادة المحاولة `409 idempotency_in_progress`. انتظر وأعد المحاولة بالمفتاح نفسه.
- **أخطاء الخادم لا تُحفظ.** خطأ `5xx` يقع قبل بدء أي عمل يمكن إعادته بالمفتاح نفسه وسيُنفَّذ من جديد.

### الطلبات التي تبدأ تشغيلًا

في `ask`، و`POST /v1/sessions` مع `input`، والرسائل، و`submit_tool_outputs`، يرتبط المفتاح **بالتشغيل** فور وجوده:

- تعيد إعادة المحاولة الحالة **الحالية** للتشغيل بالشكل المعتاد لاستجابة نقطة النهاية — فإن كان قد انتهى منذ ذلك الحين تحصل على التشغيل المنتهي؛
- إعادة طلب بثّ تعيد ربطك ببث ذلك التشغيل **من بدايته**؛
- لا يكون `409 idempotency_in_progress` ممكنًا إلا في اللحظة القصيرة قبل وجود التشغيل.

### الطلبات التي تعيد سرًا

إنشاء مفتاح API أو استبداله، وإصدار رمز عميل، وبدء جلسة ودجت، وإنشاء نقطة استقبال ويب هوك أو تغيير سرها، وقراءة سر توقيع الأدوات — كلها تعيد سرًا **مرة واحدة**. وإعاداتها ترجع المورد نفسه مع السر بقيمة `null` و`"secret_redacted": true` — فالسر نفسه لا يُحفظ لإعادته أبدًا.

## `client_message_id`

لرسائل المحادثة آلية ثانية أبسط: أعطِ كل رسالة معرّفك الخاص.

```bash
curl https://api.k-agent.kerneltics.com/v1/sessions/order-8812/messages \
  -H "Authorization: Bearer $KAGENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"input": "شكرًا!", "client_message_id": "wa-msg-77121"}'
```

- المعرّف `client_message_id` نفسه في الجلسة نفسها يعيد `{session, message, run}` الأصلية بالحالة `200` مع `Idempotent-Replayed: true` — دون رسالة ثانية ودون إجابة ثانية.
- المعرّف نفسه مع نص مختلف يعيد `409 client_message_id_conflict`.
- لا تنتهي صلاحيته ما دامت الجلسة موجودة، ما يجعله الأداة المناسبة للقنوات التي تعيد تسليم الرسائل بعد ساعات (الويب هوك من منصات المراسلة، وتطبيقات الجوال التي تعيد الإرسال بعد استعادة الاتصال).

استخدم **الاثنين** متى استطعت: `client_message_id` لمنع تكرار الرسالة نفسها، و`Idempotency-Key` لجعل طلب HTTP آمنًا عند إعادته.
