A frontier model knows an astonishing amount about software in general and nothing about your software in particular. It doesn’t know that your checkout service depends on payments, that a specific error string traces to a known config drift, or who owns the pipeline that just started dropping data. An AI teammate earns its title by learning those things and getting sharper about your environment every time it’s used.
Why similarity search falls short
Embed every past conversation into a vector store and retrieve the top-k by cosine similarity. This is a fine start, and we do use it, but two problems surface fast. Vectors can’t answer structural questions (“if this node dies, who is affected?”), because similarity is not reachability. And an incident finding that was true during one outage is actively misleading if it’s replayed as current state during the next. Naïve RAG doesn’t just fail to help there; it confidently hurts.

Two tiers, on purpose
The vector tier (mem0 over Milvus, 1536-dim embeddings) holds two categories, each with its own extraction prompt. Semantic memory is durable preferences, ownership, and identity, gated by a “durability test”: only store what will still be true across future interactions, and only extract it from what the user said, never from what the assistant guessed. Episodic memory is incident findings, root causes, and resolutions, gated by three tests: the Tomorrow Test (still true tomorrow?), the Reuse Test (helps a different future incident?), and the Discovery Test (something learned, not merely observed). Each extraction is capped at five facts per conversation, and retrieval is partitioned deliberately: by user ID in a DM channel (private preferences), and by org ID everywhere else (shared knowledge).
The knowledge-graph tier, backed by Amazon Neptune, models the org’s topology as nodes (Service, Repo, Team, Person, PagerDutyService, AwsResource, Incident…) and edges (DEPENDS_ON, OWNED_BY, AFFECTS, CORRELATES_WITH), split into a topology namespace (discovered structure) and a learned namespace (what agents observed). This is what answers the questions embeddings structurally can’t: blast-radius (forward reachability: if this fails, what is downstream?) and criticality (reverse-reachability BFS scoring sum(1/distance) over every transitive dependent), with a graceful degree-based fallback for orgs whose graph is still sparse, and a basis field so the caller knows which it got.
Designing against staleness
Nothing is hard-deleted. Every node and edge is bi-temporal (validAt, invalidAt, firstSeen, lastSeen, observedCount), and re-discovery is reinforcement, not duplication:
// openCypher on Neptune, the synthetic pk *is* the uniqueness constraint
MERGE (n {pk: $orgId + '::' + $type + '::' + $externalId})
ON CREATE SET n.firstSeen = $now, n.observedCount = 1
ON MATCH SET n.lastSeen = $now, n.observedCount = n.observedCount + 1
// reads filter `invalidAt IS NULL` and order by observedCount DESC
Episodic memory also has a 30-day TTL while semantic facts persist; IDs are time-sortable (ULID) so pagination is cheap; near-duplicates are suppressed at write time. And retrieved memory is always framed to the model as a hint to confirm with live tools, never as ground truth. The prompt header reads “Related Past Analysis, verify with current tool calls.” Growth is measured, too: we emit a memory-created metric tagged by agent and category, so “the teammate is learning your org” is a number on a dashboard, not a claim.
Building on Neptune’s constraints
Neptune is not Neo4j, and the code carries a catalogue of hard-won differences. No uniqueness constraints (idempotency is enforced by the synthetic MERGE key above). No APOC, so type labels can’t be parameterised and are whitelisted against an enum before interpolation, which is also the injection boundary. No list predicates, so paths that cross an invalidated edge are filtered in Python, not Cypher, under hard caps (5,000 paths for blast-radius, 500 for subgraphs). Variable-length path bounds can’t be parameterised either, so hop counts are integer-validated to 1–3 before interpolation. And at least one genuine footgun we hit and documented: filter an absent property the wrong way and Neptune silently drops every row once that property key exists anywhere in the graph. These details don’t exist until you run a per-org graph in production.
Build-it-yourself reality check
A RAG-over-chat-history prototype is a weekend. A memory system that won’t poison an investigation with last month’s transient state is a standing team: the right split between graph and vectors, quality-gated extraction that resists storing noise, bi-temporal invalidation, per-org isolation, and a managed graph database whose query dialect actively fights you.
The failure mode is what makes it insidious, it’s silent. A broken memory system doesn’t crash. It confidently tells the agent something that used to be true, the agent acts on it, and you don’t find out until the postmortem. Correctness here isn’t “does it return results,” it’s “are the results still true”, a much harder property to test for.
Where this stands, topology, criticality/blast-radius, and vector memory are live. Learned-episode ingestion into the graph is still landing; this post describes the parts that ship.
Generic intelligence is table stakes. Knowing your system, and knowing when what it knows has gone stale, is the product.





