Procedural Memory Is a Pattern, Not a Fact

This post assumes you’re already familiar with procedural memory in the agent memory management paradigm. If that’s not the case, the first post will give you enough context before you continue.

There are three places an agent can be wrong about a fact it has about you: extraction, retrieval, and resolution. All three assume the fact came out of a conversation.

Some of the things an agent believes about you were never in any conversation. They came from the bookkeeping.

A user opens the gift-shop chatbot for the fifth time this quarter. Before she finishes her first message, the agent has already warmed up a product-comparison view. She never asked it to. But every previous time she’s landed on this page around this date, she’s compared three options within the next two turns. The agent isn’t guessing. It’s acting on a pattern it noticed and stored — one the user never explicitly authorized, and probably doesn’t know exists.

That storage is procedural memory. And its trust semantics are unlike anything the first post covered.

When is “she usually does X” a real signal?

You’ve watched a user comparison-shop three times in a row. Is that a habit?

The intuitive answer is yes. Three times counts.

But three times of what? Three sessions in a week is different from three sessions across two years. Three completed purchases is different from three abandoned carts. And three data points, taken as evidence of a stable behavioral tendency, is a very small number to make a claim about a person from.

The honest answer: it depends on the strength of the signal and the cost of being wrong. If the pattern is “compares before buying” and the action taken on the belief is “prefetch product images,” being wrong is cheap — some wasted compute, invisible to the user. If the pattern is “defers to spouse on high-ticket items” and the action is “route the conversation to a different flow,” being wrong is expensive, and the failure mode is uncomfortable.

Every procedural memory system needs a threshold — a minimum sample size before a pattern is trusted enough to act on. That threshold isn’t a fact about statistics. It’s a fact about your product’s tolerance for being wrong in the specific way procedural memory can be wrong.

What shape does a habit even have?

If you had to write down “this user tends to compare products before deciding” in a way another program could reason with, what data structure would you reach for?

The natural shape is a transition graph. Classify the user’s current turn into one of a small number of statesbrowsing, comparing, deciding, checking_out, abandoned — and track which state each turn transitions to next. Over enough sessions, per user, you accumulate a probability distribution:

browsing → comparing:    P = 0.72   (n = 47)
browsing → deciding:     P = 0.18   (n = 12)
browsing → abandoned:    P = 0.10   (n = 7)

Read the top row: for this user, when the current state is browsing, the next state is comparing 72% of the time, based on 47 observations. That’s a habit encoded as data — and, crucially, encoded in a way that’s comparable across time. If next week’s distribution looks different, you can measure how different.

The technical term for this is a Markov chain. The important word isn’t Markov — it’s chain. The claim being stored is not “the user does X.” It’s “given the user just did A, the user is likely to do B next.”

But what counts as a “state,” and who decides?

The whole approach depends on turning a messy natural-language turn into one of a small, fixed set of discrete states. Who defines those states? And how does classification actually work in practice?

The state space is a design decision that belongs to the product, not the model. A retail agent might have browsing / comparing / deciding / purchasing / support. A code assistant might have exploring / debugging / refactoring / testing / stuck. A therapy agent might have — well, a therapy agent probably shouldn’t be flattening a user’s emotional state into five buckets at all, which is itself a design lesson about when procedural memory is and isn’t the right tool.

Given a state space, the classifier itself is straightforward. Send the turn plus a short context window to a small classifier (a fine-tuned encoder is cheapest; a zero-shot LLM call works too) and record the label alongside the turn:

STATE_SPACE = [
    "browsing", "comparing", "deciding",
    "checking_out", "abandoned", "support"
]

def classify_state(user_message, session):
    """Assign the current turn to one of the predefined states."""
    return state_classifier.predict(
        text=user_message,
        recent_states=session.last_n_states(3),
        labels=STATE_SPACE
    )

# After each turn
prev_state = session.last_state
curr_state = classify_state(user_message, session)

procedural_store.record_transition(
    user_id=user.id,
    from_state=prev_state,
    to_state=curr_state,
    session_id=session.id,
    timestamp=now()
)
session.last_state = curr_state

