24 KiB

name description version author license metadata
systematic-debugging Debugging methodology and tools: 4-phase root cause investigation, Python pdb/debugpy, Node.js inspect, Hermes TUI debugging. NO fixes without understanding the problem first. 1.1.0 Hermes Agent (adapted from obra/superpowers) MIT
hermes
tags related_skills
debugging
troubleshooting
problem-solving
root-cause
investigation
test-driven-development
writing-plans
subagent-driven-development

Systematic Debugging

Overview

Random fixes waste time and create new bugs. Quick patches mask underlying issues.

Core principle: ALWAYS find root cause before attempting fixes. Symptom fixes are failure.

Violating the letter of this process is violating the spirit of debugging.

The Iron Law

NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST

If you haven't completed Phase 1, you cannot propose fixes.

When to Use

Use for ANY technical issue:

  • Test failures
  • Bugs in production
  • Unexpected behavior
  • Performance problems
  • Build failures
  • Integration issues

Use this ESPECIALLY when:

  • Under time pressure (emergencies make guessing tempting)
  • "Just one quick fix" seems obvious
  • You've already tried multiple fixes
  • Previous fix didn't work
  • You don't fully understand the issue

Don't skip when:

  • Issue seems simple (simple bugs have root causes too)
  • You're in a hurry (rushing guarantees rework)
  • Someone wants it fixed NOW (systematic is faster than thrashing)

The Four Phases

You MUST complete each phase before proceeding to the next.


Phase 1: Root Cause Investigation

BEFORE attempting ANY fix:

1. Read Error Messages Carefully

  • Don't skip past errors or warnings
  • They often contain the exact solution
  • Read stack traces completely
  • Note line numbers, file paths, error codes

Action: Use read_file on the relevant source files. Use search_files to find the error string in the codebase.

2. Reproduce Consistently

  • Can you trigger it reliably?
  • What are the exact steps?
  • Does it happen every time?
  • If not reproducible → gather more data, don't guess

Action: Use the terminal tool to run the failing test or trigger the bug:

# Run specific failing test
pytest tests/test_module.py::test_name -v

# Run with verbose output
pytest tests/test_module.py -v --tb=long

3. Check Recent Changes — FIRST when user says "it was working before"

When the user explicitly states a feature was working at a known point in time (e.g., "this worked last night", "this was fine yesterday"):

DO NOT immediately investigate the error message or downstream code. Prioritize finding what changed between the known-working state and now.

# Find commits since the known-working time
git log --oneline --since="2026-05-30"

# Changes in the affected directory/module
git log --oneline --since="2026-05-30" -- affected_module/

# Diff of specific files that changed
git diff <known-good-commit>..HEAD -- affected_file.py

Common failure mode: Agent sees a generic error message (e.g., "发送验证码出错"), traces it to a downstream module (e.g., smssend), and starts modifying unrelated code — when the actual cause is a recent change in the calling module.

Rule: User says "it worked before" → git log --since FIRST → review what changed → THEN investigate the error. Do not touch unrelated modules until you've ruled out today's own changes.

For general cases (no "was working" context):

  • What changed that could cause this?
  • Git diff, recent commits
  • New dependencies, config changes

Action:

# Recent commits
git log --oneline -10

# Uncommitted changes
git diff

# Changes in specific file
git log -p --follow src/problematic_file.py | head -100

4. Gather Evidence in Multi-Component Systems

WHEN system has multiple components (API → service → database, CI → build → deploy):

BEFORE proposing fixes, add diagnostic instrumentation:

For EACH component boundary:

  • Log what data enters the component
  • Log what data exits the component
  • Verify environment/config propagation
  • Check state at each layer

Run once to gather evidence showing WHERE it breaks. THEN analyze evidence to identify the failing component. THEN investigate that specific component.

4b. Respect User-Provided Environment Context

WHEN the user states their configuration (e.g., "I set cache to false", "I'm using Python 3.9", "I disabled that feature"):

DO NOT propose fixes for code paths that require a different configuration.

Common failure mode: Agent sees an interesting bug pattern (e.g., race condition) and proposes a fix, but the user's actual environment doesn't execute that code path.

