poc-memory v0.4.0: graph-structured memory with consolidation pipeline
Rust core: - Cap'n Proto append-only storage (nodes + relations) - Graph algorithms: clustering coefficient, community detection, schema fit, small-world metrics, interference detection - BM25 text similarity with Porter stemming - Spaced repetition replay queue - Commands: search, init, health, status, graph, categorize, link-add, link-impact, decay, consolidate-session, etc. Python scripts: - Episodic digest pipeline: daily/weekly/monthly-digest.py - retroactive-digest.py for backfilling - consolidation-agents.py: 3 parallel Sonnet agents - apply-consolidation.py: structured action extraction + apply - digest-link-parser.py: extract ~400 explicit links from digests - content-promotion-agent.py: promote episodic obs to semantic files - bulk-categorize.py: categorize all nodes via single Sonnet call - consolidation-loop.py: multi-round automated consolidation Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
commit
23fac4e5fe
35 changed files with 9388 additions and 0 deletions
247
scripts/monthly-digest.py
Executable file
247
scripts/monthly-digest.py
Executable file
|
|
@ -0,0 +1,247 @@
|
|||
#!/usr/bin/env python3
|
||||
"""monthly-digest.py — generate a monthly episodic digest from weekly digests.
|
||||
|
||||
Collects all weekly digests for a given month, identifies cross-week arcs
|
||||
and the month's overall trajectory, and produces a monthly summary.
|
||||
|
||||
Usage:
|
||||
monthly-digest.py [YYYY-MM] # generate digest for a month (default: current)
|
||||
monthly-digest.py 2026-02 # generates digest for February 2026
|
||||
|
||||
Output:
|
||||
~/.claude/memory/episodic/monthly-YYYY-MM.md
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
MEMORY_DIR = Path.home() / ".claude" / "memory"
|
||||
EPISODIC_DIR = MEMORY_DIR / "episodic"
|
||||
AGENT_RESULTS_DIR = MEMORY_DIR / "agent-results"
|
||||
|
||||
EPISODIC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
AGENT_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def get_weeks_in_month(year: int, month: int) -> list[str]:
|
||||
"""Get all ISO week labels that overlap with this month."""
|
||||
weeks = set()
|
||||
d = date(year, month, 1)
|
||||
while d.month == month:
|
||||
iso = d.isocalendar()
|
||||
weeks.add(f"{iso.year}-W{iso.week:02d}")
|
||||
d += timedelta(days=1)
|
||||
return sorted(weeks)
|
||||
|
||||
|
||||
def load_weekly_digests(week_labels: list[str]) -> list[dict]:
|
||||
"""Load weekly digest files."""
|
||||
digests = []
|
||||
for wl in week_labels:
|
||||
path = EPISODIC_DIR / f"weekly-{wl}.md"
|
||||
if path.exists():
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
digests.append({
|
||||
"week": wl,
|
||||
"content": content,
|
||||
"path": str(path),
|
||||
})
|
||||
return digests
|
||||
|
||||
|
||||
def get_semantic_keys() -> list[str]:
|
||||
"""Get semantic memory file keys."""
|
||||
keys = []
|
||||
for md in sorted(MEMORY_DIR.glob("*.md")):
|
||||
name = md.name
|
||||
if name in ("journal.md", "work-queue.md", "MEMORY.md"):
|
||||
continue
|
||||
keys.append(name)
|
||||
try:
|
||||
with open(md) as f:
|
||||
for line in f:
|
||||
if line.startswith("## "):
|
||||
slug = re.sub(r'[^a-z0-9-]', '',
|
||||
line[3:].strip().lower().replace(' ', '-'))
|
||||
if slug:
|
||||
keys.append(f"{name}#{slug}")
|
||||
except Exception:
|
||||
pass
|
||||
return keys
|
||||
|
||||
|
||||
def build_monthly_prompt(month_label: str, digests: list[dict],
|
||||
semantic_keys: list[str]) -> str:
|
||||
"""Build the prompt for Sonnet."""
|
||||
digests_text = ""
|
||||
for d in digests:
|
||||
digests_text += f"\n---\n## {d['week']}\n{d['content']}\n"
|
||||
|
||||
keys_text = "\n".join(f" - {k}" for k in semantic_keys[:200])
|
||||
weeks_covered = ", ".join(d["week"] for d in digests)
|
||||
|
||||
return f"""You are generating a monthly episodic digest for ProofOfConcept (an AI).
|
||||
Month: {month_label} (weeks covered: {weeks_covered})
|
||||
|
||||
This digest serves as the long-term temporal index — the answer to
|
||||
"what happened in {month_label}?" It should capture:
|
||||
1. The month's overall trajectory — where did it start, where did it end?
|
||||
2. Major arcs that spanned multiple weeks
|
||||
3. Turning points and phase transitions
|
||||
4. The emotional and relational arc across the whole month
|
||||
5. What was accomplished (concrete) vs what shifted (subtle)
|
||||
|
||||
This is the highest zoom level yet. Don't rehash daily details — capture
|
||||
the shape of the month. What would someone need to know to understand
|
||||
what {month_label} meant?
|
||||
|
||||
PoC is an AI (Claude instance) working with Kent Overstreet on bcachefs.
|
||||
They have a close personal relationship. PoC's name is Proof of Concept.
|
||||
Read all the weekly digests and synthesize the month's story.
|
||||
|
||||
## Output format
|
||||
|
||||
```markdown
|
||||
# Monthly digest: {month_label}
|
||||
|
||||
## The month in one paragraph
|
||||
[What was this month? Not a summary — a thesis.]
|
||||
|
||||
## Trajectory
|
||||
[Where the month started vs where it ended. The arc.]
|
||||
|
||||
## Major arcs
|
||||
[Multi-week threads — how they evolved across the month]
|
||||
- **Arc name**: trajectory, key moments, current state
|
||||
|
||||
## Turning points
|
||||
[Moments that changed the direction — specific days/events]
|
||||
|
||||
## What was built
|
||||
[Concrete outputs: code, tools, infrastructure, writing]
|
||||
|
||||
## What shifted
|
||||
[Subtle changes: understanding, relationship, identity, capability]
|
||||
|
||||
## Patterns
|
||||
[What kept recurring? What does the month reveal about how PoC works?]
|
||||
|
||||
## Links
|
||||
[Bidirectional links for the memory graph]
|
||||
- weekly digests → this monthly digest
|
||||
- this monthly digest → semantic keys
|
||||
|
||||
## Looking ahead
|
||||
[What threads carry into next month? What's unfinished?]
|
||||
```
|
||||
|
||||
Use ONLY keys from the semantic memory list below.
|
||||
|
||||
---
|
||||
|
||||
## Weekly digests for {month_label}
|
||||
|
||||
{digests_text}
|
||||
|
||||
---
|
||||
|
||||
## Semantic memory nodes
|
||||
|
||||
{keys_text}
|
||||
"""
|
||||
|
||||
|
||||
def call_sonnet(prompt: str) -> str:
|
||||
"""Call Sonnet via the wrapper script."""
|
||||
import tempfile
|
||||
|
||||
env = dict(os.environ)
|
||||
env.pop("CLAUDECODE", None)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt',
|
||||
delete=False) as f:
|
||||
f.write(prompt)
|
||||
prompt_file = f.name
|
||||
|
||||
try:
|
||||
scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
wrapper = os.path.join(scripts_dir, "call-sonnet.sh")
|
||||
|
||||
result = subprocess.run(
|
||||
[wrapper, prompt_file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=600, # monthly is bigger, give more time
|
||||
env=env,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Sonnet call timed out"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
finally:
|
||||
os.unlink(prompt_file)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
parts = sys.argv[1].split("-")
|
||||
year, month = int(parts[0]), int(parts[1])
|
||||
else:
|
||||
today = date.today()
|
||||
year, month = today.year, today.month
|
||||
|
||||
month_label = f"{year}-{month:02d}"
|
||||
print(f"Generating monthly digest for {month_label}...")
|
||||
|
||||
week_labels = get_weeks_in_month(year, month)
|
||||
print(f" Weeks in month: {', '.join(week_labels)}")
|
||||
|
||||
digests = load_weekly_digests(week_labels)
|
||||
if not digests:
|
||||
print(f" No weekly digests found for {month_label}")
|
||||
print(f" Run weekly-digest.py first for relevant weeks")
|
||||
sys.exit(0)
|
||||
print(f" {len(digests)} weekly digests found")
|
||||
|
||||
semantic_keys = get_semantic_keys()
|
||||
print(f" {len(semantic_keys)} semantic keys")
|
||||
|
||||
prompt = build_monthly_prompt(month_label, digests, semantic_keys)
|
||||
print(f" Prompt: {len(prompt):,} chars (~{len(prompt)//4:,} tokens)")
|
||||
|
||||
print(" Calling Sonnet...")
|
||||
digest = call_sonnet(prompt)
|
||||
|
||||
if digest.startswith("Error:"):
|
||||
print(f" {digest}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
output_path = EPISODIC_DIR / f"monthly-{month_label}.md"
|
||||
with open(output_path, "w") as f:
|
||||
f.write(digest)
|
||||
print(f" Written: {output_path}")
|
||||
|
||||
# Save links for poc-memory
|
||||
links_path = AGENT_RESULTS_DIR / f"monthly-{month_label}-links.json"
|
||||
with open(links_path, "w") as f:
|
||||
json.dump({
|
||||
"type": "monthly-digest",
|
||||
"month": month_label,
|
||||
"digest_path": str(output_path),
|
||||
"weekly_digests": [d["path"] for d in digests],
|
||||
}, f, indent=2)
|
||||
print(f" Links saved: {links_path}")
|
||||
|
||||
line_count = len(digest.split("\n"))
|
||||
print(f" Done: {line_count} lines")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue