Module: Rubino::Memory::SalienceGate

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

Overview

Salience pre-filter for the auto-extraction path (r5 F5/F6/F7).

The aux-LLM extraction prompt already asks the model to emit nothing (href="">add”:[],“supersede”:) for trivial turns, but in practice it still mints facts from greetings, a one-word “help”, and transient task chatter (“User decided to remove the mode feature”). Following Claude Code’s auto-memory (“decides what’s worth remembering for a FUTURE conversation”), mem0’s NOOP path, and Letta’s “save only durable facts”, this is a cheap heuristic NOOP gate that runs BEFORE the aux call: if a turn’s USER text carries no plausibly-durable assertion, skip the extraction entirely. That both saves the aux spend and guarantees no fact is minted from throwaway input — the model never gets a chance to over-extract.

Deliberately conservative: it only suppresses turns that are clearly non-durable (greetings/acknowledgements, bare command words, very short throwaway Q&A with no first-person assertion). Anything with a first-person statement, a preference/decision verb, or substantive length passes through to the aux model, which remains the real salience judge (and applies the durable-vs-stale doctrine in its prompt). False negatives here only cost a redundant aux call; they never store a bad fact.

Constant Summary collapse

TRIVIAL_WORDS =

Single bare words / short interjections that are never durable on their own — slash-less command reflexes and greetings a sloppy user fires.

%w[
  help commands ? h hi hey hello yo sup yes no ok okay yeah yep nope nah
  thanks thank ty thx please cool nice great done quit exit bye q :q :wq
  continue go next stop wait what why how when who
].to_set.freeze
DURABLE_SIGNALS =

First-person / assertion signals that mark a turn as plausibly carrying a durable fact, preference, or correction worth the aux model’s attention. Kept broad on purpose — the aux model is the precise judge; this only decides whether it’s worth ASKING it.

[
  /\bmy name is\b/i,
  /\bi(?:'m| am)\b/i,
  /\bi (?:prefer|like|love|hate|want|need|use|always|never|usually|work|deploy|run|don't|do not)\b/i,
  /\bwe (?:use|prefer|decided|always|never|deploy|run|agreed)\b/i,
  /\bcall me\b/i,
  /\b(?:please )?(?:always|never|don't ever|do not ever) (?:use|do|run|call|assume)\b/i,
  /\bremember (?:that|this|:)/i,
  /\bthe (?:project|repo|codebase|team|convention|standard) (?:uses|is|prefers|requires)\b/i
].freeze
MIN_CONTENT_WORDS =

A turn shorter than this (in informative words, after dropping trivial/ stopword tokens) and lacking any explicit durable signal is treated as throwaway. 3 lets “I use Kamal” through (signal-matched anyway) while dropping “help”, “thanks”, “what now”.

3

Class Method Summary collapse

Class Method Details

.informative_words(text) ⇒ Object

Lowercase content words with trivial command/greeting tokens and pure punctuation removed, used to measure throwaway-ness.



96
97
98
99
100
# File 'lib/rubino/memory/salience_gate.rb', line 96

def informative_words(text)
  text.downcase.scan(/[\p{L}\p{N}']+/).reject do |w|
    TRIVIAL_WORDS.include?(w)
  end
end

.salient?(turn_text) ⇒ Boolean

Decide whether the turn’s transcript is worth feeding the aux extractor. ‘turn_text` is the rendered USER/ASSISTANT transcript the backend already builds. Returns true when the turn MAY carry durable info (let the aux model decide), false to NOOP immediately.

Returns:

  • (Boolean)


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rubino/memory/salience_gate.rb', line 55

def salient?(turn_text)
  user = user_lines(turn_text)
  # No user text at all (e.g. an assistant-only turn) → nothing the user
  # asserted to remember.
  return false if user.empty?

  joined = user.join(" ").strip
  return true if DURABLE_SIGNALS.any? { |re| joined.match?(re) }

  # Strip the USER text to its informative core. A turn that is only
  # greetings/acknowledgements/bare command words once normalized has no
  # durable assertion to mine.
  meaningful = informative_words(joined)
  return false if meaningful.empty?

  # Very short throwaway turns with no durable signal (a one-word "help", a
  # bare "thanks", "what is this") aren't worth an extraction pass. A turn
  # only clears the bar once it has a few content words — long enough to
  # plausibly state something durable. The aux model still vets it.
  meaningful.size >= MIN_CONTENT_WORDS
end

.user_lines(turn_text) ⇒ Object

USER-role lines from the rendered transcript (the backend prefixes each line with “USER: ” / “ASSISTANT: ”). We gate on what the user actually said, not on the assistant’s narration (which is where transient “User decided to remove X” task-chatter leaks in).



87
88
89
90
91
92
# File 'lib/rubino/memory/salience_gate.rb', line 87

def user_lines(turn_text)
  turn_text.to_s.lines.filter_map do |line|
    m = line.match(/\AUSER:\s?(.*)\z/m)
    m && m[1].strip
  end.reject(&:empty?)
end