Module: Rubino::Memory::AuxRetry

Included in:
Backends::Sqlite
Defined in:
lib/rubino/memory/aux_retry.rb

Overview

Bounded retry/backoff for the aux memory-extraction call (r5 C-2).

The aux client calls the adapter directly and so — unlike the main conversation loop, whose Agent::ModelCallRunner owns retry/backoff — got NO retry: under concurrent load a single RubyLLM::RateLimitError (429) was caught at the call site, logged ‘memory.sqlite.skip`, and the extracted fact was DROPPED for good. This mixin wraps the aux call in the SAME jittered-backoff policy the main loop uses, retrying retryable errors (429/overloaded/5xx/transport, per LLM::ErrorClassifier) up to a small budget and honouring Retry-After on a rate-limit. After the budget is exhausted (or on a non-retryable error) it re-raises to the caller, which leaves the per-session cursor put so the turn is re-fed next time rather than silently lost.

Host requirements: ‘@config` (a Config::Configuration answering #dig) and a `DEFAULT_EXTRACT_MAX_RETRIES` constant on the including class.

Instance Method Summary collapse

Instance Method Details

#with_aux_retryObject

Run ‘block` (the aux call), retrying transient errors. Re-raises the last error once the budget is exhausted or the error is non-retryable.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/rubino/memory/aux_retry.rb', line 24

def with_aux_retry
  attempts = 0
  begin
    # Honour a detached-polishing cancel (Esc to skip): the background
    # housekeeping thread binds Rubino.aux_cancel_token, so an Esc that
    # cancelled it must abort BEFORE spending another aux-LLM call rather
    # than running to completion off-screen (#319).
    aux_check_cancelled!
    yield
  rescue Rubino::Interrupted
    # Cancellation is terminal — re-raise straight through so the detached
    # polishing thread unwinds and leaves the cursor put (re-runs next turn).
    raise
  rescue StandardError => e
    classified = LLM::ErrorClassifier.classify(e)
    raise unless classified.retryable && attempts < extract_max_retries

    attempts += 1
    wait = aux_backoff.wait_seconds(
      attempts,
      base: Agent::BackoffPolicy::ERROR_PATH[:base],
      max: Agent::BackoffPolicy::ERROR_PATH[:max],
      retry_after: aux_rate_limit_retry_after(classified, e)
    )
    log_aux_retry(e, attempts, wait)
    # Sleep in short slices so an Esc during the (possibly long, Retry-After
    # honouring) backoff wait aborts within ~100ms instead of holding the
    # detached worker for the full window (#319). On the foreground/API
    # path no token is bound, so this is one uninterrupted sleep as before.
    aux_cancellable_sleep(wait)
    retry
  end
end