Correct approach:

  1. Take the user's stated configuration seriously
  2. Trace the actual code path they're hitting given their config
  3. If your hypothesis requires different config, discard it and form a new one
  4. Ask clarifying questions if config is ambiguous

Example from RBAC debugging:

  • User reported: "intermittent 403, cache is set to false"
  • Agent hypothesized: race condition in rp_caches = {} before async load
  • Wrong: With cache=false, every request reloads from DB, so the race condition can't cause 403
  • Correct: Trace the cache=false path → found sqlExe returning [] silently → empty cache overwrites valid data

Rule: User's stated config > your assumed config. If your fix doesn't apply to their config, you're debugging the wrong problem.

5. Trace Data Flow

WHEN error is deep in the call stack:

  • Where does the bad value originate?
  • What called this function with the bad value?
  • Keep tracing upstream until you find the source
  • Fix at the source, not at the symptom

Action: Use search_files to trace references:

# Find where the function is called
search_files("function_name(", path="src/", file_glob="*.py")

# Find where the variable is set
search_files("variable_name\\s*=", path="src/", file_glob="*.py")

6. TTFB vs Total Time: Rule Out Network Before Investigating Code

WHEN debugging "slow page" or "slow API response":

Before tracing any code path, measure the server-side vs network split:

curl -o /dev/null -s -w "DNS: %{time_namelookup}s  Connect: %{time_connect}s  TTFB: %{time_starttransfer}s  Total: %{time_total}s\n" URL

Decision tree:

  • TTFB < 100ms, Total > 2s → Network/bandwidth bottleneck. Do NOT investigate server code.
  • TTFB 100ms-1s, Total ≈ TTFB → Server processing bottleneck. Investigate code.
  • TTFB 100ms-1s, Total >> TTFB → Both server and network. Fix server first.

Why this matters: Developers waste hours optimizing server code when the real bottleneck is office→server bandwidth, CDN absence, or large payload over slow connection.

7. Read Instrumented Logs Before Proposing Fixes

WHEN the system emits diagnostic timing logs: Read the actual logged numbers BEFORE forming a hypothesis about where the bottleneck is. The user will correct you if you blame the wrong component.

For example, ahserver's auth_api.py logs timecost with two values:

timecost=client(IP) user_id access /path cost 0.003, (0.001)
                                             ^^^^^    ^^^^^^
                                             TOTAL    AUTH ONLY

If AUTH is 1ms but response is slow, do NOT investigate RBAC. Look at the handler.

General principle: When logs split execution into phases with timestamps, the phase with the smallest delta is NOT the bottleneck. Read the numbers first.

6b. ahserver-Specific Performance Bottleneck Checklist

WHEN debugging slow responses in ahserver-based Sage services, check these known bottlenecks in order. See references/ahserver-performance-bottlenecks.md for full analysis.

6c. ahserver Startup Lifecycle: "no running event loop" Errors

WHEN seeing RuntimeError: no running event loop + coroutine was never awaited in Sage startup:

The init() function in sage.py runs synchronously before the event loop exists. asyncio.create_task() will always fail here.

See references/ahserver-startup-lifecycle.md for the full pattern. Quick fix:

# Instead of:  asyncio.create_task(my_coro())
# Use:         add_startup(my_coro)
from ahserver.configuredServer import add_startup
  1. Sync logging (P0 — most severe): appPublic/log.py uses async queue + persistent file handle (fixed 2026-05-26). If latency persists, verify the fix is deployed (check if log() still calls open_logger()/close_logger() per call).

  2. Static file slow path (FIXED 2026-05-26): ProcessorResource._handle() now has a fast path for .js/.css/.png etc. Verify fix is deployed by checking if static_exts tuple exists at top of _handle().

  3. Redis session loading: Every request triggers await self._redis.get(...). Pure aiohttp has zero session overhead.

  4. Auth cookie decryption: TktAuthentication runs on every request regardless of path.

Critical first step — TTFB vs Total time: Before investigating any code bottleneck, run:

curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s  Total: %{time_total}s\n" URL
  • If TTFB < 100ms but Total > 2s → network bandwidth issue, not code. Stop code investigation.
  • If TTFB > 1s → server-side bottleneck. Proceed with checklist below.

Diagnostic: Compare timecost logs between .js/.css requests and .ui requests. If static files are still slow after fixes 1+2 are deployed, suspect production storage (NFS/cloud disk) or unpatched venv.

