diff --git a/.agents/scripts/memory-helper.sh b/.agents/scripts/memory-helper.sh index ead1eb297..995fd5192 100755 --- a/.agents/scripts/memory-helper.sh +++ b/.agents/scripts/memory-helper.sh @@ -1730,6 +1730,147 @@ EOF fi } +####################################### +# Prune repetitive pattern entries by keyword (t230) +# Consolidates entries where the same error/pattern keyword appears +# across many tasks, keeping only a few representative entries +####################################### +cmd_prune_patterns() { + local keyword="" + local dry_run=false + local keep_count=3 + local types="FAILURE_PATTERN,ERROR_FIX,FAILED_APPROACH" + + while [[ $# -gt 0 ]]; do + case "$1" in + --keyword) keyword="$2"; shift 2 ;; + --dry-run) dry_run=true; shift ;; + --keep) keep_count="$2"; shift 2 ;; + --types) types="$2"; shift 2 ;; + *) + if [[ -z "$keyword" ]]; then + keyword="$1" + fi + shift ;; + esac + done + + if [[ -z "$keyword" ]]; then + log_error "Usage: memory-helper.sh prune-patterns [--keep N] [--dry-run]" + log_error "Example: memory-helper.sh prune-patterns clean_exit_no_signal --keep 3" + return 1 + fi + + # Validate keep_count is a positive integer + if ! [[ "$keep_count" =~ ^[1-9][0-9]*$ ]]; then + log_error "--keep must be a positive integer (got: $keep_count)" + return 1 + fi + + init_db + + # Build type filter SQL + local type_sql="" + local IFS=',' + local type_parts=() + read -ra type_parts <<< "$types" + unset IFS + local type_conditions=() + for t in "${type_parts[@]}"; do + type_conditions+=("'$t'") + done + type_sql=$(printf "%s," "${type_conditions[@]}") + type_sql="${type_sql%,}" + + local escaped_keyword="${keyword//"'"/"''"}" + + # Count matching entries + local total_count + total_count=$(db "$MEMORY_DB" \ + "SELECT COUNT(*) FROM learnings WHERE type IN ($type_sql) AND content LIKE '%${escaped_keyword}%';") + + if [[ "$total_count" -le "$keep_count" ]]; then + log_success "Only $total_count entries match '$keyword' (keep=$keep_count). Nothing to prune." + return 0 + fi + + local to_remove=$((total_count - keep_count)) + log_info "Found $total_count entries matching '$keyword' across types ($types)" + log_info "Will keep $keep_count newest entries, remove $to_remove" + + if [[ "$dry_run" == true ]]; then + log_info "[DRY RUN] Would remove $to_remove entries. Entries to keep:" + db "$MEMORY_DB" </dev/null || true + db "$MEMORY_DB" "DELETE FROM learning_relations WHERE supersedes_id IN (SELECT id FROM learnings WHERE $delete_where);" 2>/dev/null || true + db "$MEMORY_DB" "DELETE FROM learning_access WHERE id IN (SELECT id FROM learnings WHERE $delete_where);" + db "$MEMORY_DB" "DELETE FROM learnings WHERE $delete_where;" + + # Rebuild FTS index + db "$MEMORY_DB" "INSERT INTO learnings(learnings) VALUES('rebuild');" + + log_success "Pruned $to_remove repetitive '$keyword' entries (kept $keep_count newest)" + + # Clean up old backups + cleanup_sqlite_backups "$MEMORY_DB" 5 + + return 0 +} + ####################################### # Export memories ####################################### @@ -2110,6 +2251,7 @@ COMMANDS: stats Show memory statistics validate Check for stale/duplicate entries (with detailed reports) prune Remove old entries (auto-runs every 24h on store) + prune-patterns Remove repetitive pattern entries by keyword (e.g., clean_exit_no_signal) dedup Remove exact and near-duplicate entries consolidate Merge similar memories to reduce redundancy export Export all memories @@ -2167,6 +2309,12 @@ PRUNE OPTIONS: --dry-run Show what would be deleted --include-accessed Also prune accessed entries +PRUNE-PATTERNS OPTIONS: + Error/pattern keyword to match (required) + --keep Number of newest entries to keep (default: 3) + --types Comma-separated types to search (default: FAILURE_PATTERN,ERROR_FIX,FAILED_APPROACH) + --dry-run Show what would be removed without deleting + DEDUP OPTIONS: --dry-run Show what would be removed without deleting --exact-only Only remove exact duplicates (skip near-duplicates) @@ -2321,6 +2469,7 @@ main() { stats) cmd_stats ;; validate) cmd_validate ;; prune) cmd_prune "$@" ;; + prune-patterns) cmd_prune_patterns "$@" ;; dedup) cmd_dedup "$@" ;; consolidate) cmd_consolidate "$@" ;; export) cmd_export "$@" ;; diff --git a/.agents/scripts/supervisor-helper.sh b/.agents/scripts/supervisor-helper.sh index b0a255b3b..b879be0d6 100755 --- a/.agents/scripts/supervisor-helper.sh +++ b/.agents/scripts/supervisor-helper.sh @@ -10032,8 +10032,10 @@ store_failure_pattern() { retry) # Only store retry patterns if they indicate a recurring issue # Skip transient ones like rate_limited, timeout, interrupted + # Skip clean_exit_no_signal retries — infrastructure noise (t230) + # The blocked/failed outcomes above still capture the final state case "$outcome_detail" in - rate_limited|timeout|interrupted_sigint|killed_sigkill|terminated_sigterm) + rate_limited|timeout|interrupted_sigint|killed_sigkill|terminated_sigterm|clean_exit_no_signal) return 0 ;; esac @@ -10043,6 +10045,21 @@ store_failure_pattern() { ;; esac + # Rate-limit: skip if 3+ entries with the same outcome_detail exist in last 24h (t230) + # Prevents memory pollution from repetitive infrastructure failures + local recent_count=0 + local escaped_detail + escaped_detail="$(sql_escape "$outcome_detail")" + if [[ -r "$MEMORY_DB" ]]; then + recent_count=$(sqlite3 "$MEMORY_DB" \ + "SELECT COUNT(*) FROM learnings WHERE type = 'FAILURE_PATTERN' AND content LIKE '%${escaped_detail}%' AND created_at > datetime('now', '-1 day');" \ + 2>/dev/null || echo "0") + fi + if [[ "$recent_count" -ge 3 ]]; then + log_info "Skipping failure pattern storage: $outcome_detail already has $recent_count entries in last 24h (t230)" + return 0 + fi + # Look up model tier from task record for pattern routing (t102.3) local model_tier="" local task_model