mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-09-07 08:26:02 +00:00
Turns DESIGN.md from a spec into a working plugin. Five stdlib scripts, three
hooks, agent, command, three references, plugin manifests.
The gates are the design:
L1 -> L2 >= 3 distinct sessions spanning >= 2 distinct calendar days
(`stated` = 2 sessions, day rule still applies; `verified` = 1
observation and is the only day-exempt path)
L2 -> L3 >= 2 distinct projects, >= 30 days, uncontested
Two gates refuse rather than guess. `redacted: true` blocks promotion on any
volume of evidence -- a durability-independent barrier, since a secret restated
across five sessions passes every recurrence gate; the flag firing means the
text was altered, a lexical filter finding one secret is not proof it found all
of them, and L2/L3 are committed to git. An open contradiction freezes both
claims, found by reverse join because the newer atom carries no flag.
All three hooks fail open: a broken memory system costs memory, never a session.
SessionEnd stages promotions to .memory/staged/ and never touches a CLAUDE.md;
only an explicit human adopt does, after backing both files up.
Verified, not asserted:
- all three pinned atom ids from DESIGN.md reproduce exactly
- both blocking gates demonstrated on sample input, named in the output
- end-to-end: two transcripts across two calendar days -> merged L1 atom ->
staged L2 promotion with the path prefix stripped
- reverse join blocks the unflagged newer atom
- cross-tier L2/L3 collision marked at injection time
- recall p50 29ms / p95 31ms / max 35ms spawn-to-exit, scoring itself 2-3ms
over 500 atoms -- interpreter cold start is the entire cost
- validate_examples.py 69 checks 0 failures; SKILL.md 6/6 PASS
- derive_counters --check, check_plugin_json --all, check_paths all clean
DESIGN.md 10.1's "+6" tool estimate corrected to +8 -- the delivered surface is
5 scripts + 3 hooks. README.md's deviations list is authoritative for that and
five other divergences from the pre-implementation spec.
Concept from TencentCloud/TencentDB-Agent-Memory (MIT). No upstream code.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EM5xmJ7AmTMg31rq68BCym
151 lines
5.2 KiB
Python
151 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""UserPromptSubmit -- recall relevant L1 atoms. DESIGN.md 5.2.
|
|
|
|
PROVISIONAL. hooks.json says so and so does this file: 9.5 is an open decision
|
|
whose option (c) is to delete this hook, because the measured cost is dominated
|
|
by interpreter cold start (p50 ~12 ms, p95 ~31 ms just to reach `main`) rather
|
|
than by anything below. The scoring pass itself measured 2-3 ms over 500 atoms.
|
|
Shipping it does not close that decision.
|
|
|
|
Two limits, not one (5.2 is emphatic that conflating them misses the point):
|
|
|
|
internal self-budget 100 ms, enforced HERE against a monotonic clock.
|
|
Past budget: stop scoring, return what we have.
|
|
hook timeout backstop 1 second, enforced by Claude Code. Kills a wedged
|
|
process. Finishing under 1 s does NOT satisfy the spec.
|
|
|
|
* top 5 atoms, 1 KB max
|
|
* a `contested` atom renders with the 4.2 tag, never as a bare claim
|
|
* reads take NO lock (5.4) -- atomic os.replace on the writer side is what
|
|
makes a lock-free read safe, and blocking here on an async SessionEnd's
|
|
lock would blow the budget for a hook whose failure mode is "return nothing"
|
|
* disable with AGENT_MEMORY_USERPROMPTSUBMIT=0
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
|
|
BUDGET_S = 0.100
|
|
MAX_ATOMS = 5
|
|
MAX_BYTES = 1024
|
|
|
|
sys.path.insert(0, os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"skills", "agent-memory", "scripts"))
|
|
|
|
# Weighting: a constraint the user stated out loud is worth more at recall time
|
|
# than a passively observed preference.
|
|
KIND_WEIGHT = {"constraint": 1.4, "correction": 1.3, "failure": 1.2,
|
|
"preference": 1.0, "fact": 1.0}
|
|
CONF_WEIGHT = {"verified": 1.3, "stated": 1.1, "observed": 1.0}
|
|
|
|
_STOP = {
|
|
"the", "a", "an", "and", "or", "but", "if", "is", "are", "was", "were", "be",
|
|
"to", "of", "in", "on", "for", "with", "that", "this", "it", "as", "at", "by",
|
|
"do", "does", "did", "can", "you", "i", "we", "me", "my", "our", "please",
|
|
"what", "how", "why", "when", "where", "not", "no", "so", "up", "out", "then",
|
|
}
|
|
_WORD = re.compile(r"[a-z0-9_.\-/]+")
|
|
|
|
|
|
def _tokens(text):
|
|
return {t for t in _WORD.findall(text.casefold()) if t not in _STOP and len(t) > 2}
|
|
|
|
|
|
def score(atom, prompt_tokens, now_day, core):
|
|
"""Token overlap x kind x confidence x recency. Deterministic, lexical, no
|
|
embeddings and no API call (5.2, and the repo-wide no-LLM-in-scripts rule)."""
|
|
at = _tokens(atom["claim"])
|
|
if not at:
|
|
return 0.0
|
|
overlap = len(at & prompt_tokens)
|
|
if not overlap:
|
|
return 0.0
|
|
base = overlap / (len(at) ** 0.5)
|
|
try:
|
|
age = max(0, now_day - int(core.parse_iso(atom["last_seen"]).timestamp() // 86400))
|
|
except Exception:
|
|
age = 0
|
|
recency = 1.0 / (1.0 + age / 30.0)
|
|
return (base
|
|
* KIND_WEIGHT.get(atom["kind"], 1.0)
|
|
* CONF_WEIGHT.get(atom["confidence"], 1.0)
|
|
* (0.7 + 0.3 * recency))
|
|
|
|
|
|
def recall(atoms, prompt, core, deadline=None):
|
|
"""Single linear pass, bounded top-5 -- no index build, no full sort (5.2).
|
|
Returns (rows, budget_expired)."""
|
|
ptok = _tokens(prompt)
|
|
if not ptok:
|
|
return [], False
|
|
now_day = int(time.time() // 86400)
|
|
top = [] # kept tiny; insertion sort into <=5 slots beats sorting 500
|
|
expired = False
|
|
for a in atoms:
|
|
if a["tier"] != "L1":
|
|
continue
|
|
if deadline is not None and time.monotonic() > deadline:
|
|
expired = True
|
|
break
|
|
s = score(a, ptok, now_day, core)
|
|
if s <= 0:
|
|
continue
|
|
if len(top) < MAX_ATOMS or s > top[-1][0]:
|
|
top.append((s, a))
|
|
top.sort(key=lambda r: r[0], reverse=True)
|
|
del top[MAX_ATOMS:]
|
|
return top, expired
|
|
|
|
|
|
def render(top, atoms, core):
|
|
lines, used = [], 0
|
|
for s, a in top:
|
|
tag = ""
|
|
if core.open_contradiction(a, atoms):
|
|
# 5.2 -- a contested atom must NOT surface as a bare claim.
|
|
tag = " [contested — newer evidence %s]" % a["last_seen"][:10]
|
|
ln = "- %s%s" % (a["claim"], tag)
|
|
cost = len(ln.encode("utf-8")) + 1
|
|
if used + cost > MAX_BYTES:
|
|
break
|
|
lines.append(ln)
|
|
used += cost
|
|
if not lines:
|
|
return ""
|
|
return "\n".join(["<agent_memory_recall>",
|
|
"Possibly relevant, remembered from earlier sessions:"]
|
|
+ lines + ["</agent_memory_recall>"])
|
|
|
|
|
|
def main():
|
|
if os.environ.get("AGENT_MEMORY_USERPROMPTSUBMIT") == "0":
|
|
return 0
|
|
deadline = time.monotonic() + BUDGET_S
|
|
try:
|
|
raw = "" if sys.stdin.isatty() else sys.stdin.read()
|
|
payload = json.loads(raw) if raw.strip() else {}
|
|
except Exception:
|
|
return 0
|
|
prompt = payload.get("prompt") or ""
|
|
if not prompt:
|
|
return 0
|
|
try:
|
|
import memory_core as core
|
|
cwd = payload.get("cwd") or os.getcwd()
|
|
atoms = core.AtomStore(os.path.join(cwd, ".memory")).read()
|
|
top, _expired = recall(atoms, prompt, core, deadline)
|
|
block = render(top, atoms, core)
|
|
if block:
|
|
sys.stdout.write(block + "\n")
|
|
except Exception:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|