Self-improvement & memory¶
"This harness should be self-evolving and improving all the time so it only fails on one thing once and repairs itself, then evolves and gets better over time β like a self-improving loop or reinforcement learning. Plus long-term and short-term memory."
That is the requirement. This page explains how it is implemented, what it writes to disk, which knobs exist, and how to inspect or delete all of it.
Four packages do the work:
| Package | Responsibility |
|---|---|
pkg/memory | Four layers of memory: working, episodic, semantic, procedural |
pkg/evolve | Failure fingerprinting, repair rules, a policy bandit, reflection, regression checks |
pkg/learning | Turning a finished task into a prose lesson, and routing it into pkg/memory |
pkg/eval/metrics | Per-run metrics, baseline-vs-current comparison, offline replay |
Three rules apply to every part of it:
- Deterministic core, optional LLM. Nothing here needs a model. Distillation and reflection accept an optional summarizer and are merely better with one β never dependent on a small model getting a summary right.
- Bounded, prunable, safe to be wrong. Every collection has a hard cap, every store has a prune policy, and every read path returns a usable zero value on corrupt data. A memory system that grows without limit or crashes on a bad record is worse than none.
- Fully inspectable and reversible. Plain JSON, JSONL and Markdown.
rm -rf .slmcode/memory .slmcode/evolveis a supported operation.
1. Memory¶
Working memory (short-term, run-scoped)¶
Lives in process for the duration of a run. Holds the current task, its focus files, the last 24 tool calls with outcomes, open failures, decisions taken and a compact rolling summary.
RecordTool is on the hot path after every tool call, so it does no I/O, no token counting and no regular expressions β a handful of slice appends. Token counting happens only when the block is rendered.
The rendered block projects onto pkg/compact's MustPreserve schema, so a compacted run and a fresh run present the same headings in the same order: files read β files edited β commands and exit status β failed calls β decisions.
w := store.Working()
w.Start(runID, task, role)
w.Focus("pkg/http/client.go")
w.RecordTool(memory.ToolEvent{Tool: "ws_edit", Path: "β¦", OK: false, Error: "β¦"})
w.Resolve(fingerprint, "re-read then retried", "rule:rule_ab12")
block := w.Render(700) // tokens
Caps: 24 tool events, 12 files read, 12 files edited, 5 commands, 8 open failures, 8 resolved failures, 5 decisions, 16 focus files.
Episodic memory (long-term, per project)¶
One append-only JSONL record per completed task or turn: the query, plan, files changed, tools used, commands run, failures and how each was resolved, gate outcomes, tokens, wall time, model and a success verdict.
Recall uses a BM25F-style lexical scorer over the structured fields, not embeddings. The reasoning: recall runs on every task start, must be deterministic under CI, must work with zero embedding calls, and scores fields (paths, tool names, tags) where exact token overlap is the signal β a path token like runner.go is worth far more than its cosine similarity to anything. Embeddings would also need a cache keyed on a model that can change between runs, which is exactly the silent staleness this subsystem exists to avoid. pkg/retrieval remains the right tool for prose-heavy code chunks; episodes are not prose.
Precision is enforced two ways, because for a 7B model an irrelevant-but-plausible memory is worse than no memory:
- a coverage gate β an episode must contain at least 34 % of the distinct query terms. (A raw BM25 threshold cannot do this: its scale depends on corpus size, so on a fresh project every term looks common and every score collapses. Coverage is corpus-independent.)
- a relative floor β matches scoring below 45 % of the best match are dropped, so one strong hit does not drag in three weak ones for company.
Scores are then decayed by recency (45-day half-life) and nudged up 15 % for episodes that ended in success.
Semantic memory (long-term, distilled)¶
Durable, deduplicated, confidence-scored facts about this project: build and test commands that actually worked, the real module layout, conventions observed, gotchas, per-file summaries.
Distillation is pure counting β no model:
| Fact kind | Derived from |
|---|---|
command | commands seen β₯ 2 times with at least one success, plus their success ratio |
layout | the directories that change most often |
file | per-file change frequency |
gotcha | resolved failures, keyed by fingerprint, with the fix that worked |
convention | edit-format apply rate observed in this repo |
dependency | the project's primary language |
Confidence is a Beta(1,1) posterior mean: (support+1)/(support+contradict+2). A single sighting scores 0.67, not 1.0 β a hint, not a law. Observing the same subject with a different claim is a contradiction; once contradictions outweigh support the fact's text is replaced and its counters reset. That is how a fact decays when the project changes under it.
Numeric drift is not a contradiction: "works here (2/2 runs)" and "works here (7/8 runs)" are recognized as the same claim with fresher arithmetic. Without that, re-distilling would thrash the store back and forth forever.
Facts with "pinned": true are user-authored: never overwritten, never refuted, never pruned. Edit facts.json by hand to add one.
The rendered block targets β€ 400 tokens, grouped action-first (commands and gotchas before layout and files), at most 6 facts per kind.
The block is query-conditioned. Facts.RenderFor(query, budget) scores every eligible fact against the current task with the same BM25F ranker episodic recall uses, and within each kind the matching facts take the six slots first, in relevance order. Render(budget) is the unconditioned form and is byte-for-byte what RenderFor produces for an empty query β the conditioning is additive, so SEMANTIC.md and any caller without a task in hand are unchanged.
Relevance orders the block; it does not filter it. A fact that shares no tokens with the query is still a fact about this project, and emptying the block because a query happened to use different words would be a regression dressed up as precision. Candidates are ranked with a zero timestamp on purpose: staleness is already priced into Fact.Score, and decaying it twice would let a fresh but barely-relevant fact outrank an old and exactly-relevant one.
Procedural memory (cross-project, user-scoped)¶
Under ~/.slmcode/memory/: what works for a given model family and language. Namespaced by both, so a Python project's lessons never pollute a Go one and a lesson about qwen2.5-coder never leaks into gpt-4o-mini.
Model ids are folded to families by dropping quantization, parameter count and serving-format suffixes at the first such token:
Qwen3-Coder-30B-A3B-Instruct-MLX-4bit β qwen3-coder
qwen2.5-coder:7b-instruct-q4_K_M β qwen2.5-coder
deepseek-chat β deepseek
gpt-4o-mini β gpt-4o-mini
Best(topic, family, language) requires at least 3 observations before it will recommend anything. Lookup widens (family, language) β (family, *) β (*, language) β (*, *), never across languages before it has widened across models.
Lessons: the prose layer, and how it joins the typed one¶
After every wave and at the end of every run, pkg/learning turns finished and blocked board tasks into Lesson{TaskID, Kind, Text, At} β success, convention or failure β and mirrors them into .slmcode/MEMORY.md as Markdown bullets. That mirror is for humans; it is not the memory system.
Two things make it a real part of the loop rather than a write-only log.
Provenance survives the write. A bullet carries its task and timestamp in a trailing HTML comment:
- β Import cycle: pkg/graph must not import pkg/orchestrator <!-- slm task=T7 kind=failure at=2026-08-24T10:11:12Z -->
Invisible in any rendered Markdown, plain text in an editor, and exactly recoverable: RenderMarkdown and ParseMarkdown are inverses. Bullets written before this existed still parse, with the kind recovered from the glyph. StripProvenance removes the bookkeeping before anything reaches a prompt β a model has no use for it. Previously RenderMarkdown emitted only - <glyph> <Text> and dropped TaskID and At on the floor, which destroyed provenance at write time, before storage, so no later reader could recover it.
Every lesson becomes a Fact. learning.FactFromLesson maps a lesson onto semantic memory and RecordFacts folds it in, so a prose observation inherits the whole typed machinery for free:
| Lesson kind | Fact kind | Why |
|---|---|---|
failure | gotcha | a trap that cost a run β the definition of a gotcha |
convention | convention | |
success | convention | a pattern observed to work here |
| anything else | convention | an unrecognized kind is still an observation about this project |
The fact's subject β its identity β is the first five significant words of the lesson, lowercased, namespaced lesson:, with task ids and bare numbers removed. Task ids look stable but are per-run, so keeping them would both split one recurring claim across every run that made it and collide two unrelated claims that happened to be task 1. Five words is the leading noun phrase of a typical bullet: enough to separate two topics, coarse enough that two rival claims about the same topic actually meet and can contradict each other. Fact.Sources carries the originating task and run id, so provenance is queryable from facts.json and not merely readable in MEMORY.md.
What that buys, none of which flat Markdown could ever do: a lesson seen twice outranks one seen once (Beta posterior); a claim the project has outgrown is contradicted and eventually superseded instead of being injected forever; a heavily contradicted lesson stops being rendered; and the store prunes itself.
The Markdown mirror keeps being written. This is additive.
Selecting lessons for a prompt¶
learning.RecentAdaptiveMemoryFor(query, project, global, maxBytes) picks which stored lessons a worker actually sees. It ranks them against the current task with memory.RankText β the same BM25F scorer, coverage gate, relative floor and recency decay that rank episodes, factored out of recall.go so there is exactly one implementation to tune and to trust. Matching lessons lead, in relevance order; the remaining slots (10 lines, 1600 bytes, unchanged) are filled with the most recent lines, and lines are dropped whole rather than sliced mid-sentence. With no query the fallback is pure recency β deterministic, no model.
Removed: the keyword gate. Selection used to be an eleven-substring allowlist β
timeout, timed out, deadline, max_parallel, contention, smoke, qa_gate, acceptance, placeholder, stub, max retriesβ applied on the only path from a stored lesson to a future prompt. Any lesson without one of those words was written to disk and then silently discarded. Measured against this repo's ownMEMORY.md, four of seven lessons never survived. Worse, the list was a guess frozen at the moment it was typed: a project whose real recurring problem is an import cycle, a flaky fixture or a naming convention learned nothing, forever, because nobody had thought of those words.pkg/loop'srecentLessonLinescarried the same list and is gone too.
adaptiveGuidance's canned advice for the failure classes the harness understands natively (timeouts, contention, verification, placeholders) survives as enrichment β it says something concrete the raw lesson text does not. It no longer decides which lessons live.
2. Evolve: fail once, then never again¶
Failure fingerprinting¶
Any failure becomes a stable Fingerprint:
- Normalize the message β strip ANSI, cut stack traces, then replace timestamps, durations, URLs, IPs, hex addresses, hashes, paths, line:column pairs, quoted payloads and bare numbers with placeholders. Lowercase, collapse whitespace, cap at 300 bytes.
- Classify into one of 24 classes (
edit_not_found,edit_ambiguous,edit_line_numbers,file_not_read,malformed_json,truncated_output,compile_error,test_failure,timeout,context_overflow,provider_error,no_progress,permission_denied,out_of_scope_write, β¦). Needle matching uses word boundaries for bare words, so the identifierwaveTimeoutis not mistaken for a network timeout. - Hash class + tool + language + model family + a salient string.
The salient string is the interesting part. For structural classes β every old_str not found is the same problem regardless of which file or which text missed β the message is excluded from the hash entirely, so superficially different messages collapse to one fingerprint. For content classes (compile errors, test failures) the normalized message participates, so undefined: alpha and undefined: beta stay distinct while the same undefined: alpha in two different files collapses.
out_of_scope_write is structural for the same reason: the path, the role name and which of the guard's three refusal texts fired are all presentational, so every blocked write in a project shares one fingerprint and therefore one stored repair. Content-classing it would mint a fresh fingerprint per (role, path) pair and the harness would relearn the same lesson for the explorer, then the docs reader, then the reviewer. The role-specific instruction is not lost β it is stated by the guard's own refusal, which the agent reads directly above the repair guidance.
Repair rules¶
A rule is {Fingerprint, Trigger, Repair, Evidence, Successes, Failures, Confidence, CreatedAt, LastUsed, Scope}.
Repair is a small typed union, not free text, because the point is for the harness to apply a remembered fix rather than describe it to a model and hope:
| Kind | Effect | Costs an LLM call? |
|---|---|---|
guidance | inject text into the next prompt | yes (a targeted one) |
transform_args | rewrite the failed call's arguments with a named transform | no |
switch_tool | retry with a different tool | no |
edit_format | switch edit format for the retry | no |
config | change a config knob | no |
shell | propose a fixup command (run by the harness under the permission system) | no |
action | a named recovery: re-read, compact, raise max_tokens, split, back off⦠| no |
Named argument transforms: strip_line_number_prefix, set_replace_all, trim_trailing_whitespace, unfence_code, repair_json, shrink_old_str.
Confidence is a Beta posterior mean. Seeded rules start at Beta(4,1) β 0.80 β believed, because they encode failure modes we already understand. Synthesized rules start at Beta(1,2) β 0.33 β below the 0.45 apply bar, so a guess is suggested but not applied until it has proved itself. Rules gain confidence on success and lose it on failure; below 0.18 and with at least 4 samples they retire themselves. The sample floor is the guardrail: one unlucky early result cannot silently kill a good repair.
Lookup is exact-fingerprint first, then trigger patterns, ordered by confidence then trigger specificity.
The shipped rule set¶
These make the harness useful on day one:
| Failure | Repair |
|---|---|
ws_read line-number gutter leaked into old_str | transform_args: strip_line_number_prefix, retry |
old_str not found | action: reread_file β re-read, copy 2β3 lines verbatim, retry with a smaller uniquely-anchored span |
old_str missed on whitespace only | transform_args: trim_trailing_whitespace, retry |
old_str found N times | guidance β add surrounding context for a unique anchor; replace_all only if you mean it |
old_str empty | guidance β ws_write to create, anchor on the last lines to append |
No-op edit (old_str == new_str) | guidance β make a real change or finish |
| File edited before being read | action: reread_file, retry |
| Multi-hunk diff failure | edit_format: search_replace β then whole file as a last resort |
JSON truncated by max_tokens | action: raise_max_tokens β never guess past a truncation |
| Malformed JSON | transform_args: repair_json, retry |
| Context overflow | action: compact_context, retry |
| Repeated identical tool call | action: force_different_action |
| Reviewer rejected repeatedly | action: split_task |
| Shell command not permitted | guidance β propose the allowed equivalent, do not retry |
| Write outside the task focus files, or from a read-only role | action: force_different_action β a scope decision, not an edit-syntax problem: edit a focus file, or report the change and finish. Never reword the call |
| Path does not exist | action: reread_file β list before assuming a layout |
| Missing tool/module | guidance β report it, do not reimplement it |
| Rate limited | action: backoff_retry |
| Timeout | action: split_task |
Go: declared and not used | guidance |
Go: undefined: X | action: reread_file β grep before inventing |
| Python: indentation error | transform_args: unfence_code, retry |
To disable a shipped rule, set "retired": true on it in rules.json. Deleting it does not work β seeds are re-merged on every load.
What is learned from outcomes vs. what is measured up front¶
Two different subsystems, deliberately not merged:
| Learned by the bandit | Measured by calibration | |
|---|---|---|
| Answers | edit format, think passes, explore phase, review strictness, role model, retry ladder | concurrency knee, latency baseline, decode rate, context window |
| Evidence | run outcomes β did the patch apply, did the gate pass | a handful of tiny completions, seconds of wall clock |
| Cost to learn | many real runs | ~10-25 seconds, once per (model, endpoint) |
The split is about what a cheap synthetic probe can honestly tell you. Whether a second think pass improves a patch is unknowable without doing the work, so it belongs to the bandit. How many requests the server runs at once is observable in seconds and does not need a single real task, so guessing it from a provider name was never justified.
Calibration therefore seeds, and never competes:
- it seeds role-latency memory for roles with no observations yet, so a never-before-seen model does not spend its first runs on the full
task_timeoutceiling; - it seeds the decode-rate tracker per-call deadlines come from;
- it seeds no bandit posterior. A 16-token completion is evidence about none of the bandit's decisions, and the warm starts below already encode what is genuinely known up front. Inventing evidence would be worse than the uniform prior it replaced.
Policy learning: a bandit over harness choices¶
pkg/evolve runs a contextual multi-armed bandit keyed on (decision, model family, language) over the harness's discrete choices: edit format, which model handles a role, thinking passes, whether to run the explore phase, retry-ladder ordering, review strictness.
Thompson sampling over Beta posteriors, not UCB1. Four reasons:
- the reward is naturally a bounded [0,1] score, which makes Beta conjugate β an O(1) update and two floats per arm, both legible in the JSON you are invited to read;
- the sample counts are tiny. One developer on one project produces tens of observations per arm, not thousands. UCB1's confidence bound is only meaningful once every arm has been pulled and over-explores badly in the low-n regime β which here means deliberately using an edit format you already know applies 60 % of the time;
- warm starting is exactly expressible β a prior is "pretend we already saw Ξ± successes and Ξ² failures", so shipped defaults and learned evidence live on the same scale;
- deterministic mode is a one-line change (argmax of the posterior mean), and CI must be reproducible.
Reward function, in [0,1]:
correctness = 0.60Β·applied + 0.25Β·gate_passed + 0.15Β·(1 β min(retries,3)/3)
cost = 0.50Β·token_efficiency + 0.50Β·time_efficiency
reward = 0.85Β·correctness + 0.15Β·cost
hard failure β reward capped at 0.10
A gate that did not run scores 0.5 (neutral). Unknown budgets score 0.5, never a bonus and never a penalty. Correctness outweighs cost roughly six to one on purpose: the harness must never learn to prefer a cheaper option that produces broken code. Cost exists only to break ties between options that work equally well.
The Beta update is Ξ± += r; Ξ² += 1 β r, which keeps the posterior mean an unbiased estimate of expected reward for a bounded reward.
Warm start. Shipped priors, worth a handful of pseudo-observations each, so a fresh install behaves sensibly immediately:
edit_format: search_replace Ξ²(8,2) unified_diff Ξ²(3,5) whole_file Ξ²(4,4)
think_passes: 1 Ξ²(5,3) 2 Ξ²(5,4) 3 Ξ²(3,5)
explore: on Ξ²(6,3) off Ξ²(4,4)
review: normal Ξ²(6,3) strict Ξ²(4,4) lenient Ξ²(3,5)
Guardrails against locking in a bad arm:
- prior pseudo-counts are never removed, so no arm can be driven to certainty by a handful of samples;
- every arm must be pulled twice (
MinPulls) before sampling takes over; - an explicit Ξ΅ starts at 0.20 and decays with a 40-pull half-life, but never below 0.02 β a model upgrade or a refactor can change the answer, so a little exploration is permanent;
- once a key passes 200 pulls its posterior is decayed halfway back toward its prior, keeping it responsive and bounding the numbers on disk.
Deterministic mode (EngineOptions{Deterministic: true}, the --no-explore knob) replaces sampling with a greedy argmax and disables Ξ΅ entirely. Runs are then bit-for-bit reproducible.
Explaining a choice:
$ (via Bandit.Why)
edit_format (model qwen2.5-coder, language go) β 37 observations
β search_replace 91% Β±4% (28 pulls, Ξ±=26.4 Ξ²=2.6)
whole_file 62% Β±11% (6 pulls, Ξ±=5.1 Ξ²=3.1)
unified_diff 41% Β±13% (3 pulls, Ξ±=3.4 Ξ²=4.9)
mode: Thompson sampling, Ξ΅=0.11
Reflection¶
After each run, Reflect(RunReport) deterministically compares intent with outcome β tasks planned vs done, gates passed, retries, tokens, wall time, and every failure with how it was resolved β and emits:
- an
Episodeforpkg/memory; - candidate repair rules, synthesized only from failures that were fixed without an existing rule (those are the ones we do not yet know);
- bandit rewards for the choices the run made;
- regression checks for failures that were fixed;
.slmcode/memory/REFLECTION.md, a human-readable report.
An optional summarizer appends a "Model commentary" section labeled advisory only. It is strictly additive β an error, a timeout or an empty answer leaves the computed report byte-for-byte unchanged.
Regression memory¶
Every fixed failure is recorded with, where one exists, a cheap way to prove it has not come back: a command, a "file contains", a "file absent", or a "file exists" assertion. Regressions().Checks() hands them to the harness.
evolve never executes a command itself β the harness runs those under the permission system. RunOffline(root) evaluates only the file-based checks, which are safe.
3. Measurement¶
pkg/eval/metrics writes one record per run to .slmcode/metrics/runs.jsonl:
- task pass rate
- edit-format apply rate β first-class, because for a small model edit-format compliance is the bottleneck: a plan that is right and an edit that will not apply produce exactly zero working code. Aider's leaderboard reports "% of responses using the correct edit format" next to task success for the same reason.
- tool error rate, redundant-call rate
- LLM calls per task, tokens in/out, wall time
- gate outcomes
- repair-rule hit rate
- how many failures were resolved from memory vs from a fresh LLM round-trip
Compare(baseline, current) renders a Markdown delta. Rates are pooled (sum of numerators over sum of denominators), not averaged per run β averaging rates over runs of different sizes silently overweights the small ones. A metric with no data on either side reports "no data" rather than a fabricated zero.
## Metrics: 12 baseline run(s) β 12 current run(s)
| Metric | Baseline | Current | Change |
|---|---:|---:|---:|
| task pass rate | 58.3% | 75.0% | +16.7 pp β
|
| edit-format apply rate | 61.0% | 92.0% | +31.0 pp β
|
| failures fixed from memory | 0.0% | 68.0% | +68.0 pp β
|
| LLM calls per task | 7.20 | 4.90 | β2.30 β
|
**Verdict: improved.**
Offline replay¶
A stored trajectory is a recording of what a model actually emitted β tool calls, arguments, results β plus, for each failed step, the arguments that eventually worked. Replaying it against a Repairer (satisfied by *evolve.Rules) answers one precise question with no live model:
how many of these failures would this repair store have fixed deterministically, and how many would still have cost a round-trip?
fixtures, _ := metrics.LoadTrajectories("testdata/trajectories")
cmp := metrics.ABTest(fixtures, rules, "qwen2.5-coder")
fmt.Println(cmp.Render())
Both arms must land the same edits β the repair saves cost, it does not change correctness. If it changed correctness the A/B would be measuring two things.
4. On-disk layout¶
Everything is human-readable and safe to edit, version-control or delete.
<project>/.slmcode/
βββ MEMORY.md prose lessons, one bullet each, with provenance
βββ memory/
β βββ episodes.jsonl one JSON object per completed task/turn
β βββ episodes.index.json searchable projection + byte offsets
β βββ facts.json semantic memory (distilled + lessons, confidence-scored)
β βββ SEMANTIC.md human-readable mirror of facts.json
β βββ WORKING.md last run's short-term state (debug only)
β βββ REFLECTION.md last run's intent-vs-outcome report
βββ evolve/
β βββ rules.json project-scoped + builtin repair rules
β βββ regressions.json fixed failures and their re-checks
βββ graph/
β βββ edges.jsonl typed edges between the records above
β βββ edges.index.json adjacency index, rebuildable from the log
βββ metrics/
βββ runs.jsonl one metrics record per run
~/.slmcode/
βββ MEMORY.md cross-project prose lessons
βββ memory/
β βββ procedures.json cross-project: what works per model + language
β βββ PROCEDURES.md human-readable mirror
βββ evolve/
βββ rules.json user-scoped repair rules (model-level lessons)
βββ policy.json bandit posteriors
MEMORY.md is the human mirror; facts.json is what the harness reasons over. Deleting MEMORY.md loses the prose, not the learning.
All writes go through pkg/internal/atomicfile (temp file + rename), except the two append-only JSONL logs, which use a single write(2) per record β on POSIX a sub-PIPE_BUF append is atomic, so a crashed run leaves whole records, never a spliced one.
A file that fails to parse is moved aside to <name>.corrupt and the store starts clean, with the problem reported through Warnings(). A corrupt line in a JSONL log is skipped; the records either side of it survive. A stale index is detected and rebuilt from the log.
Bounds¶
| Store | Cap | Prune policy |
|---|---|---|
| Episodes | 300 records | also drops anything older than 180 days; the JSONL log is rewritten so the file shrinks too |
| Facts | 200 | drops confidence < 0.25 and anything unseen for a year; pinned facts are exempt |
| Procedures | 400 | drops entries unused for a year |
| Repair rules | 400 | drops retired and unused-after-a-year learned rules; seeded rules are never removed |
| Bandit keys | 300 | least-used first |
| Regression checks | 200 | oldest first |
| Metrics log | 2000 runs | oldest first |
| Graph edges | 20000 | drops edges not re-observed for 180 days, newest kept; the JSONL log is rewritten so the file shrinks too |
5. Knobs¶
| Knob | Where | Effect |
|---|---|---|
evolve | config / --evolve / --no-evolve | turn the whole subsystem on or off (default on) |
deterministic | config / --no-explore | greedy policy, no exploration β for CI and reproducible runs; dry_run implies it |
memory_tokens | config | token budget for the injected memory block (default 300) |
regression_checks | config | replay stored regression checks around the QA gate |
EngineOptions.Deterministic | evolve.OpenWith | the library-level form of deterministic |
EngineOptions.Seed | evolve.OpenWith | reproducible exploration |
EngineOptions.ReadOnly | evolve.OpenWith | open every store without writing |
EngineOptions.ProjectPolicy | evolve.OpenWith | keep bandit posteriors in the project instead of ~ |
EngineOptions.NoSeedRules | evolve.OpenWith | start with no shipped repair rules |
memory.Limits | memory.OpenWith | per-store caps and per-layer token budgets |
memory.PrunePolicy | Store.Prune | ages and counts |
evolve.RulePolicy | Rules.Prune | rule-store bounds |
Query.MinCoverage / MinScore | RecallEpisodes | recall precision |
"pinned": true | facts.json | a fact you wrote that must never be overwritten or pruned |
"retired": true | rules.json | disable a repair rule (including a shipped one) |
6. Inspecting and resetting¶
From the CLI¶
slmcode memory show --role worker # the memory block a role actually receives
slmcode memory show --budget 500 # β¦at a different token budget
slmcode memory episodes 20 # the most recent runs the harness remembers
slmcode memory facts --kind command # distilled semantic facts, filtered by kind
slmcode memory forget episodic --yes # working|episodic|semantic|procedural|project|all
slmcode evolve rules # repair rules with confidence and hit counts
slmcode evolve rules --all # include seeded-but-unused and retired rules
slmcode evolve why edit_format # the posterior table behind a learned choice
slmcode evolve regressions # stored regression checks and their status
slmcode evolve regressions --run # replay the offline (file-based) checks now
slmcode evolve reset --yes # rules, policy, regressions and memory
slmcode metrics show # the latest run
slmcode metrics show --last 10 # β¦plus an aggregate over the last 10
slmcode metrics compare 12 # newest 12 runs vs the 12 before them
Every one of these takes --json.
evolve why answers two questions and labels which is which. The bandit keys its posterior on decision | model family | language, so the tables it has learned are not all about this project. The command names the model family it is answering for, prints the tables recorded under that family first, and puts anything learned under a different model below a β other models (recorded, not used here) β divider. When the current family has no evidence it says exactly that β no evidence for this model yet β the harness uses the shipped default β instead of printing "no evidence yet" directly above a table of ten pulls, which is what it used to do.
From the shell¶
cat .slmcode/memory/SEMANTIC.md # distilled project facts
cat .slmcode/memory/REFLECTION.md # what happened last run
cat ~/.slmcode/memory/PROCEDURES.md # what works for your model
jq . .slmcode/evolve/rules.json # repair rules and their confidence
jq . ~/.slmcode/evolve/policy.json # bandit posteriors
jq -s 'length' .slmcode/metrics/runs.jsonl
Forget selectively, in code:
store.Forget(memory.ScopeWorking) // this run only
store.Forget(memory.ScopeEpisodic) // the run log
store.Forget(memory.ScopeSemantic) // distilled facts
store.Forget(memory.ScopeProcedural) // cross-project model lessons
store.Forget(memory.ScopeProject) // episodic + semantic
store.Forget(memory.ScopeAll) // everything, including ~/.slmcode
engine.Forget(memory.ScopeAll) // the above plus rules, policy, regressions
Or by hand β this is fully supported and breaks nothing:
The next run starts from the shipped repair rules and the shipped bandit priors β which is to say, it behaves exactly like a fresh install, and then starts learning again.
7. The knowledge graph (.slmcode/graph)¶
Everything above stores its cross-references as strings that nothing follows. Fact.Sources names the episodes a fact was distilled from, Rule.Evidence names what created a repair rule, Episode.FilesChanged names the files a turn touched, FailureNote.ResolvedBy names the rule that fixed a failure β and none of them could be traversed. Nothing could walk from a file to the failure classes it has produced to the rule that resolved them, because that join spans three files in two packages.
pkg/graph is a fourth store that materializes exactly those references as typed edges under .slmcode/graph:
| Record field | Edge |
|---|---|
Episode.RunID | run -parent_of-> episode |
Episode.FilesChanged | episode -touched-> file |
Episode.Failures[].Fingerprint | episode -produced-> failure |
FailureNote.ResolvedBy | failure -resolved_by-> rule |
Fact.Sources | fact -derived_from-> episode | run |
Rule.Evidence | rule -derived_from-> episode | run |
It adds no data of its own. Edges are content-addressed on (from, to, type), so a run backfills after every turn without the log growing, and node identity is exact β no fuzzy matching, no entity resolution, and a reference that resolves to no known record is dropped rather than invented. The same corrupt-line, stale- index and .corrupt rules described above apply, and rm -rf .slmcode/graph is a supported operation because one backfill rebuilds every edge.
slmcode graph stats # edges by type, node count, file size
slmcode graph file pkg/loop/runner.go # failure classes seen here, and what fixed them
slmcode graph neighbors <node> # one hop, --dir in|out|either, --type
slmcode graph walk <node> # bounded traversal, --depth (hard cap 6)
slmcode graph backfill # materialize edges (idempotent)
slmcode graph prune --max-age 720h # bound the store; rewrites the log
slmcode graph forget --yes # delete the index; backfill rebuilds it
Full reference, including the node-id scheme, the rest of the edge vocabulary and where the graph is not worth its cost: Knowledge graph.