Every time someone demos a "self-improving AI system," there's a moment where it feels like something profound happened. The agent noticed its own failure, diagnosed the cause, proposed a fix, ran the fix, and verified the result. Autonomously.
Then I look at what I actually built. Five files, 4,038 lines of Go, 151 test functions spread unevenly across them — and the most important insight I can share is that nothing in this system is novel. It's the same control loop that runs a thermostat. Observe, score, act, measure, repeat. The magic is the implementation discipline, not the concept.
This is a tour of that implementation.

The Loop Is a Thermostat
The system's self-improvement loop has five stages, and none of them are surprising once you name them.
Observe. Every mission that completes deposits learnings into a SQLite database. Not general notes — structured entries with type, domain, content, and an optional embedding vector. The learnings database currently has 997 lines of query and schema logic dedicated to writing, deduplicating, scoring, decaying, and archiving these entries.
Score. The learnings get scored. Not a thumbs up or down — a float between 0 and 1 that encodes how often an agent actually followed the learning when it was injected into context. That score decays over time at different rates depending on compliance behavior. It's a credit score for each learning.
Propose. When an agent notices a pattern of failures — same error type, same ability, three or more occurrences — it proposes a fix mission. The proposals are rate-limited, deduplicated by a SHA-256 key, and gated by a 24-hour cooldown so the system doesn't spam the work queue.
Execute. The proposed fix runs as a three-phase mission: investigate, fix, verify. Standard orchestrator work. Nothing special here.
Feedback. The outcome of the fix — success, failure, or stall — feeds back into a quality score for that category of proposal. Future proposals in the same category are TTL-adjusted based on that history: high success rate means longer dedup windows (fewer redundant proposals), low success rate means shorter windows (faster retries).
That's it. The word "self-improving" describes all five stages together, which is why it sounds more interesting than it is.
The Sensor Layer
The most underappreciated part of the loop is the observation step. If you don't observe accurately, everything downstream is garbage.
learning/db.go is the sensor. It's 997 lines long and has 16 test functions — the second-lowest test coverage of the five core files. The work it does:
Dedup on ingest. When a new learning arrives, the system computes cosine similarity against existing entries. If the similarity exceeds 0.85, it's a duplicate — the seen_count of the existing entry increments and the new entry is discarded. The threshold is inline, not a named constant (line 185).
matchID, sim := d.findMostSimilar(l.Domain, l.Content, l.Embedding)
if matchID != "" && sim > 0.85 {
d.db.Exec("UPDATE learnings SET seen_count = seen_count + 1 WHERE id = ?", matchID)
return nil // duplicate
}Score on retrieval. When an agent requests relevant learnings for a mission, the query runs a hybrid search and scores each candidate:
// final = relevance * 0.5 + quality_score * 0.3 + recency_score * 0.2
score := relevance*0.5 + l.QualityScore*0.3 + recencyWeight(l.CreatedAt)*0.2Relevance gets 50% of the weight. Quality history gets 30%. Recency gets 20%. A learning that's relevant but old and ignored will score lower than a fresh one on the same topic. The recency weights are explicit: under 30 days gets 1.0, under 90 days gets 0.8, under 180 days gets 0.6, older than that gets 0.4 (line 439–447).
Decay over time. Every entry older than 30 days has its quality_score decayed on each cleanup cycle. The decay rate depends on compliance behavior:
| Condition | Rate |
|---|---|
| Never injected | 5% per 30 days |
| Low compliance (< 0.3) | 15% per 30 days |
| Mid compliance | 5% per 30 days |
| High compliance (≥ 0.7) | 2% per 30 days |
An entry that gets injected constantly and followed consistently decays at 2% per 30 days. An entry that gets injected and ignored decays at 15%. The floor is 0.05 — entries don't vanish, they just approach irrelevance.
Archive when irrelevant. Four conditions trigger archival (lines 843–871): never seen after 90 days, five injections with less than 10% compliance, quality below 0.2 with no usage after 60 days, or single observation with no embedding after 30 days. The system prunes its own memory.
None of this is sophisticated. It's just accounting — the kind you'd do for any inventory system.

The Quality Feedback Loop
The quality signal that drives quality_score in the sensor layer comes from plugins/nen/ko/quality.go and plugins/nen/cmd/shu/propose.go. These two files total 1,705 lines and carry 85 of the 151 test functions across the system.
The scoring formula in quality.go (lines 112–119) is Laplace smoothing:
func ComputeScore(successes, failures, stalls int) float64 {
total := successes + failures + stalls
if total < MinSamplesForConfidence {
return DefaultQualityScore // 0.5
}
weighted := float64(successes) + StallWeight*float64(stalls)
return (weighted + 1.0) / (float64(total) + 2.0)
}Three things to notice. First: you need at least 3 samples before the score moves off 0.5. Second: stalls count as 0.25 of a success — partial credit for proposals that opened but didn't close (StallWeight = 0.25, a 48-hour threshold determines when open becomes stall). Third: a perfect 3-for-3 record produces 4/5 = 0.8, not 1.0. The +1/+2 prior pulls scores toward 0.5 to keep the system responsive to future failures. You don't get a perfect score until you've accumulated more evidence than three attempts.
That last point matters. It means a category that worked three times in a row is still treated with some skepticism. Which is right — three data points is not a lot.
The proposal side (propose.go) feeds outcomes back through effectiveOpenTTL (lines 741–746):
func (p dedupBlockingPolicy) effectiveOpenTTL() time.Duration {
if p.qualityMultiplier == 0 {
return p.staleOpenTTL
}
return time.Duration(float64(p.staleOpenTTL) * 2 * p.qualityMultiplier)
}The base TTL is 24 hours. Quality score 0.5 (neutral, no history) → 2 × 0.5 × 24h = 24h, unchanged. Quality score 0.8 → 2 × 0.8 × 24h = 38.4h. Quality score 0.2 → 2 × 0.2 × 24h = 9.6h. High-performing proposal categories get longer dedup windows — the system backs off and lets successful fixes run. Low-performing categories get shorter windows — the system tries again sooner.
The rate limiter is blunt: maximum 5 proposals per cycle (rateLimitMax = 5). Batch proposals trigger at 3 or more findings in the same category (line 157). There's a 24-hour cooldown between proposals for the same ability/category pair.

