Pattern reinforcement (issue #687) generalizes the procedural memory miner into a universal mechanism: any observation that recurs across sessions is merged into a single reinforced primitive with a confidence boost, regardless of whether it is a procedure, a fact, or a preference.
This feature tracks issue #687.
Also see: Procedural memory (the procedure-specific miner that ships alongside), Recall X-ray (surfacing reinforcementBoost in score decomposition), Config reference.
The procedural miner already detects recurring multi-step runbooks. Pattern reinforcement extends that principle to all configurable memory categories:
- A user expressing the same preference across 30 sessions → reinforced preference primitive.
- A debugging pattern recurring across 20 sessions in different repos → reinforced engineering practice.
- The same project context referenced repeatedly → reinforced project anchor.
The procedural miner is unchanged. Pattern reinforcement runs as a separate maintenance job on a configurable cadence and considers only the categories you configure (default: preference, fact, decision).
The job runs runPatternReinforcement() from packages/remnic-core/src/maintenance/pattern-reinforcement.ts using a storage interface that accepts any StorageManager-compatible implementation.
Each memory is keyed by category::normalizedContent:
normalizedContent = content.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 200)
key = `${category}::${normalizedContent}`
Truncating to 200 characters means long-form content with a stable opening still clusters together even when the tail differs slightly. The category prefix ensures that identical text in different categories (e.g., a fact and a decision with the same wording) is never cross-superseded.
The job:
- Clusters all active and superseded memories in the configured categories by cluster key. Forgotten, archived, quarantined, pending_review, and rejected memories are excluded.
- Picks the most-recent active member of each cluster with
cluster.size >= minCountas the canonical. - Stamps the canonical with
reinforcement_count(total cluster size) andlast_reinforced_at(ISO 8601). Provenance fieldsderived_from(source IDs) andderived_via: "pattern-reinforcement"are also written. - Marks older duplicates
status: "superseded"with asupersededBypointer to the canonical's ID.
The job is idempotent for the counter: re-running on the same corpus does not double-bump reinforcement_count. The bump-only-on-change guard compares cluster size to the canonical's previous counter and bumps only when it grew.
Note that the canonical's frontmatter can still be rewritten on a re-run when the cluster membership changes (new sources joined or older sources rotated out, even at the same total count) or when derived_via needs to be set to "pattern-reinforcement" for the first time. In those cases the maintenance job updates derived_from and updated to keep provenance accurate, while leaving reinforcement_count unchanged. Operators tuning write churn should treat these refresh writes as the steady-state cost of accurate provenance, not as counter drift.
Reinforced canonicals carry these additional fields:
reinforcement_count: 12
last_reinforced_at: "2026-04-27T08:00:00.000Z"
derived_from:
- mem_abc123
- mem_def456
- mem_ghi789
derived_via: "pattern-reinforcement"Superseded duplicates carry:
status: superseded
supersededBy: mem_jkl012Enable the job in plugin config:
{
"patternReinforcementEnabled": true,
"patternReinforcementCadenceMs": 604800000,
"patternReinforcementMinCount": 3,
"patternReinforcementCategories": ["preference", "fact", "decision"]
}| Key | Default | Notes |
|---|---|---|
patternReinforcementEnabled |
false |
Master gate. Set to true to enable the maintenance job. |
patternReinforcementCadenceMs |
604800000 (7 days) |
Minimum milliseconds between runs. Set to 0 to disable cadence gating (run on every invocation of the MCP/cron trigger). |
patternReinforcementMinCount |
3 |
Minimum cluster size before a canonical is promoted. Clamped to [2, 1000]; clusters of 1 are degenerate. |
patternReinforcementCategories |
["preference", "fact", "decision"] |
Categories the job scans. Empty array means no categories are processed. |
The cadence guard is entirely in-memory and is NOT derived from the last_reinforced_at field written to memory frontmatter. The orchestrator keeps a lastPatternReinforcementAtByNs Map (keyed by namespace) that records the epoch-ms timestamp when each run completes. If Date.now() - lastRunAt < patternReinforcementCadenceMs, the job returns early with skippedReason: "cadence".
Because the map is in-process, it resets on every process restart. A freshly restarted gateway will always run the job on the first invocation that follows, regardless of when the previous process last ran it. Operators who need cross-restart cadence control should rely on external scheduling — for example a system cron job that calls the remnic.pattern_reinforcement_run MCP tool on a fixed interval — rather than the in-process gate alone. Set patternReinforcementCadenceMs: 0 to disable cadence gating entirely and run on every invocation.
Reinforced primitives can be weighted higher in recall. This is opt-in (reinforcementRecallBoostEnabled: false by default):
{
"reinforcementRecallBoostEnabled": true,
"reinforcementRecallBoostMax": 0.3
}| Key | Default | Notes |
|---|---|---|
reinforcementRecallBoostEnabled |
false |
When true, memories with reinforcement_count > 0 receive an additive score boost. |
reinforcementRecallBoostMax |
0.3 |
Maximum additive reinforcement boost per result. Range [0, 1]. The raw boost is reinforcementRecallBoostWeight × reinforcement_count, clipped to this cap. |
A third key reinforcementRecallBoostWeight (default 0.05) controls the per-unit boost. The formula:
boost = min(reinforcementRecallBoostMax, reinforcementRecallBoostWeight × reinforcement_count)
A memory reinforced 12 times with default weight and max would receive min(0.3, 0.05 × 12) = min(0.3, 0.6) = 0.3 — the cap.
When reinforcementRecallBoostEnabled is on and a result actually received a non-zero boost (reinforcementBoost > 0), Recall X-ray attaches the value to the per-result explain object. The shared X-ray renderer formats it inline as reinforcement_boost=<value> alongside the other score components (importance, mmr_penalty, tier_prior, etc.). Results that did not receive a boost simply omit the field.
This makes it easy to audit which results were boosted by pattern reinforcement vs. which won on raw relevance. See Recall X-ray for the full per-result explain schema.
The remnic patterns command group exposes pattern-reinforcement output. Both subcommands read from the active memoryDir and require no extra config.
Lists memories whose reinforcement_count > 0, sorted by count descending.
remnic patterns list [--limit N] [--category cat1,cat2] [--since ISO] [--format text|markdown|json]| Flag | Default | Description |
|---|---|---|
--limit N |
50 |
Maximum rows to show (positive integer). |
--category list |
all categories | Comma-separated category filter (e.g. preference,fact). |
--since ISO |
all time | Only include memories reinforced on or after this ISO 8601 timestamp. |
--format fmt |
text |
Output format: text, markdown, or json. |
Example output (--format text):
Pattern memories (3):
[12x] mem_jkl012 (preference, last_reinforced=2026-04-27T08:00:00.000Z, status=active)
prefer short inline comments over block comments for single-line notes...
path: memories/preferences/mem_jkl012.md
[8x] mem_abc456 (fact, last_reinforced=2026-04-20T10:00:00.000Z, status=active)
the project uses pnpm workspaces...
path: memories/facts/mem_abc456.md
[5x] mem_def789 (decision, last_reinforced=2026-04-15T14:30:00.000Z, status=active)
decided to use the port/adapter pattern...
path: memories/decisions/mem_def789.md
Shows the full reinforcement picture for a single canonical:
reinforcement_countandlast_reinforced_atderived_fromsource memory IDs (each cluster member'sfrontmatter.id) stamped by the maintenance job- Canonical body
- Cluster members — memories whose
supersededBypoints at this canonical
remnic patterns explain <memoryId> [--format text|markdown|json]Exits with code 1 and a descriptive error if <memoryId> is not found or has no reinforcement_count > 0.
Invalid flag values (--format xml, --limit 0, --since not-a-date) throw a listed-options error rather than silently defaulting (see CLAUDE.md rule 51).
Example:
$ remnic patterns explain mem_jkl012
Pattern: mem_jkl012
reinforcement_count: 12
last_reinforced_at: 2026-04-27T08:00:00.000Z
category: preference
status: active
derived_via: pattern-reinforcement
path: memories/preferences/mem_jkl012.md
Canonical content:
prefer short inline comments over block comments for single-line notes.
Derived from (2):
- mem_abc123
- mem_def456
Cluster members (2):
- mem_abc123 (status=superseded, supersededAt=2026-04-27T08:00:00.000Z)
prefer terse implementation comments.
- mem_def456 (status=superseded, supersededAt=2026-04-27T08:00:00.000Z)
avoid block comments unless they explain a larger invariant.
Run `remnic patterns explain mem_jkl012 --format json` for machine-readable output.Pattern reinforcement is not triggered automatically by the Dreams REM phase. The runtime call site is EngramAccessService.patternReinforcementRun, which is exposed through:
- MCP tool:
remnic.pattern_reinforcement_run(canonical) /engram.pattern_reinforcement_run(legacy alias) - Maintenance scheduler / cron: the job can be registered as a standalone maintenance cron entry
To trigger an ad-hoc run, call the MCP tool directly:
{ "name": "remnic.pattern_reinforcement_run", "arguments": {} }Pass "force": true to bypass the in-process cadence gate for an immediate run regardless of when the last run completed.
See Dreams: phased consolidation for the Dreams pipeline; pattern reinforcement scheduling is independent of it.
Pattern reinforcement and the procedural miner are siblings, not replacements:
| Aspect | Procedural miner | Pattern reinforcement |
|---|---|---|
| Input | Causal trajectory records | All memories in configured categories |
| Cluster key | ${goal}|${entityRefs} from trajectory |
${category}::${normalizedContent(200)} |
| Output | category: procedure with ordered steps |
reinforcement_count + last_reinforced_at on any category |
| Min threshold | procedural.minOccurrences (default 3) |
patternReinforcementMinCount (default 3) |
| Config gate | procedural.enabled (default true) |
patternReinforcementEnabled (default false) |
| Recall injection | Task-initiation procedure block | Score boost via reinforcementRecallBoostEnabled |
Procedure memories themselves are not in the default patternReinforcementCategories list, so the two pipelines do not interfere.
Minimal config to turn on the job with weekly cadence:
{
"patternReinforcementEnabled": true
}{
"patternReinforcementEnabled": true,
"reinforcementRecallBoostEnabled": true,
"reinforcementRecallBoostMax": 0.25
}{
"patternReinforcementEnabled": true,
"patternReinforcementCategories": ["preference"],
"patternReinforcementMinCount": 5
}Pattern reinforcement has its own MCP tool:
{ "name": "remnic.pattern_reinforcement_run", "arguments": {} }Pass "force": true to bypass the in-process cadence gate. The legacy alias engram.pattern_reinforcement_run also works.
For the separate procedural miner, use remnic.procedure_mining_run.
- Bench fixture: 30 sessions repeating the same preference; reinforcement merges them into one primitive within one maintenance cycle.
- Reinforced primitives outrank one-shot equivalents in recall on a controlled fixture (requires
reinforcementRecallBoostEnabled: true). remnic patterns explain <id>traces a reinforced primitive back to its sources.- Procedural-miner behavior unchanged.