From 934f39abe69d8e7553cec518f9d7f4df4fdd3574 Mon Sep 17 00:00:00 2001 From: fabi Date: Tue, 16 Jun 2026 18:57:43 +0200 Subject: [PATCH] fix(vision-manager): LOW watermark must not idle-stop a running vision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The memory-yield LOW (start-inhibit) watermark forced pending=0, which the idle-debounce path then read as 'no work' and used to idle-stop a RUNNING, actively-analyzing vision once the box floated past LOW (idle stack + LLM ≈ 80% on this Pi) — so vision was killed mid-analysis at ~80%, not the 92% HIGH watermark. Split the two levers: LOW now sets a mem_block_start flag that only holds off STARTING a stopped vision; only the HIGH active stop (or a genuinely drained queue) stops a running one. Observed live: vision now holds through the 79–81% band and completes pages. Co-Authored-By: Claude Opus 4.8 --- vision-manager/manager.sh | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/vision-manager/manager.sh b/vision-manager/manager.sh index 2313df9..00dbd4d 100644 --- a/vision-manager/manager.sh +++ b/vision-manager/manager.sh @@ -200,19 +200,27 @@ while true; do analysis_off=0 fi - # Memory-yield gates (before the backlog logic). The active stop handles a - # running vision the idle path can't; the inhibit forces pending=0 so the - # existing idle-debounce path keeps it down (same shape as the crawl mutex). + # Memory-yield: two SEPARATE levers, never conflated. + # - active stop (HIGH watermark): mem_check_and_stop SIGTERMs a RUNNING + # vision when the host is critically short — the ONLY thing that stops a + # vision that is actively working. + # - start block (LOW watermark / cooldown): holds off STARTING a stopped + # vision when memory is already tight, via the mem_block_start flag. + # It must NOT zero `pending`: doing so made the idle-debounce path read + # "no work" and idle-stop a vision mid-analysis once the box floated past + # LOW (idle stack + LLM ≈ 80% here). LOW is a START gate, not a stop + # threshold. mem_check_and_stop running="$(vision_running)" # refresh: the active stop may have just flipped it + mem_block_start=0 if [ "$(mem_yield_active)" = "1" ]; then + mem_block_start=1 if [ "$mem_inhibited" != "1" ]; then - log "memory-yield: inhibiting vision start (used >= ${MEM_LOW_WATERMARK_PCT}% or cooldown active)" + log "memory-yield: blocking vision START (used >= ${MEM_LOW_WATERMARK_PCT}% or cooldown); a running vision keeps working until the ${MEM_HIGH_WATERMARK_PCT}% active stop" mem_inhibited=1 fi - pending=0 elif [ "$mem_inhibited" = "1" ]; then - log "memory-yield: pressure cleared — start inhibit lifted" + log "memory-yield: pressure cleared — start block lifted" mem_inhibited=0 fi @@ -225,7 +233,12 @@ while true; do if [ "$pending" -gt 0 ]; then idle_for=0 if [ "$running" != "true" ]; then - if [ "$RESPECT_CRAWL_MUTEX" != "0" ] && [ "$(crawl_running)" != "0" ]; then + if [ "$mem_block_start" = "1" ]; then + # Memory pressure / cooldown — hold off STARTING (flip already logged). + # A RUNNING vision never reaches here; idle_for stayed 0 above because + # pending>0, so a working vision is never idle-stopped under pressure. + : + elif [ "$RESPECT_CRAWL_MUTEX" != "0" ] && [ "$(crawl_running)" != "0" ]; then # Log once per deferral episode, not every poll, so a long crawl # doesn't flood the log. if [ "$crawl_deferred" != "1" ]; then