From ba3e49f992dc971f18d414862a7052afa1e16aac Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:37:26 -0700 Subject: [PATCH] query: answer mid-edit at once, and index the no-caller lookups A query whose answer touched a file edited since the graph was built waited up to AXIOMCODE_FRESH_WAIT (default 30 s) for the background rebuild. On a 1,300-file Java repository an agent asking about the code it was editing waited 20-33 s per query, though the answer from the previous graph already marks those rows and names the files it predates. The default is now 0: answer at once; --fresh still waits. impact spent 1.2 s of 1.8 s finding why a method has no callers: the bases written in a type's header were read from type_refs by file and line, and a partial type's parts from symbols by qualified name, neither indexed. Two indexes take impact to 0.5-0.75 s with byte-identical answers on five targets; the graph grows 4%. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../axiomcode/skills/axiomcode/scripts/ax_fresh.py | 14 +++++++------- .../axiomcode/skills/axiomcode/scripts/axiomcode | 4 ++-- .../skills/axiomcode/scripts/axiomcode-index | 4 ++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py index bdb05e8c..ffcd4976 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py @@ -37,7 +37,7 @@ set it for that one query: a read-only answer from the graph as it is, still saying which edits it predates); AXIOMCODE_REFRESH_DEBOUNCE (seconds, default 2) is the quiet window; AXIOMCODE_REFRESH_BATCH (default 3; 1 = off) is how many edits a rebuild waits for once the last build took AXIOMCODE_REFRESH_BATCH_ABOVE seconds (default 20); AXIOMCODE_REFRESH_MAX (default 2, 0 = no cap) is how many background rebuilds run at once on -the machine, the rest queued; AXIOMCODE_FRESH_WAIT (seconds, default 30) is the most a query whose answer touches an edited file +the machine, the rest queued; AXIOMCODE_FRESH_WAIT (seconds, default 0) is the most a query whose answer touches an edited file waits for a refresh expected to finish within it, AXIOMCODE_FRESH=1 (--fresh) makes it wait for the refresh whatever it takes, up to AXIOMCODE_FRESH_MAX (default 600); AXIOMCODE_BUILD_WAIT (seconds, default 900) is how long a query that finds no graph waits for a build that is running rather than starting @@ -1285,11 +1285,11 @@ def note(s, marked=None, named=None, off=False): # ── STALE-WHILE-REVALIDATE (#1595) ────────────────────────────────────────────────────────────────────────────────── # A query never blocks on a refresh by default: it answers from the last good graph and says precisely what is stale. # Every row whose declaration or call site lies in a file edited, added or removed since the graph was built is marked; -# rows from untouched files are exactly as current as the graph and carry nothing. It WAITS only when that matters and -# will pay off: the answer (a row, or the name asked about) touches an edited file, the rebuild is not compiling the -# engine's rules, and the last build of this repository says it will be done within AXIOMCODE_FRESH_WAIT seconds -# (default 30). A fixed wait (10 s) was shorter than every rebuild measured, so it was spent and the answer came from -# the old graph anyway. --fresh (MCP fresh=true) waits for the rebuild whatever it costs, saying so as it goes. +# rows from untouched files are exactly as current as the graph and carry nothing. AXIOMCODE_FRESH_WAIT seconds (default +# 0) lets it wait when the answer (a row, or the name asked about) touches an edited file, the rebuild is not compiling +# the engine's rules, and the last build says it will be done within that. The default was 30: mid-edit, an agent's query +# about the code it was editing waited 20-33 s per ask on a 1,300-file repository, for an answer the marked rows already +# qualified (1.8 s without the wait). --fresh (MCP fresh=true) waits for the rebuild whatever it costs, saying so as it goes. MARK = ' (may be out of date)' _CODE_LINE = re.compile(r'^\s*(\d+ )?\| ') # a line of quoted source (context --source): never marked _TOKEN = re.compile(r'[\w.$+@/-]+') @@ -1497,7 +1497,7 @@ def with_note(n): # the answer as it is, then named = names_in_edits(repo, query_names(verb, argv, repo), stale) touched = touched or bool(named) if touched and not fresh and not s.get('failed') and not hold: - budget = float(os.environ.get('AXIOMCODE_FRESH_WAIT') or 30) + budget = float(os.environ.get('AXIOMCODE_FRESH_WAIT') or 0) left = expected_left(repo) if budget > 0 and not compiling(repo) and (left is None or left <= budget): print(f"waiting for the graph to refresh: this answer touches {', '.join(stale.files[:3])}" + (' …' if len(stale.files) > 3 else '') + diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode index 84766d19..2ebaa16d 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode @@ -149,8 +149,8 @@ case "${ARGS[0]:-}" in -h|--help) if [ -n "$cmd" ] && [ "${#ARGS[@]}" = 1 ]; the # A QUERY NEVER BLOCKS ON A REFRESH BY DEFAULT (#1305, #1595). Before a query verb answers, the graph is checked against # the files it was built from. When one differs the background refresher is started and the answer comes from the last # good graph at once, every row that lies in a changed file marked "(may be out of date)" and one line on stderr naming -# the files. It waits (up to AXIOMCODE_FRESH_WAIT, default 30 s) only when the answer touches a changed file and the last -# build says the refresh will finish within that; never on a compile of the engine's rules. --fresh waits for the +# the files. It waits only when AXIOMCODE_FRESH_WAIT (seconds, default 0) allows, the answer touches a changed file and +# the last build says the refresh will finish within that; never on a compile of the engine's rules. --fresh waits for the # refresh, saying how it goes, and answers from the new graph. ax_fresh.py query does all of it; a fresh graph costs one # check and an exec. The repository is the last argument that is a directory, as the verbs themselves take it, else the # working directory. No graph: nothing here, the verb builds its first one. diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index index c3905612..1ff8a709 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index @@ -738,6 +738,10 @@ CREATE INDEX IF NOT EXISTS ce_callee ON call_edges(callee_method_id); CREATE IND -- lookups by id and by file + line: without them each per-site or per-test query scans the whole table CREATE INDEX IF NOT EXISTS symbols_id ON symbols(id); CREATE INDEX IF NOT EXISTS refs_file_line ON refs(file, line); CREATE INDEX IF NOT EXISTS literals_file_line ON literals(file, line); +-- the bases written in a type's header (graph_sql.library_bases, asked once per type a no-caller reason looks at) and +-- the parts of a partial type (type_parts): without these each ask scanned type_refs / symbols, 1.2 s of a 1.8 s impact +-- on a 1,300-file Java repository → 0.5-0.75 s, answers unchanged +CREATE INDEX IF NOT EXISTS type_refs_file_line ON type_refs(file, line); CREATE INDEX IF NOT EXISTS symbols_qn ON symbols(qualified_name); -- the fields a caller reads: via_base_rows asks per single-target call site, and without the index each ask -- scanned the whole table (65,797 rows × 5,162 sites on an 8,619-file repository = 24 s of a 171 s warm → 1.4 s) CREATE INDEX IF NOT EXISTS fa_caller ON field_access(caller_id);