Repository navigation
Memory System
How cadillac accumulates and uses experience across builds. Source: cadillac/memory.py + memory.jsonl at the repo root.
Currently: 403 lessons, 222 do / 181 dont, 98.8% reuse rate, avg confidence 0.945.
@dataclass
class Lesson:
ts: float # unix timestamp written
type: str # error_pattern | architecture | tool_pattern | dependency | performance
trigger: str # one-line description of the situation
fix: str # what to do (or NOT do — see polarity)
confidence: float = 0.5 # [0, 1], reinforced by successful reuse, decayed by backfire
used: int = 0 # how many times recalled
polarity: str = "do" # "do" = positive pattern; "dont" = anti-pattern
tags: list = [] # ["python", "flask", "sqlite", ...]
source_task: str = "" # originating task text (truncated to 200 chars)JSONL on disk, one lesson per line. Inspect with any jq / grep / python3 -c "json.loads(line)" you want.
-
Reflection extracts — after each phase ends (success or failure),
_run_phase_reflection()calls the LLM to extractTYPE | TRIGGER | FIXlines from the round's observations. -
parse_reflection()— parses LLM output into Lesson records:- Fresh confidence: 0.3 (lowered from 0.5 in the 2026-05-26 hygiene pass) — forces 2+ reuses before outweighing scoring noise floor
- Tags: explicit from caller, or inferred from trigger+fix via
infer_tags_from_text()if caller omits -
source_task: the build's original task text, truncated to 200 chars
-
save_lesson()— atomic append tomemory.jsonl(cross-process locked via fcntl) -
deduplicate()— O(n) token-bucket dedup on the trigger text. New lessons whose trigger overlaps an existing lesson by >70% are merged in.
recall(task_description, limit=10) returns the top-N most relevant lessons for the current task. Scoring formula:
base = (keyword_overlap × 2) + recency_score + confidence + (used × 0.1)With three filtering / decay layers:
task_tags = infer_task_tags(task_description) # known stack words
if task_tags:
lessons = [l for l in lessons if not l.tags or set(l.tags) & task_tags]A lesson with tags=["typescript", "react"] is filtered out of a Flask task. Untagged lessons survive the filter — they're treated as stack-agnostic.
For lessons with a populated source_task field — a lesson whose originating task has zero stack-tag overlap with the current task gets score /= 5.0.
if task_tags and lesson.source_task:
src_tags = infer_tags_from_text(lesson.source_task)
if src_tags and not (src_tags & task_tags):
total /= 5.0Why: a lesson from an IRC asyncio task should not outrank a stack-matched Flask lesson on a Flask task, even if its confidence + used count are higher. Same-language counts as same-stack (Python-Python transfer is real).
For legacy lessons without source_task (pre-2026-05-26), apply a softer penalty when the lesson's body text strongly implies a different stack:
if task_tags and not lesson.tags and not lesson.source_task:
content_tags = infer_tags_from_text(trigger + " " + fix)
if content_tags and not (content_tags & task_tags):
total /= 2.0Catches the case where a lesson mentions tsconfig.json in its body but happens to be untagged — it's clearly a TypeScript lesson and shouldn't outrank a Python-tagged one on a Python task.
Two functions:
Strict — only literal matches against the canonical _KNOWN_TAGS set. Used on the task description.
_KNOWN_TAGS = {
"python", "typescript", "react", "vue", "angular", "electron",
"go", "rust", "wordpress", "browser_extension", "pytorch",
"flask", "django", "fastapi", "express", "node",
"pytest", "vitest", "jest", "phpunit",
"sqlite", "asyncio", "aiosqlite", ...
"plan", "deps", "scaffold", "review", "build", "integrate",
"wiring", "validate", "package", # phase tags
}Broader — used on arbitrary text (lesson body, source_task). Adds:
-
Synonym expansion:
"aiosqlite"→{python, sqlite, asyncio},"tsc"→{typescript},"cargo"→{rust},"uvicorn"→{python, fastapi} -
File-extension hints:
.py→ python,.ts/.tsx→ typescript,.rs→ rust,.go→ go
So a lesson whose source_task is "async IRC server with websocket fanout" infers {python, asyncio} even though the literal word "python" isn't in it.
After a build succeeds, the lessons whose triggers matched a recalled-and-applied pattern get confidence += 0.1 (clamped to 1.0) and used += 1.
After a build that consumed a lesson THEN hit an error containing that lesson's trigger words, the lesson gets confidence -= 0.1. This catches a lesson that backfired — it advised something that turned out to be wrong in the current context.
Returns the list of penalized triggers for emit/visibility — you can see in the build log when a recalled lesson got dinged.
Called on every recall. Lessons older than 90 days lose confidence linearly until they're either reinforced or drop off relevance. Prevents long-tail lessons from staying authoritative forever.
format_for_prompt(lessons) renders the recalled lessons into the system prompt:
## Lessons from past builds (apply these):
- [error_pattern] When: vitest 0.34 + jsdom on Node 18 -> Do: pin vitest to ^1 in devDependencies
- [architecture] When: SQLite write during async handler -> Do: use aiosqlite with async context manager
...
## Anti-patterns from past builds (NEVER do these):
- NEVER: hardcoding CORS origins in source code (Reason: breaks builds on different machines)
- NEVER: f-string SQL with user input (Reason: SQL injection — use parameterized queries)
The do/dont split is honored — anti-patterns get the explicit NEVER framing so they don't get accidentally adopted as positive guidance.
When cadillac improve lands a patch and the matrix score lifts by ≥0.05, commit_applied() writes an architecture-class lesson:
Lesson(
type="architecture",
trigger=f"weakness {weakness_id} in {target_file}",
fix=f"matrix score improved by +{delta:.3f} on iter {iter}: {reason}",
confidence=0.6,
polarity="do",
tags=["cadillac", "self"],
source_task="cadillac improve cycle",
)Future cadillac improve iterations recall these and see "what kinds of changes have moved the score before." The autobuilder accumulates institutional memory about its own evolution.
A second JSONL file at the repo root: phase_budgets.jsonl. Schema:
{"ts", "phase", "rounds", "n_files", "tags"}After each completed phase, record_phase_outcome() appends one row. Next build's budget for that phase is computed by compute_budgets():
matching = filter(history, lambda r: tag_overlap(r.tags, task_tags))
budget[phase] = max(DEFAULT_BUDGET[phase], p90(matching.rounds) * 1.2)Two failure modes this fixes:
- Static-too-small — pygame BUILD historically needs 45 rounds; static budget of 30 was a premature abort
- Static-too-large — simple Python CLI BUILD p90 is 8 rounds; 30 was wasted LLM time
The corpus is just JSONL — interrogate it directly:
# Total lessons
wc -l memory.jsonl
# Lessons by type
python3 -c "import json; from collections import Counter; print(Counter(json.loads(l)['type'] for l in open('memory.jsonl')))"
# Top-used lessons
python3 -c "
import json
lessons = sorted([json.loads(l) for l in open('memory.jsonl')], key=lambda x: -x['used'])
for l in lessons[:10]:
print(f\"{l['used']:3d}× {l['trigger'][:80]}\")
"
# Lessons matching a tag
python3 -c "
import json
for line in open('memory.jsonl'):
l = json.loads(line)
if 'flask' in l.get('tags', []):
print(l['trigger'][:80])
"- Improve Cycle — how meta-lessons get seeded
- Resilience Stack — what triggers reflection
- Architecture — where in the phase loop reflection fires