An AI agent is only as good as what it knows about the user’s world. The usual way to store that is a boolean with a TTL: customersHaveOrders: true, expires: 7d. That breaks in two ways. A fact that was confirmed ten times reads the same as one seen once, and when the TTL hits, you go from sure to knowing nothing in one step.

Instead, every fact keeps a short list of observations, and how much you trust it is worked out when you read it. Each observation counts for less as it gets older. Nothing gets written to mark a fact stale. It just fades.

The stamp

Every fact carries the same stamp, whatever it’s about:

type Observation = {
  t: number;                                       // when
  outcome: 'confirmed' | 'refuted' | 'changed' | 'used';
};

type Stamp = {
  first: number;
  last: number;
  n: number;            // how many times it was observed
  obs: Observation[];   // newest last, capped at 20
};

const observe = (prior: Stamp | undefined, o: Observation): Stamp => ({
  first: prior?.first ?? o.t,
  last: o.t,
  n: (prior?.n ?? 0) + 1,
  obs: [...(prior?.obs ?? []), o].slice(-20),
});

Writers only ever append. They never write a score.

Belief on read

Each observation’s weight halves every half-life. Confirmations add to a, refutations add to b, and belief is the Beta mean a / (a + b):

const weight = (t: number, now: number, halfLife: number) =>
  2 ** (-(now - t) / halfLife);

function belief(obs: Observation[], now: number, kind: Kind) {
  const prior = { a: 1, b: 1 };          // "no idea": p = 0.5
  let a = prior.a, b = prior.b, live = 0;
  for (const o of obs) {
    if (o.outcome === 'changed') {       // the world moved: old evidence is about a past reading
      a = prior.a; b = prior.b; live = 0;
      continue;
    }
    if (o.outcome === 'confirmed') { const w = weight(o.t, now, kind.confirmHalfLife); a += w; live += w; }
    if (o.outcome === 'refuted')   { const w = weight(o.t, now, kind.refuteHalfLife);  b += w; live += w; }
    // 'used' says someone wanted it, not that it's true: no effect on belief
  }
  return { p: a / (a + b), live };
}
weight of one confirmation (90-day half-life)

age      0d     45d    90d    180d   270d
weight   1.00   0.71   0.50   0.25   0.13
         ██████ ████▎  ███    █▌     ▊

Read the result as one of four states, and only call something confirmed or refuted when there’s enough live evidence behind it:

function status({ p, live }) {
  if (live === 0) return 'unprobed';
  if (live >= 0.5 && p >= 0.9) return 'confirmed';
  if (live >= 0.5 && p <= 0.1) return 'refuted';
  return 'unknown';                     // mixed, or too faded to lean on
}

Half-lives are per kind

How fast a fact should fade depends on what it is. All the tuning lives in one table:

Kind Half-life Why
data shape (“customers place orders”) 90 days confirmed, 7 days refuted shape changes slowly. An absence can flip with one data load, so refutations fade much faster
proven answer (a query that worked) 14 days strong when fresh, re-proven often
product usage 10 days what someone does lately
plan feature, integration health 1 hour cheap to check, costly to get wrong
goals, pins, instructions never the user said it, so it holds until they change it

Uneven half-lives are the important part. “This exists” and “this doesn’t exist” don’t age at the same rate.

Things that rest on facts

A derived thing, like a saved answer, keeps its own stamp, lists the facts it rests on, and is never trusted more than they are. If one of those facts breaks, the answer is out on the next read and comes back when the fact does. Nothing gets invalidated by hand.

answer "open orders from EU customers"   p = 0.83
  rests on ─┬─ path   Customer → Order               p = 0.97 ✓
            ├─ value  Customer.region ∋ "EU"          p = 0.94 ✓
            └─ value  Order.status ∋ "open"           p = 0.41 ✗  ──▶  answer is out

Why it works

  • Confidence matches the evidence. Ten confirmations outweigh one, and an old one counts for less than a new one.
  • Nothing to clean up. There are no cron jobs flipping flags. Staleness is just arithmetic at read time.
  • One meaning everywhere. Graph facts, account facts, and user facts share the stamp and the math, so “fresh” and “believed” mean the same thing wherever you read them.
  • Usage stays separate from truth. Demand and use are recorded on the same stamp, but they decide what to look at next (value of information), not what’s true.