{"record":{"id":"d5bc7c61b13a095a","repo":"microsoft/semantic-kernel","slug":"task-done-called-too-many-times","errorCode":null,"errorMessage":"task_done() called too many times","messagePattern":"task_done\\(\\) called too many times","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/semantic_kernel/agents/runtime/in_process/queue.py","lineNumber":232,"sourceCode":"    def task_done(self) -> None:\n        \"\"\"Indicate that a formerly enqueued task is complete.\n\n        Used by queue consumers. For each get() used to fetch a task,\n        a subsequent call to task_done() tells the queue that the processing\n        on the task is complete.\n\n        If a join() is currently blocking, it will resume when all items have\n        been processed (meaning that a task_done() call was received for every\n        item that had been put() into the queue).\n\n        shutdown(immediate=True) calls task_done() for each remaining item in\n        the queue.\n\n        Raises ValueError if called more times than there were items placed in\n        the queue.\n        \"\"\"\n        if self._unfinished_tasks <= 0:\n            raise ValueError(\"task_done() called too many times\")\n        self._unfinished_tasks -= 1\n        if self._unfinished_tasks == 0:\n            self._finished.set()\n\n    async def join(self) -> None:\n        \"\"\"Block until all items in the queue have been gotten and processed.\n\n        The count of unfinished tasks goes up whenever an item is added to the\n        queue. The count goes down whenever a consumer calls task_done() to\n        indicate that the item was retrieved and all work on it is complete.\n        When the count of unfinished tasks drops to zero, join() unblocks.\n        \"\"\"\n        if self._unfinished_tasks > 0:\n            await self._finished.wait()\n\n    def shutdown(self, immediate: bool = False) -> None:\n        \"\"\"Shut-down the queue, making queue gets and puts raise QueueShutDown.\n","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/microsoft/semantic-kernel/blob/c028a0c7dc4f0814cdcbaba9d998f187a41197bf/python/semantic_kernel/agents/runtime/in_process/queue.py#L214-L250","documentation":"The Queue tracks unfinished tasks: incremented on put(), decremented on task_done(). If task_done() is called more times than items were put (i.e. _unfinished_tasks <= 0), ValueError is raised. This mirrors the standard asyncio.Queue contract. shutdown(immediate=True) calls task_done() for each remaining item.","triggerScenarios":"Calling task_done() on the queue more times than items were added — e.g. a consumer calling task_done() twice per item, or calling it after shutdown(immediate=True) already drained and accounted for all items. Also triggered by a race where task_done is called for an item that was never put.","commonSituations":"Custom message processing code that calls task_done() manually (it should normally not be called by users — the runtime manages it internally). A bug in the runtime's internal queue management where task_done accounting goes negative.","solutions":["Do not call task_done() on the runtime's internal queue — the runtime manages this internally.","If you have a custom Queue subclass, ensure task_done() is called exactly once per get()/put() pair.","Check for double-processing or duplicate consumer tasks that may call task_done() twice."],"exampleFix":"# before\nitem = await queue.get()\nqueue.task_done()\nqueue.task_done()  # raises on second call\n\n# after\nitem = await queue.get()\nqueue.task_done()  # exactly once per item","handlingStrategy":"validation","validationCode":"# Users should not call task_done() on the runtime's internal queue.\n# If managing your own queue, track task_done calls:\nclass SafeQueue:\n    def __init__(self):\n        self._done_count = 0\n        self._put_count = 0\n    def safe_task_done(self, queue):\n        if self._done_count >= self._put_count:\n            raise ValueError('task_done called too many times')\n        queue.task_done()\n        self._done_count += 1","typeGuard":"null","tryCatchPattern":"try:\n    queue.task_done()\nexcept ValueError:\n    pass  # already accounted for — ignore","preventionTips":["Do not call task_done() on the runtime's internal queue — the runtime manages it.","In custom queue subclasses, call task_done() exactly once per put()/get() pair.","Watch for duplicate consumer tasks that double-process items."],"tags":["agent-runtime","queue","task-done","asyncio","semantic-kernel"],"backgroundTag":null,"analyzedSha":"c028a0c7dc4f0814cdcbaba9d998f187a41197bf","analyzedAt":"2026-08-13T13:48:05.040Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}