Skip to content

Latest commit

 

History

History
283 lines (197 loc) · 13 KB

File metadata and controls

283 lines (197 loc) · 13 KB

Pattern Reinforcement

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.

Concept

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).

Reinforcement model

The job runs runPatternReinforcement() from packages/remnic-core/src/maintenance/pattern-reinforcement.ts using a storage interface that accepts any StorageManager-compatible implementation.

Cluster key

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.

What gets reinforced

The job:

  1. Clusters all active and superseded memories in the configured categories by cluster key. Forgotten, archived, quarantined, pending_review, and rejected memories are excluded.
  2. Picks the most-recent active member of each cluster with cluster.size >= minCount as the canonical.
  3. Stamps the canonical with reinforcement_count (total cluster size) and last_reinforced_at (ISO 8601). Provenance fields derived_from (source IDs) and derived_via: "pattern-reinforcement" are also written.
  4. Marks older duplicates status: "superseded" with a supersededBy pointer 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.

YAML frontmatter fields

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_jkl012

Knobs

Enable 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.

Recall boost

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.

X-ray surfacing

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.

CLI surface

The remnic patterns command group exposes pattern-reinforcement output. Both subcommands read from the active memoryDir and require no extra config.

remnic patterns list

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

remnic patterns explain <memoryId>

Shows the full reinforcement picture for a single canonical:

  • reinforcement_count and last_reinforced_at
  • derived_from source memory IDs (each cluster member's frontmatter.id) stamped by the maintenance job
  • Canonical body
  • Cluster members — memories whose supersededBy points 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.

Triggering the job

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.

Relationship to procedural memory

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.

Examples

Enabling pattern reinforcement

Minimal config to turn on the job with weekly cadence:

{
  "patternReinforcementEnabled": true
}

Enabling recall boost

{
  "patternReinforcementEnabled": true,
  "reinforcementRecallBoostEnabled": true,
  "reinforcementRecallBoostMax": 0.25
}

Restricting to preferences only

{
  "patternReinforcementEnabled": true,
  "patternReinforcementCategories": ["preference"],
  "patternReinforcementMinCount": 5
}

Running the job manually via MCP

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.

Acceptance criteria (from issue #687)

  • 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.