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>
2026-02-28 22:17:00 -05:00
|
|
|
#!/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]:
|
2026-02-28 23:32:47 -05:00
|
|
|
"""Get semantic memory keys from the store."""
|
|
|
|
|
from store_helpers import get_semantic_keys as _get_keys
|
|
|
|
|
return _get_keys()
|
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>
2026-02-28 22:17:00 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|