mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-09-05 08:05:57 +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
136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
"""SessionStart -- inject L3 (persona) + L2 (this project). DESIGN.md 5.1.
|
|
|
|
Contract, in the order it matters:
|
|
|
|
* NEVER BLOCKS. Any failure exits 0 with no output. A memory system that can
|
|
break session start is worse than no memory system.
|
|
* Budgets: 2 KB L3, 4 KB L2. Over budget -> truncate by `last_seen` desc and
|
|
SAY SO in the block. "A memory system that silently drops is worse than
|
|
none" (5.1).
|
|
* Never emit two contradictory lines unmarked (5.1). The 4.2.1 detector
|
|
cannot reach L3, so nothing upstream guarantees L2 and L3 agree; a collision
|
|
is marked here at injection time.
|
|
* Disable with AGENT_MEMORY_SESSIONSTART=0.
|
|
|
|
No internal self-budget, unlike user_prompt_submit.py -- and 5.1 argues why:
|
|
this runs once per session, and its work is bounded by the byte caps above
|
|
rather than by a scan that grows with history.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"skills", "agent-memory", "scripts"))
|
|
|
|
L3_BUDGET = 2048
|
|
L2_BUDGET = 4096
|
|
|
|
|
|
def _contested_tag(atom, atoms, core):
|
|
if not core.open_contradiction(atom, atoms):
|
|
return ""
|
|
return " [contested — newer evidence %s]" % atom["last_seen"][:10]
|
|
|
|
|
|
def _fit(lines, budget):
|
|
"""Take lines in priority order until the byte budget is spent. Returns
|
|
(kept, dropped) -- the caller must disclose `dropped`."""
|
|
kept, used = [], 0
|
|
for i, ln in enumerate(lines):
|
|
cost = len(ln.encode("utf-8")) + 1
|
|
if used + cost > budget:
|
|
return kept, len(lines) - i
|
|
kept.append(ln)
|
|
used += cost
|
|
return kept, 0
|
|
|
|
|
|
def _cross_tier_conflicts(l2, l3, core):
|
|
"""5.1's never-emit-contradictory-lines constraint. 4.2.1's detector is
|
|
project-scoped and so structurally cannot see an L2-vs-L3 pair; this is the
|
|
injection-time backstop.
|
|
|
|
This marks BOTH sides rather than shadowing one. That is the least
|
|
committal of the three options 5.1 says all satisfy the constraint -- it
|
|
surfaces the collision without deciding precedence, which is exactly what
|
|
the open decision on L3 contradictions has not yet decided. If that decision
|
|
lands on specificity-wins, this is the one function that changes.
|
|
"""
|
|
marks = {}
|
|
for a in l2:
|
|
for b in l3:
|
|
if core.contradicts(a["claim"], b["claim"], a["kind"], b["kind"]):
|
|
marks[a["id"]] = b["claim"]
|
|
marks[b["id"]] = a["claim"]
|
|
return marks
|
|
|
|
|
|
def build(atoms, project, core):
|
|
l3 = [a for a in atoms if a["tier"] == "L3"]
|
|
l2 = [a for a in atoms if a["tier"] == "L2" and a.get("project") == project]
|
|
l3.sort(key=lambda a: a["last_seen"], reverse=True)
|
|
l2.sort(key=lambda a: a["last_seen"], reverse=True)
|
|
if not l3 and not l2:
|
|
return ""
|
|
|
|
conflicts = _cross_tier_conflicts(l2, l3, core)
|
|
|
|
def render(a):
|
|
line = "- %s%s" % (a["claim"], _contested_tag(a, atoms, core))
|
|
if a["id"] in conflicts:
|
|
line += "\n [conflicts with another remembered rule: %s — " \
|
|
"neither governs; ask before relying on either]" % conflicts[a["id"]]
|
|
return line
|
|
|
|
out = ["<agent_memory>",
|
|
"Remembered from previous sessions. Recurrence-gated, not verified fact;",
|
|
"correct anything wrong and the correction is itself remembered."]
|
|
if l3:
|
|
kept, dropped = _fit([render(a) for a in l3], L3_BUDGET)
|
|
out.append("")
|
|
out.append("## Stable (L3, all projects)")
|
|
out.extend(kept)
|
|
if dropped:
|
|
out.append("- [%d more L3 item(s) omitted for space, oldest-seen first]" % dropped)
|
|
if l2:
|
|
kept, dropped = _fit([render(a) for a in l2], L2_BUDGET)
|
|
out.append("")
|
|
out.append("## This project (L2: %s)" % project)
|
|
out.extend(kept)
|
|
if dropped:
|
|
out.append("- [%d more L2 item(s) omitted for space, oldest-seen first]" % dropped)
|
|
out.append("</agent_memory>")
|
|
return "\n".join(out)
|
|
|
|
|
|
def main():
|
|
if os.environ.get("AGENT_MEMORY_SESSIONSTART") == "0":
|
|
return 0
|
|
try:
|
|
payload = {}
|
|
if not sys.stdin.isatty():
|
|
raw = sys.stdin.read()
|
|
if raw.strip():
|
|
payload = json.loads(raw)
|
|
except Exception:
|
|
payload = {}
|
|
try:
|
|
import memory_core as core
|
|
cwd = payload.get("cwd") or os.getcwd()
|
|
project = os.path.basename(os.path.abspath(cwd))
|
|
block = build(core.AtomStore(os.path.join(cwd, ".memory")).read(), project, core)
|
|
if block:
|
|
sys.stdout.write(block + "\n")
|
|
except Exception:
|
|
# 5.1 -- never blocks. No memory this session is the failure mode.
|
|
return 0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|