light-heart-labs--dreamserver
9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
339 行
12 KiB
Bash
可执行文件
339 行
12 KiB
Bash
可执行文件
#!/bin/bash
|
|
# memory-shepherd.sh — Periodic memory baseline reset for LLM agents
|
|
# Usage: memory-shepherd.sh [agent-name|all]
|
|
set -euo pipefail
|
|
|
|
# Cross-platform stat helpers — BSD (Darwin) / GNU diverge on -c vs -f
|
|
_stat_mtime() {
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
stat -f %m "$1"
|
|
else
|
|
stat -c %Y "$1"
|
|
fi
|
|
}
|
|
|
|
_stat_size() {
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
stat -f %z "$1"
|
|
else
|
|
stat -c %s "$1"
|
|
fi
|
|
}
|
|
|
|
TIMESTAMP=$(date '+%Y-%m-%d_%H%M')
|
|
LOCKFILE=/tmp/memory-shepherd.lock
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# ── Logging ────────────────────────────────────────────────────────────
|
|
|
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [memory-shepherd] $1"; }
|
|
|
|
# ── Lock Management ────────────────────────────────────────────────────
|
|
|
|
cleanup_lock() { rm -f "$LOCKFILE"; }
|
|
trap cleanup_lock EXIT
|
|
|
|
if [ -f "$LOCKFILE" ]; then
|
|
lock_age=$(( $(date +%s) - $(_stat_mtime "$LOCKFILE") ))
|
|
if [ "$lock_age" -gt 120 ]; then
|
|
log "WARN: Stale lock (age: ${lock_age}s) — removing"
|
|
rm -f "$LOCKFILE"
|
|
else
|
|
log "Another reset running (lock age: ${lock_age}s) — exiting"
|
|
exit 0
|
|
fi
|
|
fi
|
|
echo $$ > "$LOCKFILE"
|
|
|
|
# ── Config Parser ──────────────────────────────────────────────────────
|
|
|
|
declare -A CONFIG
|
|
AGENTS=()
|
|
|
|
find_config() {
|
|
if [ -n "${MEMORY_SHEPHERD_CONF:-}" ] && [ -f "$MEMORY_SHEPHERD_CONF" ]; then
|
|
echo "$MEMORY_SHEPHERD_CONF"
|
|
elif [ -f "$SCRIPT_DIR/memory-shepherd.conf" ]; then
|
|
echo "$SCRIPT_DIR/memory-shepherd.conf"
|
|
elif [ -f "/etc/memory-shepherd/memory-shepherd.conf" ]; then
|
|
echo "/etc/memory-shepherd/memory-shepherd.conf"
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
parse_config() {
|
|
local conf_file="$1"
|
|
local section=""
|
|
while IFS= read -r line; do
|
|
# Strip comments and whitespace
|
|
line="${line%%#*}"
|
|
line="${line#"${line%%[![:space:]]*}"}"
|
|
line="${line%"${line##*[![:space:]]}"}"
|
|
[[ -z "$line" ]] && continue
|
|
|
|
if [[ "$line" =~ ^\[([a-zA-Z0-9_-]+)\]$ ]]; then
|
|
section="${BASH_REMATCH[1]}"
|
|
if [[ "$section" != "general" ]]; then
|
|
AGENTS+=("$section")
|
|
fi
|
|
continue
|
|
fi
|
|
|
|
if [[ "$line" =~ ^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$ ]]; then
|
|
CONFIG["${section}.${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}"
|
|
fi
|
|
done < "$conf_file"
|
|
}
|
|
|
|
cfg() {
|
|
local key="${1}.${2}"
|
|
local default="${3:-}"
|
|
echo "${CONFIG[$key]:-$default}"
|
|
}
|
|
|
|
# ── Load Config ────────────────────────────────────────────────────────
|
|
|
|
CONF_FILE=$(find_config) || {
|
|
echo "ERROR: No config file found." >&2
|
|
echo "Searched: \$MEMORY_SHEPHERD_CONF, ./memory-shepherd.conf, /etc/memory-shepherd/memory-shepherd.conf" >&2
|
|
exit 1
|
|
}
|
|
|
|
parse_config "$CONF_FILE"
|
|
log "Loaded config from $CONF_FILE (${#AGENTS[@]} agents)"
|
|
|
|
# ── Global Settings ────────────────────────────────────────────────────
|
|
|
|
BASELINE_DIR=$(cfg general baseline_dir "$SCRIPT_DIR/baselines")
|
|
ARCHIVE_DIR=$(cfg general archive_dir "$SCRIPT_DIR/archives")
|
|
MAX_MEMORY_SIZE=$(cfg general max_memory_size 16384)
|
|
ARCHIVE_RETENTION_DAYS=$(cfg general archive_retention_days 30)
|
|
SEPARATOR=$(cfg general separator "---")
|
|
MIN_BASELINE_SIZE=$(cfg general min_baseline_size 500)
|
|
|
|
# Resolve relative paths against script directory
|
|
[[ "$BASELINE_DIR" != /* ]] && BASELINE_DIR="$SCRIPT_DIR/$BASELINE_DIR"
|
|
[[ "$ARCHIVE_DIR" != /* ]] && ARCHIVE_DIR="$SCRIPT_DIR/$ARCHIVE_DIR"
|
|
|
|
# ── Reset Functions ────────────────────────────────────────────────────
|
|
|
|
reset_agent() {
|
|
local agent="$1"
|
|
local memory_file="$2"
|
|
local baseline="$3"
|
|
local archive_dir="$4"
|
|
|
|
if [ ! -f "$baseline" ]; then
|
|
log "ERROR: Baseline missing for $agent at $baseline — skipping"
|
|
return 0
|
|
fi
|
|
|
|
local baseline_size
|
|
baseline_size=$(_stat_size "$baseline")
|
|
if [ "$baseline_size" -lt "$MIN_BASELINE_SIZE" ]; then
|
|
log "ERROR: Baseline for $agent is suspiciously small (${baseline_size} bytes, min: ${MIN_BASELINE_SIZE}) — skipping"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$memory_file" ]; then
|
|
log "WARN: No memory file for $agent — creating from baseline"
|
|
cp "$baseline" "$memory_file"
|
|
return 0
|
|
fi
|
|
|
|
local memory_size
|
|
memory_size=$(_stat_size "$memory_file")
|
|
if [ "$memory_size" -gt "$MAX_MEMORY_SIZE" ]; then
|
|
log "WARN: Memory file for $agent is ${memory_size} bytes (over limit) — forcing reset"
|
|
fi
|
|
|
|
local separator_line
|
|
separator_line=$(grep -n "^${SEPARATOR}$" "$memory_file" | tail -1 | cut -d: -f1 || echo "")
|
|
|
|
if [ -n "$separator_line" ]; then
|
|
local total_lines
|
|
total_lines=$(wc -l < "$memory_file")
|
|
if [ "$separator_line" -lt "$total_lines" ]; then
|
|
local scratch
|
|
scratch=$(tail -n +"$(($separator_line + 1))" "$memory_file" | sed '/^## Scratch Notes/d' | sed '/^[[:space:]]*$/d')
|
|
if [ -n "$scratch" ]; then
|
|
mkdir -p "$archive_dir"
|
|
local archive_file="$archive_dir/${TIMESTAMP}.md"
|
|
printf "# %s scratch notes — archived %s\n\n%s\n" "$agent" "$TIMESTAMP" "$scratch" > "$archive_file"
|
|
log "Archived scratch notes for $agent ($(echo "$scratch" | wc -l) lines)"
|
|
else
|
|
log "No scratch notes for $agent"
|
|
fi
|
|
else
|
|
log "No scratch notes for $agent"
|
|
fi
|
|
else
|
|
mkdir -p "$archive_dir"
|
|
cp "$memory_file" "$archive_dir/${TIMESTAMP}-full-backup.md"
|
|
log "WARN: No separator in $agent memory — backed up entire file before reset"
|
|
fi
|
|
|
|
local tmpfile="${memory_file}.reset-tmp"
|
|
cp "$baseline" "$tmpfile"
|
|
mv -f "$tmpfile" "$memory_file"
|
|
log "Reset $agent MEMORY.md to baseline (${baseline_size} bytes)"
|
|
}
|
|
|
|
reset_remote_agent() {
|
|
local agent="$1"
|
|
local remote_host="$2"
|
|
local remote_user="$3"
|
|
local remote_memory="$4"
|
|
local baseline="$5"
|
|
local archive_dir="$6"
|
|
|
|
if [ ! -f "$baseline" ]; then
|
|
log "ERROR: Baseline missing for $agent at $baseline — skipping"
|
|
return 0
|
|
fi
|
|
|
|
local baseline_size
|
|
baseline_size=$(_stat_size "$baseline")
|
|
if [ "$baseline_size" -lt "$MIN_BASELINE_SIZE" ]; then
|
|
log "ERROR: Baseline for $agent is suspiciously small (${baseline_size} bytes, min: ${MIN_BASELINE_SIZE}) — skipping"
|
|
return 0
|
|
fi
|
|
|
|
# Fetch current memory from remote
|
|
local tmpfile="/tmp/memory-shepherd-${agent}-current.md"
|
|
if ! scp -q "${remote_user}@${remote_host}:${remote_memory}" "$tmpfile" 2>/dev/null; then
|
|
log "WARN: No memory file for $agent on $remote_host — pushing baseline"
|
|
scp -q "$baseline" "${remote_user}@${remote_host}:${remote_memory}"
|
|
return 0
|
|
fi
|
|
|
|
local memory_size
|
|
memory_size=$(_stat_size "$tmpfile")
|
|
if [ "$memory_size" -gt "$MAX_MEMORY_SIZE" ]; then
|
|
log "WARN: Memory file for $agent is ${memory_size} bytes (over limit) — forcing reset"
|
|
fi
|
|
|
|
# Extract and archive scratch notes locally
|
|
local separator_line
|
|
separator_line=$(grep -n "^${SEPARATOR}$" "$tmpfile" | tail -1 | cut -d: -f1 || echo "")
|
|
|
|
if [ -n "$separator_line" ]; then
|
|
local total_lines
|
|
total_lines=$(wc -l < "$tmpfile")
|
|
if [ "$separator_line" -lt "$total_lines" ]; then
|
|
local scratch
|
|
scratch=$(tail -n +"$(($separator_line + 1))" "$tmpfile" | sed '/^## Scratch Notes/d' | sed '/^[[:space:]]*$/d')
|
|
if [ -n "$scratch" ]; then
|
|
mkdir -p "$archive_dir"
|
|
local archive_file="$archive_dir/${TIMESTAMP}.md"
|
|
printf "# %s scratch notes — archived %s\n\n%s\n" "$agent" "$TIMESTAMP" "$scratch" > "$archive_file"
|
|
log "Archived scratch notes for $agent ($(echo "$scratch" | wc -l) lines)"
|
|
else
|
|
log "No scratch notes for $agent"
|
|
fi
|
|
else
|
|
log "No scratch notes for $agent"
|
|
fi
|
|
else
|
|
mkdir -p "$archive_dir"
|
|
cp "$tmpfile" "$archive_dir/${TIMESTAMP}-full-backup.md"
|
|
log "WARN: No separator in $agent memory — backed up entire file before reset"
|
|
fi
|
|
|
|
# Push baseline to remote
|
|
scp -q "$baseline" "${remote_user}@${remote_host}:${remote_memory}"
|
|
log "Reset $agent MEMORY.md on $remote_host to baseline (${baseline_size} bytes)"
|
|
rm -f "$tmpfile"
|
|
}
|
|
|
|
# ── Dispatch ───────────────────────────────────────────────────────────
|
|
|
|
process_agent() {
|
|
local agent="$1"
|
|
|
|
local memory_file
|
|
memory_file=$(cfg "$agent" memory_file "")
|
|
local baseline_name
|
|
baseline_name=$(cfg "$agent" baseline "")
|
|
local archive_subdir
|
|
archive_subdir=$(cfg "$agent" archive_subdir "$agent")
|
|
local archive_path="$ARCHIVE_DIR/$archive_subdir"
|
|
|
|
if [ -z "$baseline_name" ]; then
|
|
log "ERROR: No baseline defined for agent '$agent' — skipping"
|
|
return 0
|
|
fi
|
|
|
|
local baseline_path="$BASELINE_DIR/$baseline_name"
|
|
local remote_host
|
|
remote_host=$(cfg "$agent" remote_host "")
|
|
|
|
if [ -n "$remote_host" ]; then
|
|
local remote_user
|
|
remote_user=$(cfg "$agent" remote_user "$(whoami)")
|
|
local remote_memory
|
|
remote_memory=$(cfg "$agent" remote_memory "")
|
|
|
|
if [ -z "$remote_memory" ]; then
|
|
log "ERROR: remote_host set for '$agent' but no remote_memory — skipping"
|
|
return 0
|
|
fi
|
|
|
|
reset_remote_agent "$agent" "$remote_host" "$remote_user" "$remote_memory" "$baseline_path" "$archive_path"
|
|
else
|
|
if [ -z "$memory_file" ]; then
|
|
log "ERROR: No memory_file defined for agent '$agent' — skipping"
|
|
return 0
|
|
fi
|
|
|
|
reset_agent "$agent" "$memory_file" "$baseline_path" "$archive_path"
|
|
fi
|
|
}
|
|
|
|
# ── Main ───────────────────────────────────────────────────────────────
|
|
|
|
TARGET="${1:-all}"
|
|
|
|
if [ "$TARGET" = "all" ]; then
|
|
if [ ${#AGENTS[@]} -eq 0 ]; then
|
|
log "No agents defined in config"
|
|
exit 0
|
|
fi
|
|
for agent in "${AGENTS[@]}"; do
|
|
process_agent "$agent"
|
|
done
|
|
else
|
|
# Check if the agent exists in config
|
|
found=false
|
|
for agent in "${AGENTS[@]}"; do
|
|
if [ "$agent" = "$TARGET" ]; then
|
|
found=true
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$found" = false ]; then
|
|
echo "ERROR: Unknown agent '$TARGET'" >&2
|
|
echo "Available agents: ${AGENTS[*]}" >&2
|
|
echo "Usage: memory-shepherd.sh [agent-name|all]" >&2
|
|
exit 1
|
|
fi
|
|
|
|
process_agent "$TARGET"
|
|
fi
|
|
|
|
# ── Cleanup ────────────────────────────────────────────────────────────
|
|
|
|
# Purge old archives
|
|
find "$ARCHIVE_DIR" -name "*.md" -mtime +"$ARCHIVE_RETENTION_DAYS" -delete 2>/dev/null || true
|
|
|
|
# Rotate log if over 1MB
|
|
local_log="$ARCHIVE_DIR/reset.log"
|
|
if [ -f "$local_log" ] && [ "$(_stat_size "$local_log" 2>/dev/null || echo 0)" -gt 1048576 ]; then
|
|
mv "$local_log" "$local_log.old"
|
|
log "Rotated log file"
|
|
fi
|
|
|
|
log "Done"
|