Skip to content

Layer 4: Meta-Learning

The Fourth Question: How well is prediction working?

Layer 4 closes the feedback loop in the Wake Intelligence brain. While Layers 1–3 answer Why, How, and What, Layer 4 asks How well — and then adjusts.


Overview

Layer 3 (Propagation Engine) calculates prediction scores using a weighted formula:

score = temporal_weight * temporal_score
      + causal_weight   * causal_score
      + frequency_weight * frequency_score

The default weights are 0.4 / 0.3 / 0.3 — but these are not universally optimal. A long-lived documentation project favors frequency (revisited regularly). A sprint-style decision project favors causality (decision chains matter). A daily standup workflow favors recency.

Layer 4 observes which predictions were accurate and adjusts the weights per project.


How It Works

1. Record Outcomes

Every time update_predictions runs and a context is subsequently accessed, Layer 4 records a prediction_outcome:

{
  context_id: "...",
  project: "api-service",
  predicted_score: 0.78,
  was_accessed: true,
  temporal_score: 0.82,
  causal_score: 0.61,
  frequency_score: 0.74
}

2. Accumulate Evidence

Learning activates after 20+ outcomes for a project. Below 20, default weights are used.

3. Adjust Weights

With sufficient outcomes, Layer 4 computes which signal was most predictive and shifts the weights toward it:

typescript
// Simplified: move weights toward the signal that was right more often
newTemporalWeight += temporalAccuracyDelta * 0.05;
newCausalWeight   += causalAccuracyDelta   * 0.05;
newFrequencyWeight += frequencyAccuracyDelta * 0.05;

4. Clamp and Redistribute

After adjustment, weights are clamped to [0.1, 0.6] to prevent any signal from dominating or being ignored. The redistribute() function ensures the weights still sum to exactly 1.0 after clamping:

typescript
function redistribute(weights: ProjectWeights): ProjectWeights {
  const clamped = clampAll(weights);  // each weight → [0.1, 0.6]
  const total = sum(clamped);
  const deficit = 1.0 - total;
  // distribute deficit proportionally to non-pinned weights
  return distributeDeficit(clamped, deficit);
}

The invariant: temporal_weight + causal_weight + frequency_weight === 1.0 always holds.


Architecture

New Domain Types

typescript
interface PredictionOutcome {
  id: string;
  project: string;
  contextId: string;
  predictedScore: number;
  wasAccessed: boolean;
  timestamp: string;
  temporalScore?: number;
  causalScore?: number;
  frequencyScore?: number;
}

interface ProjectWeights {
  project: string;
  temporalWeight: number;   // default 0.4, clamped [0.1, 0.6]
  causalWeight: number;     // default 0.3, clamped [0.1, 0.6]
  frequencyWeight: number;  // default 0.3, clamped [0.1, 0.6]
  lastUpdated: string;
  outcomeCount: number;
}

Service: MetaLearningService

MetaLearningService lives in the domain layer and owns:

  • recordOutcome(outcome) — persist a prediction result
  • updateWeights(project) — recalculate weights from ≥20 outcomes
  • getWeights(project) — retrieve current weights (default if < 20 outcomes)
  • getLearningStats(project) — human-readable weight summary

Repository: IMetaLearningRepository

Port interface for outcome and weight storage:

typescript
interface IMetaLearningRepository {
  saveOutcome(outcome: PredictionOutcome): Promise<void>;
  findOutcomes(project: string, limit?: number): Promise<PredictionOutcome[]>;
  saveWeights(weights: ProjectWeights): Promise<void>;
  findWeights(project: string): Promise<ProjectWeights | null>;
}

Implemented by D1MetaLearningRepository (Cloudflare D1 adapter).


Database Schema

Two new tables (Migration 0005):

sql
CREATE TABLE prediction_outcomes (
  id TEXT PRIMARY KEY,
  project TEXT NOT NULL,
  context_id TEXT NOT NULL,
  predicted_score REAL NOT NULL,
  was_accessed INTEGER NOT NULL DEFAULT 0,
  timestamp TEXT NOT NULL,
  temporal_score REAL,
  causal_score REAL,
  frequency_score REAL
);

CREATE TABLE project_weights (
  project TEXT PRIMARY KEY,
  temporal_weight REAL NOT NULL DEFAULT 0.4,
  causal_weight REAL NOT NULL DEFAULT 0.3,
  frequency_weight REAL NOT NULL DEFAULT 0.3,
  last_updated TEXT NOT NULL,
  outcome_count INTEGER NOT NULL DEFAULT 0
);

MCP Tools

get_learning_stats

Returns current learned weights and outcome count for a project.

get_learning_stats({ project: "api-service" })

→ Temporal weight:   0.52  (default: 0.40)
  Causal weight:     0.28  (default: 0.30)
  Frequency weight:  0.20  (default: 0.30)
  Based on 47 outcomes.

See get_learning_stats tool reference.


Integration with Layer 3

PropagationService.calculateScore() accepts an optional ProjectWeights parameter. When Layer 4 weights exist for a project, they override the default 0.4 / 0.3 / 0.3:

typescript
const weights = await metaLearningService.getWeights(project);
const score = propagationService.calculateScore(context, weights);

This integration is transparent — the API and MCP tools work the same way, but scores improve over time as learning accumulates.


Monitoring

Use get_learning_stats periodically to check whether learning has activated and which signal is winning for each project:

typescript
// Check all active projects
for (const project of activeProjects) {
  get_learning_stats({ project });
}

If a project's temporal weight has grown to 0.58, its access patterns are strongly time-based — contexts accessed recently are reliably accessed again.


See Also