Two things worth flagging. First, transitions are recorded here but not used — the pattern only becomes actionable once enough transitions have accumulated to cross the confidence threshold from the last section. Recording is cheap; acting on a bad pattern is not. Separating the two is the whole point. Second, the classifier gets recent_states as context. Whether the last three turns were browsing or comparing changes what the current message most likely is. Classifying a turn in isolation is noisier than it needs to be, and the fix — passing recent context — costs nothing.

How does the agent know when the pattern has broken?

The user’s transition graph has said browsing → comparing 72% of the time for six months. Over the last three sessions, it’s been browsing → checking_out. Is that a new habit, or noise?

This is the procedural cousin of the contradiction problem the first post spent most of its length on. But it plays out differently. There’s no dateable moment where the user said the pattern changed. The evidence trickles in, one transition at a time. Nothing about it looks like a contradiction until you’ve seen enough of it.

The mechanical answer is drift detection: compare the recent distribution of transitions to the historical one, and flag when the divergence between them exceeds a threshold.

def detect_drift(user_id, from_state,
                 window_recent=10, window_hist=100):
    historical = get_transition_distribution(
        user_id, from_state, last_n=window_hist
    )
    recent = get_transition_distribution(
        user_id, from_state, last_n=window_recent
    )

    divergence = kl_divergence(recent, historical)

    if divergence > DRIFT_THRESHOLD:
        return {
            "drift_detected": True,
            "old_top": historical.mode(),
            "new_top": recent.mode(),
            "recent_n": window_recent,
        }
    return {"drift_detected": False}

When drift is detected, the interesting question is what to do about it. The naive move is to overwrite — the recent window becomes the new belief. This fails the same way “always believe the user” fails in semantic memory: three noisy sessions overwrite six months of stable pattern.

A more disciplined move is to hold both. Keep the historical distribution as stable. Promote the recent one to emerging. Flip the primary belief only after the emerging pattern has itself accumulated enough evidence to cross the confidence threshold. That’s the same shape as the semantic memory pending_confirmation state — resolved not by asking the user, but by the passage of time and the accumulation of more transitions.

Asking the user, in the procedural case, would be strange. “I’ve noticed you’re checking out faster than usual — is that intentional?” is a question the user can’t easily answer, because they weren’t tracking the pattern in the first place. Semantic memory can defer to the user because it stores claims the user made. Procedural memory can’t defer to the user in the same way, because it stores claims the user never made.

Which is where this gets uncomfortable.

What can the agent do with this — and what should it not?

You now have, per user, a validated pattern the user never explicitly told you about. What are you allowed to do with it?

The comfortable end of the spectrum is the invisible end. Prefetch — if the user’s next state is likely comparing, warm up the comparison components while she’s still typing. Latency win, invisible to the user, no ethical weight. Adapt tone — if the pattern says this user prefers concise responses when browsing and detailed ones when deciding, adjust output length. Small personalization win, still invisible.

The uncomfortable end is where the agent starts acting on the pattern in ways the user can see. Suggest proactively: “you usually compare three options — want me to line them up?” This is the first move where the agent names the pattern back to the user, and it’s the first move where the user might feel watched rather than helped. Route the conversation: send the user down a different flow based on predicted state. This is the move where the agent starts making decisions for the user rather than with them.

The line I’d draw, and this is opinion, is that procedural memory should be allowed to shape the agent’s responsiveness freely, but should be much more careful about shaping the conversation’s direction. Prefetching what the user is likely to ask is speed. Prefetching what the user is likely to decide is presumption.

The deeper issue — and this is the trust question the first post didn’t have to raise — is that the user never authorized the claim in the first place. A semantic fact like (user.mom, likes, gardening) came out of the user’s own words. A procedural claim like “this user takes 40% longer to decide when price crosses ₹2000” came out of the agent’s own bookkeeping. The user didn’t say it. The agent inferred it. Unless the user is told, they can’t correct it, opt out of it, or know it’s shaping how the agent responds to them.

That’s a design question the semantic memory pipeline doesn’t have to answer. It’s one that procedural memory, honestly implemented, always has to. The KL divergence and threshold code above will never surface the question by itself — which is exactly why it needs to be surfaced somewhere else, in the design decisions that decide what a pattern is allowed to do once it’s been detected.