{"record":{"id":"acca4d000fa0e2b8","repo":"666ghj/MiroFish","slug":"zep-batch-batch-id-processing-is-unconfirmed","errorCode":null,"errorMessage":"Zep batch {batch_id} processing is unconfirmed","messagePattern":"Zep batch (.+?) processing is unconfirmed","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"backend/app/services/graph_builder.py","lineNumber":555,"sourceCode":"                        f\"Zep batch {batch_id} acknowledged {len(item_details or [])} \"\n                        f\"of {len(items)} items\"\n                    )\n            for item in item_details:\n                episode_uuid = getattr(item, \"episode_uuid\", None)\n                if episode_uuid:\n                    episode_uuids.append(episode_uuid)\n\n        try:\n            self.client.batch.process(batch_id=batch_id)\n        except Exception as error:\n            # A process response can be lost after the server accepted it.\n            # Reconcile with a safe GET instead of issuing a second POST.\n            summary = call_zep_read_with_retry(\n                lambda: self.client.batch.get(batch_id=batch_id),\n                operation_name=f\"reconcile batch {batch_id}\",\n            )\n            if getattr(summary, \"status\", None) in {None, \"draft\"}:\n                raise RuntimeError(\n                    f\"Zep batch {batch_id} processing is unconfirmed\"\n                ) from error\n\n        return BatchSubmission(\n            batch_id=batch_id,\n            operation_id=operation_id,\n            episode_uuids=episode_uuids,\n            item_count=total_chunks,\n        )\n\n    @staticmethod\n    def validate_batch_chunks(chunks: List[str], *, batch_size: int = 350) -> None:\n        \"\"\"Validate every Batch API limit before the first Cloud mutation.\"\"\"\n\n        if not chunks:\n            raise ValueError(\"At least one text chunk is required\")\n        if not 1 <= batch_size <= 350:\n            raise ValueError(\"batch_size must be between 1 and 350\")","sourceCodeStart":537,"sourceCodeEnd":573,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/backend/app/services/graph_builder.py#L537-L573","documentation":"RuntimeError at the end of submit_document_batch: client.batch.process(batch_id) raised, and since a process POST can succeed server-side while the response is lost, the code reconciles with a safe GET (batch.get with read retry). If the fetched status is None or 'draft', the batch was never actually queued for processing — all items were submitted but processing is unconfirmed, and re-POSTing process is deliberately avoided (the comment notes a second POST is unsafe).","triggerScenarios":"process() times out or hits a retryable network error; Zep accepted items but the process trigger never landed, leaving status='draft'; Zep Cloud incident where both POST and GET degrade.","commonSituations":"Long-running builds whose final process call crosses a proxy/gateway timeout; transient 5xx from Zep; the GET reconcile racing Zep's own state update so status still reads draft.","solutions":["Manually trigger processing once in the Zep console (or via a single careful re-POST) and confirm status leaves draft, then resume the build.","If status was merely lagging, retry the build/resume flow after a short wait — the GET will then show processing/processed.","Raise the client timeout for the process call and lengthen read-retry patience in call_zep_read_with_retry.","Check network intermediaries (nginx, service mesh) for idle timeouts that kill the long process response."],"exampleFix":"# before\nexcept Exception as error:\n    summary = call_zep_read_with_retry(lambda: self.client.batch.get(batch_id=batch_id), operation_name=f\"reconcile batch {batch_id}\")\n    if getattr(summary, \"status\", None) in {None, \"draft\"}:\n        raise RuntimeError(f\"Zep batch {batch_id} processing is unconfirmed\") from error\n\n# after - poll the GET briefly before declaring unconfirmed, include status\nexcept Exception as error:\n    for delay in (1, 3, 5):\n        summary = call_zep_read_with_retry(lambda: self.client.batch.get(batch_id=batch_id), operation_name=f\"reconcile batch {batch_id}\")\n        if getattr(summary, \"status\", None) not in {None, \"draft\"}:\n            break\n        time.sleep(delay)\n    else:\n        raise RuntimeError(\n            f\"Zep batch {batch_id} processing is unconfirmed (status={getattr(summary, 'status', None)!r}); \"\n            \"trigger processing once via the Zep console, then resume\"\n        ) from error","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    submission = builder.submit_document_batch(graph_id, chunks, batch_size=bs)\nexcept RuntimeError as e:\n    if 'processing is unconfirmed' in str(e):\n        summary = call_zep_read_with_retry(lambda: builder.client.batch.get(batch_id=project.zep_batch_id), operation_name='recheck')\n        if getattr(summary, 'status', None) in {None, 'draft'}:\n            raise  # operator must trigger process once in Zep console, then resume\n        submission = BatchSubmission(batch_id=project.zep_batch_id, operation_id=project.zep_batch_operation_id, episode_uuids=[], item_count=len(chunks))\n    else:\n        raise","preventionTips":["Set generous timeouts on the batch.process call — its response can be slow and loss is costly.","After any process failure, always reconcile with a GET (never a second process POST) before deciding.","Persist batch identity so an unconfirmed process can be finished manually in the Zep console and the build resumed.","Exclude Zep API calls from gateway idle timeouts in the deployment config."],"tags":["backend","python","zep","batch","processing","unconfirmed-operation"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}