From 48eddaab32cf96a08c47d02c34ca9ac8c94b6fd3 Mon Sep 17 00:00:00 2001 From: Viper <3088663751@qq.com> Date: Fri, 31 Jul 2026 21:47:52 +0800 Subject: [PATCH] fix(s09): re-locate memory injection turn by identity after compaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `memory_turn` was computed once as `len(messages) - 1` *before* the s08 compression pipeline runs. `snip_compact` / `compact_history` / `reactive_compact` shorten or rebuild the message list, so the index drifts: the guard `memory_turn < len(messages)` then fails and the memory injection is silently skipped — precisely when compaction triggers (long conversations), i.e. when the memory system is supposed to matter most. Fix: capture the user-turn *object* and re-locate it by identity after each compaction step via the new `_find_memory_turn`, falling back to the last str-content user turn if the query was summarized away. No change to the s08 compaction functions — the bug was entirely in agent_loop. --- s09_memory/code.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/s09_memory/code.py b/s09_memory/code.py index 117c8359a..2c9a04bee 100644 --- a/s09_memory/code.py +++ b/s09_memory/code.py @@ -332,6 +332,25 @@ def consolidate_memories(): except Exception: pass +def _find_memory_turn(messages, memory_msg): + """Locate the user turn to inject memory into, by object identity. + + s09 bug fix: the compression pipeline mutates `messages` (snip/micro/compact + all shift indices), so a precomputed position drifts and silently drops the + memory injection. Re-find the turn by identity after each compaction step. + Only str-content user turns are returned, so callers can safely + string-concatenate memory onto the content. + """ + if memory_msg is not None: + for i, m in enumerate(messages): + if m is memory_msg: + if isinstance(m.get("content"), str): + return i + break # object present but no longer a plain-text turn + for i in range(len(messages) - 1, -1, -1): + if messages[i].get("role") == "user" and isinstance(messages[i].get("content"), str): + return i + return None # Build SYSTEM with memory index def build_system() -> str: @@ -584,7 +603,12 @@ def agent_loop(messages: list): reactive_retries = 0 # s09: inject relevant memory content into the current user turn memories_content = load_memories(messages) - memory_turn = len(messages) - 1 if messages and isinstance(messages[-1].get("content"), str) else None + # s09: capture the *object* of the current user turn, not its index. The + # compression pipeline below mutates `messages` and shifts every later index, + # so a precomputed position would drift (and silently drop the memory). We + # re-locate it by identity after each compaction step instead. + memory_msg = messages[-1] if (messages + and isinstance(messages[-1].get("content"), str)) else None # s09: build system once per user turn; memory is updated after the loop returns system = build_system() @@ -602,9 +626,13 @@ def agent_loop(messages: list): print("[auto compact]") messages[:] = compact_history(messages) + # s09: re-locate the user turn by *identity* — compaction may have + # shifted its index. Returns a str-content user turn, or None. + memory_turn = _find_memory_turn(messages, memory_msg) + try: request_messages = messages - if memories_content and memory_turn is not None and memory_turn < len(messages): + if memories_content and memory_turn is not None: request_messages = messages.copy() request_messages[memory_turn] = { **messages[memory_turn],