An AI product ends up knowing things at three levels:

  • Graph: what exists in the customer’s data. “Customers place orders,” “status has values open, shipped and refunded.”
  • Account: what’s true about the customer. Plan features, integration health, which areas they use.
  • User: what’s true about one person. Their goals, pins, instructions, what they look at lately.

These usually grow up as separate systems, each with its own storage, refresh job, and idea of “stale.” The fix is to notice they’re all the same thing: observations about a subject, aged by kind. So they can share one machine.

The machine

  events ──┐                                                      ┌──▶ agent context
  polls  ──┼──▶ Observer ──▶ FactStore ──▶ Deriver ──▶ Reader ────┼──▶ UI ("what we know")
  probes ──┘       ▲         observe()      meaning,    get()     └──▶ answers, suggestions
                   │         request()      rollups     list()
                   │         get / list                 belief()
                   │                                    isFresh()
                   └──────── Scheduler ◀── value ÷ cost ──┘
                           what to look at next
Part Job
Observer one per kind of fact. It’s an event (the product pushes it: a view, a dismiss), a poll (fetch it: plan features), or a probe (go find out: does this path exist?)
FactStore observe appends to a fact’s stamp. request records demand. Writes are conditional on the version read, so two writers never lose each other’s evidence
Scheduler ranks every observer’s targets by value of information and spends the budget
Deriver builds higher-level meaning from facts: rollups, profiles, focus areas
Reader one interface for every scope. Belief and freshness come with every fact

One reader for every scope

type Knowledge<K> = {
  asOf: string | undefined;
  get(kind: K, key: string): Fact<K> | undefined;
  list(kind: K, filter?: (f: Fact<K>) => boolean): Fact<K>[];
  belief(fact: Fact<K>, now?: number): { p: number; freshness: number };
  isFresh(fact: Fact<K> | undefined, now?: number): boolean;
};

const graph = await dataKnowledge(accountId);
const user  = await userKnowledge(accountId, userId);

user.list('area.use').filter((f) => user.isFresh(f));    // what they've been looking at
graph.get('path', 'Customer|PLACED|Order');              // does the path exist, and how sure are we

The caller doesn’t care where a fact came from or how it’s stored. “Fresh” means the same thing for a graph path and for a user’s last visit, because both use the same stamp and the same per-kind half-life table.

What changes

Before After
a job that collects everything for every account every hour each kind refreshes when its own freshness runs out
a TTL and a boolean per signal an observation list, with belief computed on read
one store per team, each with its own decay code one FactStore, split by scope only where privacy needs it (user facts delete by one key)
personalization owns collection and presentation personalization is just the composer: it reads facts and decides what to show

Why it works

  • Build it once. A new signal is one observer and one row in the half-life table. Storage, decay, scheduling, and reading are already there.
  • Pull turns into push. Because the system keeps its own knowledge fresh, it can tell people what changed instead of waiting for them to go looking.
  • Agents and people read the same truth. The agent’s context and the “what we know about you” page come from the same facts, so they can’t disagree.