3535
3636Environment: AXIOMCODE_NO_REFRESH=1 turns every trigger off (a query verb's --no-refresh, and the MCP tools' refresh=false,
3737set 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)
38- is the quiet window; AXIOMCODE_REFRESH_MAX (default 2, 0 = no cap) is how many background rebuilds run at once on
38+ is the quiet window; AXIOMCODE_REFRESH_BATCH (default 3; 1 = off) is how many edits a rebuild waits for once the last build
39+ 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
3940the machine, the rest queued; AXIOMCODE_FRESH_WAIT (seconds, default 30) is the most a query whose answer touches an edited file
4041waits for a refresh expected to finish within it, AXIOMCODE_FRESH=1 (--fresh) makes it wait for the refresh whatever it
4142takes, up to AXIOMCODE_FRESH_MAX (default 600); AXIOMCODE_BUILD_WAIT
@@ -935,14 +936,51 @@ def refresher_running(repo):
935936
936937def running_line (repo , s ): return started_line (repo , s , running = True )
937938
938- def hook_kick (repo , trigger , session = '' ):
939+ # EDITS ARE BATCHED WHERE A REBUILD IS SLOW. Every edit started the refresher, which rebuilt after a 2 s quiet window: on
940+ # a repository whose build takes 5 s that keeps the graph current for free, but where it takes a minute (1,300 Java
941+ # files: 48-73 s cold, and 50-88 s again after a one- or two-file edit, since the solve is whole-program) an agent
942+ # editing every few seconds kept a rebuild running back to back. So once the last build took AXIOMCODE_REFRESH_BATCH_ABOVE
943+ # seconds (default 20), an edit tool only counts, and the AXIOMCODE_REFRESH_BATCH-th edit (default 3; 1 = off) starts the
944+ # rebuild; a shell command waits for the next edit or checkpoint. The end of a turn, a prompt, a session start, the
945+ # timer and every query still refresh at once, so the graph is current whenever the agent stops or asks.
946+ EDIT_TOOLS = ('Edit' , 'Write' , 'MultiEdit' , 'NotebookEdit' )
947+
948+ def batch_size ():
949+ try : return max (1 , int (os .environ .get ('AXIOMCODE_REFRESH_BATCH' ) or 3 ))
950+ except ValueError : return 3
951+
952+ def batching (repo ):
953+ """the edits a rebuild waits for here: batch_size() when the last build was slow, else 1 (every edit rebuilds)"""
954+ try : above = float (os .environ .get ('AXIOMCODE_REFRESH_BATCH_ABOVE' ) or 20 )
955+ except ValueError : above = 20.0
956+ secs , _ = build_seconds (repo )
957+ return batch_size () if secs is not None and secs >= above else 1
958+
959+ def batch_due (repo ):
960+ n = batching (repo )
961+ return n <= 1 or read_state (repo ).get ('edits' , 0 ) >= n
962+
963+ def batch_held (repo , event , tool ):
964+ """True when this hook event only counts toward the next rebuild; any other trigger makes the next one due"""
965+ n = batching (repo )
966+ if n <= 1 : return False
967+ if event != 'PostToolUse' :
968+ write_state (repo , edits = n ); return False
969+ if tool not in EDIT_TOOLS : return True
970+ k = read_state (repo ).get ('edits' , 0 ) + 1
971+ write_state (repo , edits = k )
972+ return k < n
973+
974+ def hook_kick (repo , trigger , session = '' , event = '' , tool = '' ):
939975 """what a hook (and the MCP server's timer) runs in place of kick: '' after starting the refresher as kick does, or,
940976 for a graph another axiomcode built (engine_change), nothing started and one line to say so, once per session and
941- difference ('' when it was said already). A graph a newer axiomcode built is never rebuilt anyway (newer_build)"""
977+ difference ('' when it was said already). A graph a newer axiomcode built is never rebuilt anyway (newer_build).
978+ `event` and `tool` name the hook event and the tool it followed, for batching edits (batch_held)"""
942979 if not enabled (repo ): return ''
943980 held = engine_change (repo )
944981 if not held :
945- kick (repo , trigger ); return ''
982+ if not batch_held (repo , event , tool ): kick (repo , trigger )
983+ return ''
946984 key = f"{ session } |{ held } "
947985 if read_state (repo ).get ('hook_held' ) == key : return ''
948986 write_state (repo , hook_held = key )
@@ -1060,8 +1098,9 @@ def worker(repo):
10601098 if not _flock (fd , False ): return 0 # single flight
10611099 debounce = float (os .environ .get ('AXIOMCODE_REFRESH_DEBOUNCE' ) or 2 ); legacy_done = False ; engine_done = '' ; eng = ''
10621100 try :
1063- for _ in range (8 ): # bounded: a tree rewritten faster than it builds must not spin forever
1101+ for i in range (8 ): # bounded: a tree rewritten faster than it builds must not spin forever
10641102 time .sleep (debounce )
1103+ if i and not batch_due (repo ): return 0 # edits made during the build wait for their batch (batching)
10651104 t = load_table (repo )
10661105 if not has_graph (repo ): return 0
10671106 nb = newer_build (repo , t ) if t else ''
@@ -1087,7 +1126,7 @@ def worker(repo):
10871126 if eng and eng == engine_done : eng = ''
10881127 if (c is None or not any (c )) and not eng and not base_moved (repo ):
10891128 if export_behind (t ): rewarm (repo , t )
1090- write_state (repo , state = 'fresh' , checked = time .time (), checked_by = os .environ .get ('AXIOMCODE_REFRESH_TRIGGER' , '' )); return 0
1129+ write_state (repo , state = 'fresh' , checked = time .time (), checked_by = os .environ .get ('AXIOMCODE_REFRESH_TRIGGER' , '' ), edits = 0 ); return 0
10911130 c = c or [[], [], []]
10921131 st = read_state (repo )
10931132 # a build that FAILED on exactly this tree is not retried on every trigger: the next edit retries it
@@ -1099,7 +1138,7 @@ def worker(repo):
10991138 engine_done = eng
11001139 env = rebuild_env (t , AXIOMCODE_BACKGROUND = '1' , AXIOMCODE_REFRESH_REASON = why )
11011140 slot = take_slot (repo ) # queued behind the machine's other background builds
1102- t0 = time .time (); write_state (repo , state = 'building' , started = t0 , files = sum (len (x ) for x in c ))
1141+ t0 = time .time (); write_state (repo , state = 'building' , started = t0 , files = sum (len (x ) for x in c ), edits = 0 )
11031142 if any (c ): print (f"{ time .strftime ('%H:%M:%S' )} refresh: { sum (len (x ) for x in c )} file(s) changed ({ ', ' .join ((c [0 ] + c [1 ] + c [2 ])[:5 ])} ) — rebuilding" , flush = True )
11041143 if eng : print (f"{ time .strftime ('%H:%M:%S' )} refresh: { eng } ; rebuilding" , flush = True )
11051144 elif not any (c ): print (f"{ time .strftime ('%H:%M:%S' )} refresh: HEAD moved — moving the baseline to it" , flush = True )
@@ -1123,7 +1162,7 @@ def worker(repo):
11231162 # an edit that landed after the last check but while the lock was still held found the worker busy and
11241163 # returned; it is picked up here, after the lock is released, by starting over
11251164 c = changes (repo )
1126- if (c and any (c ) and read_state (repo ).get ('failed_table' ) != change_key (c )) or base_moved (repo ): kick (repo )
1165+ if (( c and any (c ) and read_state (repo ).get ('failed_table' ) != change_key (c )) or base_moved ( repo )) and batch_due (repo ): kick (repo )
11271166 return 0
11281167
11291168def failure_reason (out ):
0 commit comments