diff --git a/examples/openclaw-plugin/memory-ranking.ts b/examples/openclaw-plugin/memory-ranking.ts index 3e7b615df..33ef56954 100644 --- a/examples/openclaw-plugin/memory-ranking.ts +++ b/examples/openclaw-plugin/memory-ranking.ts @@ -222,6 +222,7 @@ export function pickMemoriesForInjection( items: FindResultItem[], limit: number, queryText: string, + scoreThreshold: number = 0, ): FindResultItem[] { if (items.length === 0 || limit <= 0) { return []; @@ -254,6 +255,12 @@ export function pickMemoriesForInjection( if (used.has(item.uri)) { continue; } + // Respect score threshold when supplementing leaf memories with + // non-leaf items. Without this check, low-scoring memories bypass + // the threshold configured in recallScoreThreshold (see #1106). + if (clampScore(item.score) < scoreThreshold) { + continue; + } picked.push(item); } return picked; diff --git a/openviking/session/compressor.py b/openviking/session/compressor.py index 46b56ed41..f4e77a7f3 100644 --- a/openviking/session/compressor.py +++ b/openviking/session/compressor.py @@ -249,6 +249,16 @@ async def _merge_into_existing( target_memory.set_vectorize(Vectorize(text=payload.content)) await self._index_memory(target_memory, ctx, change_type="modified") return True + except FileNotFoundError: + logger.warning( + "Target memory %s no longer exists — removing orphaned reference", target_memory.uri + ) + # Clean up vector record for the missing file so it's not retried + try: + await self.vikingdb.delete_uris(ctx, [target_memory.uri]) + except Exception: + pass + return False except Exception as e: logger.error(f"Failed to merge memory {target_memory.uri}: {e}") return False diff --git a/openviking/utils/process_lock.py b/openviking/utils/process_lock.py index 77a5d7d56..88e2955c3 100644 --- a/openviking/utils/process_lock.py +++ b/openviking/utils/process_lock.py @@ -37,26 +37,37 @@ def _is_pid_alive(pid: int) -> bool: return False try: os.kill(pid, 0) - return True except ProcessLookupError: return False except PermissionError: # Process exists but we can't signal it. - return True + pass except (OSError, SystemError): if sys.platform == "win32": - # On Windows, os.kill(pid, 0) raises OSError for stale or invalid - # PIDs instead of ProcessLookupError. In some environments it can - # also bubble up as SystemError from the underlying Win32 wrapper. - # Common failures include: - # - WinError 87 "The parameter is incorrect" - # - WinError 11 "An attempt was made to load a program with an - # incorrect format" - # Treat these as "not alive" so stale lock files are correctly - # reclaimed on Windows. return False raise + # PID exists, but on Linux PIDs are recycled. Verify this is actually + # an OpenViking process by checking /proc/{pid}/cmdline to avoid false + # positives from PID reuse (see issue #1088). + if sys.platform.startswith("linux"): + try: + with open(f"/proc/{pid}/cmdline", "rb") as f: + cmdline = f.read().decode("utf-8", errors="replace").lower() + if "openviking" not in cmdline and "openviking-server" not in cmdline: + logger.info( + "PID %d is alive but not an OpenViking process (cmdline: %.100s). " + "Assuming stale lock from recycled PID.", + pid, + cmdline[:100], + ) + return False + except OSError: + # /proc not available or process exited between kill and open + pass + + return True + def acquire_data_dir_lock(data_dir: str) -> str: """Acquire an advisory PID lock on *data_dir*.