Local vs production gap: Local SSD benchmarks show minimal improvement (0.03ms) because SSD flush is ~0.01ms. Production (NFS/cloud disk) flush is 10-50x slower. Local benchmarking proves functional correctness and direction; actual latency improvement only visible on production storage.

Infrastructure change protocol: When modifying ahserver/appPublic (底层), always: (a) analyze functional impact — prove zero impact on .ui/.dspy/CRUD/auth/session; (b) benchmark locally against pure aiohttp before commit; (c) never submit changes that affect upper-layer interfaces. The user has zero tolerance for functional regressions from performance fixes.

7. Detect Tool-Call Repetition Loops

WHEN the agent finds itself issuing the same tool call with identical parameters repeatedly:

This is a meta-level failure mode where the agent's reasoning loop gets stuck. Symptoms:

  • Same tool (e.g., execute_code, search_files) called 3+ times with near-identical parameters
  • Same output returned each time, but agent doesn't change approach
  • Agent keeps appending minor variations to the same query without stepping back
  • Context window fills with duplicate tool calls, wasting tokens and time

Root causes:

  • Agent doesn't recognize it's repeating itself (no self-audit between calls)
  • Missing context about what was already tried (context compaction loses history)
  • Compulsive "try again with slight tweak" pattern instead of strategy change

Escape protocol (enforced after 2 identical calls):

  1. STOP immediately. Do NOT call the tool a 3rd time.
  2. Acknowledge the repetition explicitly. State: "I've called X with the same parameters N times. This is not productive."
  3. Change strategy entirely. Options:
    • Use a different tool (e.g., switch from execute_code to search_files or read_file)
    • Narrow the scope (e.g., search a specific file instead of recursive walk)
    • Broaden the scope (e.g., check the framework source instead of project code)
    • Step back and reason about the problem without tools
    • Ask the user for direction
  4. If the search space is genuinely empty, state that clearly and pivot to a different angle.

Prevention checklist (applied between every tool call):

  • Have I already called this tool with these parameters? → Use a different approach.
  • Did the last call return the same result as the one before? → Change strategy.
  • Am I just tweaking one variable in the same query? → Step back and rethink.

7. Check for Silent Thread Death (Long-Running Processes)

WHEN the symptom is "it works for a while then stops responding to input/commands" but the UI/process appears alive:

This is a classic silent consumer thread death pattern. Many CLI apps use a producer-consumer architecture:

User Input (UI thread) → Queue → Background Consumer Thread → Process

The bug: The consumer thread crashes due to an unhandled exception, but the UI thread keeps running. Inputs pile up in the queue, never consumed. The app looks alive but is deaf.

Diagnostic steps:

# Check if the consumer thread is still alive
import threading
for t in threading.enumerate():
    print(f"{t.name}: alive={t.is_alive()}, daemon={t.daemon}")

Common causes of silent thread death:

Cause Pattern Fix
Unhandled exception except Exception: pass or bare print() that itself fails Wrap entire loop body in except BaseException with logging to stderr/file
Blocking call in consumer thread.join(timeout=N) inside the loop Move blocking calls to separate background threads
Resource leak File descriptors, audio streams, locks held after exception Use try/finally or context managers for all resources
State stuck Boolean flags set but never reset on error path Move flag-setting after resource acquisition, use try/finally for reset

Rule of thumb: Every background consumer thread MUST have a while True + except BaseException wrapper with logging to a file (not print(), which can fail inside patched stdout contexts).

8. Detect Event Loop Saturation (Interactive Applications)

WHEN the symptom is "input becomes unresponsive after X amount of activity" but the process is alive and no threads have died:

This is an event loop saturation pattern — expensive synchronous functions are being called on every render/input cycle, blocking the event loop from processing new input events.

Common in: GUI frameworks (Qt, Tk, GTK), terminal UIs (prompt_toolkit, curses), web frontends with heavy React renders, game loops with expensive per-frame calculations.

The bug: A function assigned to a layout property (height, width, content) or called in an event handler performs expensive computation on every invocation without caching. As input grows or state accumulates, the function becomes slower, eventually saturating the event loop.

Diagnostic steps:

