{"record":{"id":"7ef8c9714c3f8475","repo":"bytedance/deer-flow","slug":"memory-update-queue-is-full-depth-len-self-item","errorCode":null,"errorMessage":"memory update queue is full (depth {len(self._items)} >= {max_depth}); non-signal update for thread {thread_id} rejected","messagePattern":"memory update queue is full \\(depth (.+?) >= (.+?)\\); non-signal update for thread (.+?) rejected","errorType":"exception","errorClass":"QueueFull","httpStatus":null,"severity":"warning","filePath":"backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/queue.py","lineNumber":188,"sourceCode":"        # Emergency (bypass) and normal updates coexist: the match key includes\n        # ``bypass_watermark`` so a summarization flush (bypass=True) never\n        # replaces a pending normal update for the same (thread, user, agent) --\n        # replacing it would drop the normal update's un-extracted tail, which\n        # the next turn may not re-feed if the user stops. Both are processed\n        # independently instead.\n        existing = next(\n            (c for c in self._items if queue_key(c.thread_id, c.user_id, c.agent_name) == key and c.bypass_watermark == bypass_watermark),\n            None,\n        )\n        # Backpressure: once depth reaches the cap, reject NEW non-signal normal\n        # items. Same-key updates merge (do not grow depth); signal-bearing items\n        # and emergency (bypass) flushes are always admitted. Signals capture\n        # important memories, and the emergency path captures messages about to\n        # be removed by summarization -- neither can be re-fed next turn, so\n        # shedding them under load would lose data rather than merely defer it.\n        max_depth = self._config.queue_max_depth\n        if max_depth > 0 and not bypass_watermark and not signals and existing is None and len(self._items) >= max_depth:\n            raise QueueFull(f\"memory update queue is full (depth {len(self._items)} >= {max_depth}); non-signal update for thread {thread_id} rejected\")\n\n        # Merge by signal union: a signal seen on any update for this key stays.\n        merged_signals = signals | (existing.signals if existing is not None else frozenset())\n        context = ConversationContext(\n            thread_id=thread_id,\n            messages=messages,\n            agent_name=agent_name,\n            user_id=user_id,\n            trace_id=trace_id,\n            signals=merged_signals,\n            bypass_watermark=bypass_watermark,\n        )\n        if existing is not None:\n            self._items = [c for c in self._items if not (queue_key(c.thread_id, c.user_id, c.agent_name) == key and c.bypass_watermark == bypass_watermark)]\n        self._items.append(context)\n        return context\n\n    def _reset_timer(self) -> None:","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/queue.py#L170-L206","documentation":"QueueFull from deermem's memory update queue: backpressure logic rejects NEW non-signal, non-bypass updates once queue depth reaches queue_max_depth. Same-key updates merge (never grow depth), and signal-bearing or emergency (bypass_watermark) items are always admitted because they cannot be re-fed next turn — only deferrable normal updates are shed.","triggerScenarios":"Enqueueing a normal (no signals, no bypass) memory update while len(queue._items) >= config.queue_max_depth (>0), for a thread/user/agent key not already present in the queue.","commonSituations":"Memory consumer/writer slower than the agent producing updates (long-running high-traffic threads); queue_max_depth set too low for bursty traffic; background writer stalled (SQLite lock, IO hang) so depth monotonically grows.","solutions":["Catch QueueFull at the enqueue call site and drop/defer the update (it is deferrable by design — next turn re-feeds messages).","Raise queue_max_depth in the deermem config if bursts are legitimate.","Investigate why depth stays at cap: check the drain/consumer loop health (DB locks, slow writes) and fix the stall.","If the update matters, attach a signal or use the emergency path rather than retrying the normal enqueue."],"exampleFix":"# before\nqueue.enqueue(ctx)  # raises QueueFull under load\n\n# after\ntry:\n    queue.enqueue(ctx)\nexcept QueueFull:\n    logger.warning(\"memory update deferred for thread %s\", ctx.thread_id)\n    # safe to skip: non-signal updates are re-fed next turn","handlingStrategy":"try-catch","validationCode":"def can_enqueue(queue, bypass=False, signals=frozenset()) -> bool:\n    if signals or bypass:\n        return True  # always admitted\n    return len(queue._items) < queue._config.queue_max_depth or queue._config.queue_max_depth <= 0","typeGuard":null,"tryCatchPattern":"from deermem.core.queue import QueueFull\ntry:\n    queue.enqueue(ctx)\nexcept QueueFull:\n    # by design: non-signal updates are deferrable — drop and log, next turn re-feeds\n    logger.warning('memory update deferred (queue full) thread=%s', ctx.get('thread_id'))","preventionTips":["Never retry a rejected normal enqueue in a tight loop — it is shed load, not a transient error.","Monitor queue depth; sustained cap means the consumer is stalled, fix that.","Size queue_max_depth against peak turns-per-minute times worst drain latency.","Route genuinely important updates through signals or the emergency path."],"tags":["backpressure","queue","deermem","memory","load"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}