Facts that decay
Store observations, not booleans. Belief is computed on read from a time-decayed Beta. Old evidence fades on its own, so nothing has to expire it.
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.