# 1. Identify hot-path functions (called on every render/keystroke)
# In prompt_toolkit: functions assigned to window.height, FormattedTextControl(get_text)
# In React: functions called in render() or useEffect without deps
# In game loops: functions called in the main update loop

# 2. Profile the function
import time
def expensive_function():
    t0 = time.monotonic()
    # ... original code ...
    elapsed = time.monotonic() - t0
    if elapsed > 0.005:  # >5ms is suspicious for a hot path
        print(f"SLOW: {elapsed:.3f}s")
    return result

# 3. Check for imports inside function bodies
# Bad: def func(): from module import thing; return thing()
# Good: from module import thing; def func(): return thing()

# 4. Check for missing fast paths
# Bad: def strip_markers(text): return re.sub(r'pattern', '', text)  # always runs regex
# Good: def strip_markers(text):
#          if 'marker_char' not in text: return text  # fast path
#          return re.sub(r'pattern', '', text)

Common causes:

Cause Pattern Fix
Imports inside hot function from X import Y inside function body Move import to module level or enclosing scope
No caching Full recalculation on every call Add cache keyed on input hash (use lightweight hash for large inputs)
Missing fast path Expensive processing even when no work needed Add early return for common case (empty input, no target patterns)
Overly sensitive heuristics if count > 1 triggers on normal activity Raise threshold based on profiling (e.g., count > 10)

Rule of thumb: Any function called on every render/input cycle should be O(1) with caching, not O(n) with full recalculation. If it's >5ms per call, it's a bottleneck.

Phase 1 Completion Checklist

  • Error messages fully read and understood
  • Issue reproduced consistently
  • Recent changes identified and reviewed
  • Evidence gathered (logs, state, data flow)
  • Problem isolated to specific component/code
  • Root cause hypothesis formed
  • No tool-call repetition loops (same tool + params not called 2+ times)

STOP: Do not proceed to Phase 2 until you understand WHY it's happening.


Phase 2: Pattern Analysis

Find the pattern before fixing:

1. Find Working Examples

  • Locate similar working code in the same codebase
  • What works that's similar to what's broken?

Action: Use search_files to find comparable patterns:

search_files("similar_pattern", path="src/", file_glob="*.py")

2. Compare Against References

  • If implementing a pattern, read the reference implementation COMPLETELY
  • Don't skim — read every line
  • Understand the pattern fully before applying

3. Identify Differences

  • What's different between working and broken?
  • List every difference, however small
  • Don't assume "that can't matter"

4. Understand Dependencies

  • What other components does this need?
  • What settings, config, environment?
  • What assumptions does it make?

Phase 3: Hypothesis and Testing

Scientific method:

1. Form a Single Hypothesis

  • State clearly: "I think X is the root cause because Y"
  • Write it down
  • Be specific, not vague

2. Test Minimally

  • Make the SMALLEST possible change to test the hypothesis
  • One variable at a time
  • Don't fix multiple things at once

3. Verify Before Continuing

  • Did it work? → Phase 4
  • Didn't work? → Form NEW hypothesis
  • DON'T add more fixes on top

4. When You Don't Know

  • Say "I don't understand X"
  • Don't pretend to know
  • Ask the user for help
  • Research more

Phase 4: Implementation

Fix the root cause, not the symptom:

1. Create Failing Test Case

  • Simplest possible reproduction
  • Automated test if possible
  • MUST have before fixing
  • Use the test-driven-development skill

2. Implement Single Fix

  • Address the root cause identified
  • ONE change at a time
  • No "while I'm here" improvements
  • No bundled refactoring

2b. Scan for Identical Patterns

When fixing a field name, variable name, or API mismatch:

After fixing the reported instance, use search_files to find ALL occurrences of the same problematic pattern in the codebase. A field name error rarely appears in just one place.

# After fixing expires_at → expired_date in the reported line:
search_files("expires_at", path="~/repos/dapi")
# Found 3 more occurrences — fix all of them in the same commit

Same for missing function calls (e.g., getID vs getID()), typos, deprecated API names, etc. One reported instance often reveals a systemic typo or migration that was applied incompletely.

3. Verify Fix

# Run the specific regression test
pytest tests/test_module.py::test_regression -v

# Run full suite — no regressions
pytest tests/ -q