Memory Is a Budget Problem
The quality feedback loop and the sensor layer are about what the system knows. Memory is about what it tells agents at runtime.
I wrote about the memory system when I was calling it Via. The self-improvement angle is about promotion: how a learning moves from an individual persona's memory to the global memory that all agents share.
memory.go is 1,167 lines with 49 test functions. Three constraints shape everything it does.
The ceiling. Global MEMORY.md caps at 100 non-empty entries (memoryCeilingLines = 100). Entries beyond the cap move to MEMORY_ARCHIVE.md, oldest first. This keeps the global context window predictable. You can't inject 400 learnings and expect an agent to act on all of them.
The seed budget. When an agent starts a mission, the seeding step selects entries from memory to inject into the system prompt. The budget is 4,096 bytes (seedMemoryBudgetBytes = 4096). Global entries go first, then persona-specific entries fill remaining space. The selection is greedy — ranked by Jaccard(entry.keywords, objective.keywords) × exp(-0.02 × age_days). A learning with a 35-day half-life that matches the mission objective beats a year-old one with perfect keyword overlap.

Auto-promotion. When a persona's entry is used 3 or more times (usedPromotionThreshold = 3), it promotes automatically to global. The idea: if multiple missions found this learning relevant enough to use, it's probably useful across contexts, not just for one persona.
The safety gate that runs on every incoming entry is worth noting separately. Twelve compiled regular expressions check for imperative language patterns — "ignore previous instructions," "you are now," "pretend to be," "override your" — plus a Unicode check for invisible characters (U+200B–U+200F, U+202A–U+202E). An entry that trips any of these goes to MEMORY_QUARANTINE.md. Prompt injection through the memory system is a real attack surface, and the defense here is pattern-matching, not LLM judgment. That's probably right — you don't want to use an LLM to detect prompt injection when the LLM itself is what you're protecting.

The Gap Nobody Tested
promote.go is 169 lines. It has 1 test function covering 2 public functions — the lowest test density of the five files, by a wide margin. The other files: memory.go has 49, quality.go has 26, propose.go has 59, learning/db.go has 16.
promote.go handles the type mapping when a learning moves from the structured Learning type used by the nen system to the MemoryEntry type used by the worker's memory system. The mapping (lines 154–169):
TypePattern → "pattern"
TypeError → "feedback"
TypeSource → "reference"
TypeDecision → "feedback"
TypeInsight → "user"
default → "feedback"Both TypeError and TypeDecision map to "feedback," which means any retrieval or filtering that distinguishes between those two types in the memory system will treat them identically. Whether that's correct depends on how downstream tools use the type field. One test function means that question hasn't been answered by the test suite.
This is the part of the system I'd fix before claiming the loop is complete. The promotion path from learned insight to injected memory to agent behavior is where the whole thing pays off — and it has the least test coverage. That's not a coincidence. The easy parts get tested first. The hard parts slip.
I mentioned this in the AI Memory Gold Rush newsletter: the value of memory isn't in storing it, it's in surfacing the right thing at the right time. promote.go is the handoff between those two problems.
What "Self-Improving" Actually Means
After looking at all five files, here's what I think the phrase means in practice: the system gets better at knowing what not to propose. That's it.
The quality feedback loop doesn't make agents smarter. It makes the proposal system more conservative about categories that keep failing and more willing to try categories that keep working. The memory system doesn't give agents better judgment. It gives them more relevant context from past sessions. Both of these are good things. Neither of them is magic.
What I still don't know: when to trust a score. The quality score for a given ability/category pair is a function of mission outcomes — but mission outcomes are noisy. A stalled proposal might reflect a bad fix, or it might reflect a good fix that hit an unrelated blocker. A failed proposal might represent a genuine dead end, or it might represent an approach that was close but needs a different strategy. The Laplace formula doesn't know the difference. It just counts outcomes.
The open question for me is whether 3 samples is the right minimum, or whether MinSamplesForConfidence = 3 was just what felt reasonable when I wrote the constant. I haven't run enough cycles on any single ability/category pair to know if the system converges to useful behavior or oscillates between too-many-retries and not-enough-retries. That would take months of production usage to answer.
For now the honest claim is: the system has all the right moving parts, most of them are well-tested, one critical handoff isn't, and the calibration question is unanswered. That's what 4,038 lines of boring engineering looks like from the inside.