"""The capture gate: what is allowed to become a memory, expressed as data. v1 wrote ~98 memories/day, and its single largest duplicate cluster was 119 near-identical training-progress heartbeats. This module is what stops that happening again. Two things it does that the platform's custom instructions cannot: 1. HARD_DROP -- mechanical noise (task notifications, progress/ETA/epoch/loss frames, "N of M chunks", heartbeats, tool-only turns, subagent transcripts, and windows whose normalized shape repeats one already seen) never leaves the machine, at every aggressiveness level. 2. Repo content -- windows carrying excerpts of repository files (CLAUDE.md, README, configs). VALIDATION PROVED custom instructions CANNOT filter these: a pasted convention is textually indistinguishable from a stated convention, so the extractor happily stores it. Client-side omission is the only enforcement point. This rule is mandatory, not tunable. Rules are lists of compiled patterns / predicates rather than if-branches so that eval results can retune them by editing data. Everything here is pure and runs in single-digit milliseconds; no network, no I/O. """ from __future__ import annotations import hashlib import re from dataclasses import dataclass from typing import Any, Callable, Sequence # -------------------------------------------------------------------------- # levels # -------------------------------------------------------------------------- LEVELS: tuple[str, ...] = ("conservative", "balanced", "aggressive") _RANK = {name: i for i, name in enumerate(LEVELS)} DEFAULT_LEVEL = "balanced" # How many previously-seen window shapes the repeat detector remembers. RECENT_SHAPE_WINDOW = 40 def level_rank(level: str | None) -> int: return _RANK.get((level or DEFAULT_LEVEL).lower(), _RANK[DEFAULT_LEVEL]) # -------------------------------------------------------------------------- # turn / window normalization # -------------------------------------------------------------------------- TOOL_ROLES = frozenset( {"tool", "tool_result", "tool_use", "tool_call", "function", "function_call", "system", "developer"} ) SUBAGENT_ROLES = frozenset({"subagent", "sub_agent", "subagent_result", "sidechain"}) USER_ROLES = frozenset({"user", "human"}) def _rx(*patterns: str) -> tuple[re.Pattern, ...]: return tuple(re.compile(p, re.I | re.M) for p in patterns) def turn_text(turn: Any) -> str: """Text of a turn, tolerating str content, block lists, and missing keys.""" if turn is None: return "" if isinstance(turn, str): return turn if not isinstance(turn, dict): return str(turn) content = turn.get("content", turn.get("text", "")) if isinstance(content, str): return content if isinstance(content, list): parts: list[str] = [] for block in content: if isinstance(block, str): parts.append(block) elif isinstance(block, dict): if block.get("type") in (None, "text", "input_text", "output_text"): parts.append(str(block.get("text", ""))) return "\n".join(p for p in parts if p) return "" if content is None else str(content) def turn_role(turn: Any) -> str: if isinstance(turn, dict): return str(turn.get("role", "") or "").lower() return "" def window_text(window: Sequence[Any], scope: str = "any") -> str: """Concatenated text of the window, optionally restricted to a role scope.""" out: list[str] = [] for turn in window or []: role = turn_role(turn) if scope == "user" and role not in USER_ROLES: continue if scope == "assistant" and role not in ("assistant", "ai", "model"): continue text = turn_text(turn) if text: out.append(text) return "\n".join(out) _WORD_RX = re.compile(r"[A-Za-z][A-Za-z'’-]{1,}") _FENCE_BLOCK_RX = re.compile(r"```.*?```", re.S) def natural_words(text: str) -> int: """Word count with fenced code removed -- the proxy for 'has prose in it'.""" return len(_WORD_RX.findall(_FENCE_BLOCK_RX.sub(" ", text or ""))) _JSON_ONLY_RX = re.compile(r"^\s*[\[{].*[\]}]\s*$", re.S) _TOOL_FRAME_RX = _rx( r"^\s*<(antml:)?(function_calls|invoke|function_results|tool_use|tool_result)\b", r"^\s*(running|invoking|calling) tool\b", r"^\s*\[tool[:\]]", r"^\s*tool (call|result)\s*:", ) def _is_tool_turn(turn: Any) -> bool: role = turn_role(turn) if role in TOOL_ROLES: return True if isinstance(turn, dict) and turn.get("tool_only"): return True # transcript.py marks tool/thinking-only turns for us text = turn_text(turn).strip() if isinstance(turn, dict) and (turn.get("tool_calls") or turn.get("tool_use")) and not text: return True if not text: return True if any(p.search(text) for p in _TOOL_FRAME_RX): return True if _JSON_ONLY_RX.match(text): return True return natural_words(text) < 3 def _is_subagent_turn(turn: Any) -> bool: if isinstance(turn, dict): if turn.get("subagent") or turn.get("is_sidechain") or turn.get("isSidechain"): return True if turn_role(turn) in SUBAGENT_ROLES: return True if str(turn.get("source", "") or "").lower() in SUBAGENT_ROLES: return True return False # -------------------------------------------------------------------------- # rule model # -------------------------------------------------------------------------- @dataclass(frozen=True) class Rule: """One tunable rule. Either `patterns` or `predicate` (or both) decide a match.""" name: str patterns: tuple[re.Pattern, ...] = () predicate: Callable[[Sequence[Any], str], bool] | None = None mtype: str | None = None min_level: str = "conservative" scope: str = "any" def matches(self, window: Sequence[Any], level_value: int) -> bool: if level_value < _RANK.get(self.min_level, 0): return False text = window_text(window, self.scope) if self.patterns and text and any(p.search(text) for p in self.patterns): return True if self.predicate is not None and self.predicate(window, text): return True return False @dataclass(frozen=True) class TriggerResult: action: str # "drop" | "skip" | "flag" mtype: str | None reason: str @property def flagged(self) -> bool: return self.action == "flag" # -------------------------------------------------------------------------- # HARD DROP rules -- applied at every level # -------------------------------------------------------------------------- def _tool_only(window: Sequence[Any], text: str) -> bool: turns = list(window or []) return bool(turns) and all(_is_tool_turn(t) for t in turns) def _subagent(window: Sequence[Any], text: str) -> bool: return any(_is_subagent_turn(t) for t in window or []) def _self_repeating(window: Sequence[Any], text: str) -> bool: """The same normalized turn shape three or more times inside one window.""" counts: dict[str, int] = {} for turn in window or []: body = turn_text(turn) if natural_words(body) < 5: continue sig = _normalize(body) if not sig: continue counts[sig] = counts.get(sig, 0) + 1 return any(c >= 3 for c in counts.values()) HARD_DROP_RULES: list[Rule] = [ Rule( "task_notification", _rx( r"\btask notification\b", r"\btask[-_ ]id\b", r"\bprogress (for|on) task\b", r"\b(background )?task\s+[a-z0-9]{8,}\b\s*[:(]", r"\b(status|progress) (update|report)\b", r"\bnotification\s*\(task", ), ), Rule( "progress_metrics", _rx( r"\b\d{1,3}\s?%\s*(complete|completed|done|finished|through)\b", r"\bETA\b\s*(of|:|about|~)?\s*\d", r"\bETA\b\s*(about|approximately|roughly)\b", r"\bepoch\s*[:=]?\s*\d", r"\bloss\s*(of|:|=|is)?\s*\d", r"\bgradient norm\b", r"\b(step|iteration|batch)\s+\d[\d,]*\s+of\s+\d", r"\b(train|val|eval)(ing)?\s+(metrics|accuracy|loss)\b", r"\b(elapsed|remaining)\s*(time)?\s*[:=]\s*\d", r"\b\d+\s*(min|minutes|hours|hrs)\s+remaining\b", ), ), Rule( "batch_counters", _rx( r"\b\d[\d,]*\s+of\s+\d[\d,]*\s+chunks?\b", r"\b\d[\d,]*\s+of\s+\d[\d,]*\s+\w+\s+(processed|complete|completed|done)\b", r"\b\d[\d,]*\s+chunk failures?\b", r"\b\d[\d,]*\s+(records|rows|items|files|documents|memories)\s+(processed|generated|ingested|written)\b", r"\bprocessed\s+\d[\d,]*\s*/\s*\d[\d,]*\b", ), ), Rule( "heartbeat", _rx( r"\bheart\s?beat\b", r"\bstill (running|going|processing|training|working|in progress)\b", r"\bcontinuing to (run|process|train|monitor|poll)\b", r"\bno (new )?(changes|updates|progress) since\b", r"\bwill (check|report|update|ping)\b[^.\n]{0,30}\b(back )?(again )?in\s+\d", r"\bcheck(ing)? back in\s+\d", r"\bjob is (still )?(running|queued|pending)\b", r"\b(nothing|no change) to report\b", ), ), Rule( "activity_inventory", _rx( # "I modified VERSION, chat.py, agent.py, types.py, ..." -- derivable from git. # Three or more short comma-separated items after an edit verb: an inventory, # not a sentence. Item length is capped so it cannot span real prose. r"\b(?:i|we)\s+(?:modified|changed|updated|edited|touched|created|added|removed|deleted|refactored|rewrote)\b" r"(?:[^,\n]{1,60},){3,}", r"\bfiles (changed|modified|touched|edited)\s*:", r"\b(commits|prs|pull requests) (i|we) (made|opened|pushed)\b", r"\bhere'?s? (is )?what (i|we) (did|changed|modified)\b", r"\bin this session,? (i|we) (modified|changed|touched|edited)\b", ), ), Rule("tool_only", predicate=_tool_only), Rule("subagent_transcript", predicate=_subagent), Rule("repeated_shape_in_window", predicate=_self_repeating), ] # -------------------------------------------------------------------------- # Repo-content detector -- mandatory, never gated by level # -------------------------------------------------------------------------- REPO_FILE_RX = re.compile( r"(? str | None: """Return a reason string when the window carries repository file content. Bias is deliberately toward omission: a genuine convention restated by the developer survives this (it is prose), while a paste of CLAUDE.md does not. """ text = window_text(window) if not text.strip(): return None if any(p.search(text) for p in PASTE_PHRASE_RX): return "repo_content:paste_phrase" headings = len(_HEADING_RX.findall(text)) bullets = len(_BULLET_RX.findall(text)) fences = len(_FENCE_RX.findall(text)) tables = len(_TABLE_ROW_RX.findall(text)) config_lines = len(_CONFIG_LINE_RX.findall(text)) fenced_lines = sum(b.count("\n") for b in _FENCE_BLOCK_RX.findall(text)) named = bool(REPO_FILE_RX.search(text)) if headings >= 3: return "repo_content:heading_run" if fences >= 6 or fenced_lines >= 25: return "repo_content:code_fence_heavy" if tables >= 4: return "repo_content:table_block" if named and (headings >= 1 or fences >= 2 or bullets >= 5 or config_lines >= 5 or tables >= 2): return "repo_content:named_file_block" if config_lines >= 8 and natural_words(text) < config_lines * 6: return "repo_content:config_block" return None # -------------------------------------------------------------------------- # FLAG rules -- these also assign metadata.type # -------------------------------------------------------------------------- _NUMBERED_STEP_RX = re.compile(r"^\s*(\d{1,2})[.)]\s+\S", re.M) _CONFIRMED_RX = _rx( r"\b(verified|confirmed|worked|works end to end|succeeded|ran clean|all green|that did it)\b" ) def _verified_procedure(window: Sequence[Any], text: str) -> bool: """An ordered step list that someone confirmed actually works.""" if len(_NUMBERED_STEP_RX.findall(text)) < 3: return False return any(p.search(text) for p in _CONFIRMED_RX) _COMPLETION_RX = _rx( r"\ball (tests|checks|suites) (are )?(pass|passing|passed|green)\b", r"\b(migration|refactor|rollout|upgrade|release) (is )?(now )?(complete|done|finished)\b", r"\bthat completes\b", r"\bwe'?re done with\b", r"\bfinished (the|our)\b", r"\bshipped (it|the)\b", ) def _completed_goal(window: Sequence[Any], text: str) -> bool: """A multi-step goal that reached the finish line -- worth one lesson, not ten.""" if len(list(window or [])) < 3: return False return any(p.search(text) for p in _COMPLETION_RX) REMEMBER_RULE = Rule( "remember_intent", _rx( r"\bremember (this|that|to|:)", r"\b(please )?remember\b[^.\n]{0,40}\bfor (next time|the future|future sessions)\b", r"\bdon'?t forget\b", r"\bdo not forget\b", r"\bnote that\b", r"\bmake a note\b", r"\bkeep in mind\b", r"\bfor future reference\b", r"\bwrite this down\b", ), mtype="preference", min_level="conservative", scope="user", ) CORRECTION_RULE = Rule( "user_correction", _rx( r"\bno,? actually\b", r"\bstop doing\b", r"\bdon'?t do that\b", r"\bdo not do that\b", r"\bi (already )?told you\b", r"\bi keep telling you\b", r"\bthat'?s not what i (asked|wanted|said)\b", # "stop dumping the whole diff at me" -- any gerund, from a user turn, is a correction. r"\bstop\s+\w+ing\b", r"\bplease stop\b", r"\bnever do (that|this) again\b", r"\bthat'?s (not|the opposite of) what\b", ), mtype="preference", min_level="conservative", scope="user", ) # A one-off instruction is not a memory; a standing one is. These markers are what # separate "skip tests for now" from "skip tests from now on", and a stated standing # preference is the single most valuable thing this plugin captures. # scope="user" is load-bearing: assistant narration and progress frames say "every # time" too, and those must never become the developer's preferences. STANDING_PREFERENCE_RULE = Rule( "standing_preference", _rx( r"\bfrom now on\b", r"\bgoing forward\b", r"\bin future sessions\b", r"\bas a (general )?rule\b", r"\bby default,? (always|never|please|use|do|show|give)\b", r"\bthat'?s how i (want|like) it\b", r"\bi (always|never) want you to\b", r"\bi want you to always\b", r"\bi prefer\b", r"\bi'?d (rather|prefer)\b", r"\bi would (rather|prefer)\b", r"\bplease (always|never)\b", r"\bdon'?t ever\b", r"\balways \w+ me\b", # "every time" only counts inside an actual instruction. r"\b(don'?t|do not|please|stop|always|never|show|give|ask)\b[^.\n]{0,80}\bevery time\b", r"\bevery time\b[^.\n]{0,80}\b(please|instead|don'?t|do not)\b", ), mtype="preference", min_level="conservative", scope="user", ) DECISION_RULE = Rule( "decision_language", _rx( r"\blet'?s go with\b", r"\bwe'?(ll|re going to) (use|go with|adopt|switch to|standardi[sz]e on)\b", r"\bwe (decided|settled) (on|to)\b", r"\bdecided to\b", r"\bgoing with\b", r"\binstead of\b[^.\n]{0,120}\bbecause\b", r"\bwe'?ll (keep|drop|remove|replace)\b[^.\n]{0,120}\bbecause\b", r"\bthe call is\b", ), mtype="decision", min_level="balanced", ) INSIGHT_RULE = Rule( "error_resolution_arc", _rx( r"\broot cause\b", r"\bthe (fix|problem|issue|bug) (was|turned out to be)\b", r"\bturns out\b", r"\bfails unless\b", r"\bonly works (if|when)\b", r"\bsilently (ignore[sd]?|drops?|fails?)\b", r"\bit was actually\b", r"\bthe real (problem|reason|cause)\b", r"\bgotcha\b", ), mtype="insight", min_level="balanced", ) CONVENTION_RULE = Rule( "convention_statement", _rx( r"\balways name\b", r"\bthe rule (here|is)\b", r"\bmust (be|use|go|live|match|include)\b", r"\brequired to\b", r"\bwe always\b", r"\bwe never\b", r"\bnever commit\b", r"\b(our|the team'?s?) convention (is|here)\b", r"\bby convention\b", r"\bhas to (be|go|live|match)\b", ), mtype="convention", min_level="balanced", ) RUNBOOK_RULE = Rule( "verified_procedure", _rx( r"\bverified the (release|deploy(ment)?|rollback|migration|setup) procedure\b", r"\bsteps that worked\b", r"\bthe (release|deploy|rollback|setup) procedure is\b", r"\bthis is the runbook\b", ), predicate=_verified_procedure, mtype="runbook", min_level="aggressive", ) COMPLETED_GOAL_RULE = Rule( "completed_goal", predicate=_completed_goal, mtype="insight", min_level="aggressive", ) # Order matters: the first match wins, so the most specific intent leads. FLAG_RULES: list[Rule] = [ REMEMBER_RULE, CORRECTION_RULE, STANDING_PREFERENCE_RULE, DECISION_RULE, INSIGHT_RULE, CONVENTION_RULE, RUNBOOK_RULE, COMPLETED_GOAL_RULE, ] # Rules consulted to refine the type of an explicit "remember this". _TYPED_RULES: list[Rule] = [DECISION_RULE, INSIGHT_RULE, CONVENTION_RULE, RUNBOOK_RULE] def _refine_remember_type(window: Sequence[Any]) -> str: """"remember that we decided X" is a decision, not a preference.""" for rule in _TYPED_RULES: if rule.matches(window, _RANK["aggressive"]): return rule.mtype or "preference" return "preference" # -------------------------------------------------------------------------- # shape signature (repeat detector) # -------------------------------------------------------------------------- _TOKEN_RX = re.compile(r"[a-z0-9_./%-]+") _SHAPE_TOKENS = 16 def _normalize(text: str) -> str: """Digit-bearing tokens collapse to '#', so two heartbeats differing only in ids, counts and percentages normalize to the same string.""" tokens = _TOKEN_RX.findall((text or "").lower()) out: list[str] = [] for tok in tokens: out.append("#" if any(ch.isdigit() for ch in tok) else tok.strip("./-_%")) if len(out) >= _SHAPE_TOKENS: break return " ".join(t for t in out if t) def shape_signature(window: Sequence[Any]) -> str: """Stable hash of a window's shape: role sequence plus normalized openings.""" parts: list[str] = [] for turn in window or []: parts.append((turn_role(turn) or "?") + ":" + _normalize(turn_text(turn))) return hashlib.sha1("|".join(parts).encode("utf-8", "replace")).hexdigest()[:16] # -------------------------------------------------------------------------- # public entry point # -------------------------------------------------------------------------- def classify( window: list[dict], level: str = DEFAULT_LEVEL, recent_shapes: list[str] | None = None, ) -> TriggerResult: """Decide what to do with one conversational window. "drop" -- hard-dropped noise, never sent anywhere. "skip" -- nothing worth storing right now. "flag" -- capture it, as `mtype`. """ turns = list(window or []) if not turns: return TriggerResult("skip", None, "empty_window") value = level_rank(level) for rule in HARD_DROP_RULES: if rule.matches(turns, _RANK["aggressive"]): # hard drops ignore the level return TriggerResult("drop", None, rule.name) if recent_shapes and shape_signature(turns) in set(recent_shapes): return TriggerResult("drop", None, "repeated_shape") reason = repo_content_reason(turns) if reason: return TriggerResult("drop", None, reason) if natural_words(window_text(turns)) < 4: return TriggerResult("skip", None, "no_prose") for rule in FLAG_RULES: if rule.matches(turns, value): mtype = rule.mtype if rule.name == "remember_intent": mtype = _refine_remember_type(turns) return TriggerResult("flag", mtype, rule.name) return TriggerResult("skip", None, "no_trigger")