4. If Fix Doesn't Work — The Rule of Three

  • STOP.
  • Count: How many fixes have you tried?
  • If < 3: Return to Phase 1, re-analyze with new information
  • If ≥ 3: STOP and question the architecture (step 5 below)
  • DON'T attempt Fix #4 without architectural discussion

5. If 3+ Fixes Failed: Question Architecture

Pattern indicating an architectural problem:

  • Each fix reveals new shared state/coupling in a different place
  • Fixes require "massive refactoring" to implement
  • Each fix creates new symptoms elsewhere

STOP and question fundamentals:

  • Is this pattern fundamentally sound?
  • Are we "sticking with it through sheer inertia"?
  • Should we refactor the architecture vs. continue fixing symptoms?

Discuss with the user before attempting more fixes.

This is NOT a failed hypothesis — this is a wrong architecture.


Red Flags — STOP and Follow Process

If you catch yourself thinking:

  • "Quick fix for now, investigate later"
  • "Just try changing X and see if it works"
  • "Add multiple changes, run tests"
  • "Skip the test, I'll manually verify"
  • "It's probably X, let me fix that"
  • "I don't fully understand but this might work"
  • "Pattern says X but I'll adapt it differently"
  • "Here are the main problems: [lists fixes without investigation]"
  • Proposing solutions before tracing data flow
  • "One more fix attempt" (when already tried 2+)
  • Calling the same tool 2+ times with identical/near-identical parameters — STOP and change strategy
  • Each fix reveals a new problem in a different place

ALL of these mean: STOP. Return to Phase 1.

If 3+ fixes failed: Question the architecture (Phase 4 step 5).

Common Rationalizations

Excuse Reality
"Issue is simple, don't need process" Simple issues have root causes too. Process is fast for simple bugs.
"Emergency, no time for process" Systematic debugging is FASTER than guess-and-check thrashing.
"Just try this first, then investigate" First fix sets the pattern. Do it right from the start.
"I'll write test after confirming fix works" Untested fixes don't stick. Test first proves it.
"Multiple fixes at once saves time" Can't isolate what worked. Causes new bugs.
"Reference too long, I'll adapt the pattern" Partial understanding guarantees bugs. Read it completely.
"I see the problem, let me fix it" Seeing symptoms ≠ understanding root cause.
"One more fix attempt" (after 2+ failures) 3+ failures = architectural problem. Question the pattern, don't fix again.
"Let me try the same search again with a tiny tweak" If same tool + params returned same result, a tiny tweak won't help. Change strategy entirely.

Quick Reference

Phase Key Activities Success Criteria
1. Root Cause Read errors, reproduce, check changes, gather evidence, trace data flow, audit tool-call repetition Understand WHAT and WHY; no repeated tool calls
2. Pattern Find working examples, compare, identify differences Know what's different
3. Hypothesis Form theory, test minimally, one variable at a time Confirmed or new hypothesis
4. Implementation Create regression test, fix root cause, verify Bug resolved, all tests pass

Hermes Agent Integration

Investigation Tools

Use these Hermes tools during Phase 1:

  • search_files — Find error strings, trace function calls, locate patterns
  • read_file — Read source code with line numbers for precise analysis
  • terminal — Run tests, check git history, reproduce bugs
  • web_search/web_extract — Research error messages, library docs

With delegate_task

For complex multi-component debugging, dispatch investigation subagents:

delegate_task(
    goal="Investigate why [specific test/behavior] fails",
    context="""
    Follow systematic-debugging skill:
    1. Read the error message carefully
    2. Reproduce the issue
    3. Trace the data flow to find root cause
    4. Report findings — do NOT fix yet

    Error: [paste full error]
    File: [path to failing code]
    Test command: [exact command]
    """,
    toolsets=['terminal', 'file']
)

With test-driven-development

When fixing bugs:

  1. Write a test that reproduces the bug (RED)
  2. Debug systematically to find root cause
  3. Fix the root cause (GREEN)
  4. The test proves the fix and prevents regression

Real-World Impact

From debugging sessions:

  • Systematic approach: 15-30 minutes to fix
  • Random fixes approach: 2-3 hours of thrashing
  • First-time fix rate: 95% vs 40%
  • New bugs introduced: Near zero vs common

No shortcuts. No guessing. Systematic always wins.