#!/usr/bin/env bash # # memory-tree-progress.sh — live progress monitor for the memory_tree pipeline. # # Polls the workspace SQLite DB and prints a one-line snapshot every INTERVAL # seconds: extract jobs done/pending, summaries per level, the currently # claimed job (if any), recent throughput, and a rolling cloud-LLM round-trip # estimate scraped from the core log. Exits cleanly when there is nothing left # to do (no `ready`/`running` jobs other than the daily digest dedupe). # # Optionally triggers a fresh `flush_now` first so the seal cascade picks up # whatever is currently buffered without waiting for the 50k-token threshold. # # Usage: # scripts/memory-tree-progress.sh # monitor only # scripts/memory-tree-progress.sh --flush # flush_now then monitor # scripts/memory-tree-progress.sh --interval 10 # change tick (default 5s) # scripts/memory-tree-progress.sh --log /tmp/x.log # override log path # scripts/memory-tree-progress.sh --once # one snapshot, then exit # # Env: # OPENHUMAN_WORKSPACE — workspace dir (default: derive from active_user.toml) # CORE_BIN — path to openhuman-core (default: target/debug/openhuman-core) # CORE_LOG — core log to scrape for round-trip times (default: /tmp/oh-core.log) # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$REPO_ROOT" INTERVAL=5 DO_FLUSH=0 ONCE=0 CORE_BIN="${CORE_BIN:-target/debug/openhuman-core}" CORE_LOG="${CORE_LOG:-/tmp/oh-core.log}" while [ $# -gt 0 ]; do case "$1" in --flush) DO_FLUSH=1; shift ;; --interval) [ $# -ge 2 ] || { echo "--interval requires a value (seconds)" >&2; exit 2; } [[ "$2" =~ ^[1-9][0-9]*$ ]] || { echo "--interval must be a positive integer" >&2; exit 2; } INTERVAL="$2"; shift 2 ;; --log) [ $# -ge 2 ] || { echo "--log requires a file path" >&2; exit 2; } CORE_LOG="$2"; shift 2 ;; --once) ONCE=1; shift ;; -h|--help) sed -n '2,25p' "$0"; exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done # ── Resolve workspace + DB path ───────────────────────────────────────────── if [ -z "${OPENHUMAN_WORKSPACE:-}" ]; then DEFAULT_DIR="$HOME/.openhuman-staging" [ -d "$DEFAULT_DIR" ] || DEFAULT_DIR="$HOME/.openhuman" ACTIVE_USER_FILE="$DEFAULT_DIR/active_user.toml" if [ -f "$ACTIVE_USER_FILE" ]; then USER_ID=$(awk -F'"' '/user_id/ {print $2; exit}' "$ACTIVE_USER_FILE") OPENHUMAN_WORKSPACE="$DEFAULT_DIR/users/$USER_ID/workspace" fi fi DB="${OPENHUMAN_WORKSPACE:-}/memory_tree/chunks.db" if [ ! -f "$DB" ]; then echo "memory_tree DB not found at: $DB" >&2 echo "Set OPENHUMAN_WORKSPACE to override." >&2 exit 1 fi echo "workspace: $OPENHUMAN_WORKSPACE" echo "db: $DB" echo "log: $CORE_LOG" echo # ── Optional initial flush ────────────────────────────────────────────────── if [ "$DO_FLUSH" = 1 ]; then if [ ! -x "$CORE_BIN" ]; then echo "core binary not found: $CORE_BIN — build with 'cargo build --bin openhuman-core'" >&2 exit 1 fi echo "→ triggering memory_tree.flush_now" # Capture the full output so we can echo it on failure (the call's # exit code is what we gate on; the grep below is just for the # success-path summary). flush_out="$("$CORE_BIN" call --method openhuman.memory_tree_flush_now --params '{}' 2>&1)" || { echo "$flush_out" >&2 echo "flush_now failed; aborting monitor start." >&2 exit 1 } echo "$flush_out" | grep -E '"enqueued"|"stale_buffers"|memory_tree::read' || true echo fi # ── Snapshot helper ───────────────────────────────────────────────────────── q() { sqlite3 "$DB" "$@"; } # Track previous done counts so we can show throughput per tick. PREV_EXTRACT_DONE=0 PREV_SUMMARIES=0 START_TS=$(date +%s) snapshot() { local now ts ext_done ext_ready ext_run sums_l1 sums_l2 sums_l3 sums_l0 \ chunks_pending chunks_admitted chunks_buffered \ running_kind running_started_ms running_age_s \ rt_recent_avg eta_min now=$(date +%s) ts=$(date '+%H:%M:%S') ext_done=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_jobs WHERE kind='extract_chunk' AND status='done';") ext_ready=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_jobs WHERE kind='extract_chunk' AND status='ready';") ext_run=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_jobs WHERE kind='extract_chunk' AND status='running';") sums_l0=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_summaries WHERE level=0;") sums_l1=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_summaries WHERE level=1;") sums_l2=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_summaries WHERE level=2;") sums_l3=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_summaries WHERE level>=3;") chunks_pending=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_chunks WHERE lifecycle_status='pending_extraction';") chunks_admitted=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_chunks WHERE lifecycle_status='admitted';") chunks_buffered=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_chunks WHERE lifecycle_status='buffered';") # Currently-claimed work (any kind), with age in seconds. local now_ms; now_ms=$((now * 1000)) running_row=$(q "SELECT kind, COALESCE(started_at_ms,0) FROM mem_tree_jobs WHERE status='running' ORDER BY started_at_ms ASC LIMIT 1;") if [ -n "$running_row" ]; then running_kind=$(echo "$running_row" | cut -d'|' -f1) running_started_ms=$(echo "$running_row" | cut -d'|' -f2) if [ "$running_started_ms" -gt 0 ] 2>/dev/null; then running_age_s=$(( (now_ms - running_started_ms) / 1000 )) else running_age_s="?" fi else running_kind="-" running_age_s="-" fi # Throughput since last tick. local d_extract=$((ext_done - PREV_EXTRACT_DONE)) local d_sums=$(( (sums_l0 + sums_l1 + sums_l2 + sums_l3) - PREV_SUMMARIES )) PREV_EXTRACT_DONE=$ext_done PREV_SUMMARIES=$((sums_l0 + sums_l1 + sums_l2 + sums_l3)) # Rolling round-trip estimate from the last few cloud responses. rt_recent_avg="?" if [ -f "$CORE_LOG" ]; then rt_recent_avg=$(awk ' /\[memory_tree::chat::cloud\] kind=/ { split($1, a, ":"); start = a[1]*3600 + a[2]*60 + a[3] } /\[memory_tree::chat::cloud\] response/ { split($1, a, ":"); end = a[1]*3600 + a[2]*60 + a[3] if (start > 0) { sum += (end - start); n++ } } END { if (n>0) printf "%.1fs", sum/n; else printf "?" } ' "$CORE_LOG" | tail -c 16) fi eta_min="?" if [ "$d_extract" -gt 0 ] 2>/dev/null; then # ETA based on jobs/tick * INTERVAL seconds. local secs_per=$(( INTERVAL / d_extract )) [ "$secs_per" -lt 1 ] && secs_per=1 eta_min=$(( ext_ready * secs_per / 60 ))m fi # NOTE: source-tree leaves are L1+ (raw chunks are the L0 leaves of the # tree but aren't represented in `mem_tree_summaries`); the L0 row in # `mem_tree_summaries` is only populated by global-tree daily digests. # We surface it here as `digest=` so the bucket name doesn't mislead. printf "%s extract: done=%d pending=%d run=%d (+%d/tick eta~%s) summaries L1=%d L2=%d L3+=%d digest=%d (+%d) chunks: pend=%d adm=%d buf=%d running=%s/%ss cloud_avg=%s\n" \ "$ts" "$ext_done" "$ext_ready" "$ext_run" "$d_extract" "$eta_min" \ "$sums_l1" "$sums_l2" "$sums_l3" "$sums_l0" "$d_sums" \ "$chunks_pending" "$chunks_admitted" "$chunks_buffered" \ "$running_kind" "$running_age_s" "$rt_recent_avg" # Done-condition: nothing pending or running across all kinds (digest_daily # rows are dedupe-suppressed steady state, ignore them). local active_other active_other=$(q "SELECT COALESCE(COUNT(*),0) FROM mem_tree_jobs \ WHERE status IN ('ready','running') \ AND kind <> 'digest_daily';") [ "$active_other" = "0" ] } # ── Loop ──────────────────────────────────────────────────────────────────── if [ "$ONCE" = 1 ]; then snapshot || true exit 0 fi trap 'echo; echo "interrupted."; exit 0' INT while true; do if snapshot; then echo "→ pipeline idle (no ready/running jobs). exiting." exit 0 fi sleep "$INTERVAL" done