{"id":6679,"date":"2026-07-27T14:24:19","date_gmt":"2026-07-27T14:24:19","guid":{"rendered":"https:\/\/dialnexa.com\/blogs\/?p=6679"},"modified":"2026-07-27T14:48:37","modified_gmt":"2026-07-27T14:48:37","slug":"company-memory-governance-postgres","status":"publish","type":"post","link":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/","title":{"rendered":"Company Memory Is a Governance Problem, Not a Vector Database"},"content":{"rendered":"<nav class=\"alfred-series-nav\" style=\"margin:0 0 2.25rem;padding:1.25rem 1.5rem;border:1px solid #e0e7ff;border-left:4px solid #6366f1;border-radius:10px;background:#f8f7ff;\">\n<p style=\"margin:0 0 .6rem;font-size:.78rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#4f46e5;\">Building Alfred \u00b7 a six part series<\/p>\n<ol style=\"margin:0;padding-left:1.25rem;font-size:.95rem;line-height:1.75;\">\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/company-second-brain-architecture\/\">Why a second brain is a system, not a chat box<\/a><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/grounding-ai-agent-company-data\/\">Grounding: accessing company reality safely<\/a><\/li>\n<li><strong>Memory as a governance problem<\/strong> <span style=\"color:#6b7280;font-size:.85rem;\">\u00b7 you are here<\/span><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-graduated-agency\/\">Graduated agency<\/a><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/production-ai-agent-reliability\/\">Production reliability<\/a><\/li>\n<li><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-learning-loop\/\">The learning loop<\/a><\/li>\n<\/ol>\n<\/nav>\n<p><em>Building Alfred, Part 3 of 6. <a href=\"https:\/\/dialnexa.com\/blogs\/company-second-brain-architecture\/\">Part 1 was the architecture<\/a>, <a href=\"https:\/\/dialnexa.com\/blogs\/grounding-ai-agent-company-data\/\">Part 2 was grounding<\/a>. This one is about memory, and it contains the single most contrarian decision in the system.<\/em><\/p>\n<p>A new engineer follows a procedure that worked perfectly six months ago.<\/p>\n<p>The document is still searchable. The instructions are clear. The answer looks authoritative. It is also wrong. The provider changed, the approval process moved, and the person who understood the exception has left.<\/p>\n<p>The knowledge was stored. It was never governed.<\/p>\n<p>This is where &#8220;company second brain&#8221; becomes actively dangerous. A brain that remembers everything without understanding ownership, time or confidence does not create intelligence. It creates very fast institutional confusion.<\/p>\n<h2>We shipped company memory with zero embeddings<\/h2>\n<p>Let me put the contrarian part first, because it is the thing most people will disagree with.<\/p>\n<p>Alfred&#8217;s governed memory has no vector database. No embeddings. No pgvector. No reranker. No BM25 library. A repo-wide grep for <code>embedding|pgvector|voyage|cohere|rerank|bm25|faiss|qdrant|pinecone|weaviate|chroma|text-embedding<\/code> returns two hits, both in an audio noise analysis function where <code>vector<\/code> happens to be a local NumPy variable.<\/p>\n<p>Retrieval is Postgres full-text search. That is it.<\/p>\n<p>This was not a shortcut we are embarrassed about. It follows from a claim I believe: <strong>similarity is not truth, and the hard part of company memory is not finding the passage.<\/strong><\/p>\n<p>A vector database does not know that a decision was reversed. It does not know that a customer-specific rule should be hidden from another team. It does not know that an incident note was a hypothesis, that a procedure needs review next quarter, or that a personal preference should never become company policy.<\/p>\n<p>Embed every document and conversation into one pool and you build a system that is excellent at retrieving your organisation&#8217;s contradictions.<\/p>\n<p>The right question is not &#8220;can the AI find this again.&#8221; It is whether the information has earned the right to influence a future answer. That question is answered by review, provenance, visibility and time, none of which are vector operations.<\/p>\n<p>So we spent our complexity budget there instead. Nine tables, one migration, and a retrieval query with three weighted signals.<\/p>\n<h2>The schema<\/h2>\n<p>Everything is raw psycopg3 SQL with <code>dict_row<\/code>. There is no ORM model. The only Python type is a frozen dataclass:<\/p>\n<pre class=\"wp-block-code\"><code>@dataclass(frozen=True)\nclass RetrievedMemory:\n    page_id: str\n    title: str\n    summary: str\n    body: str\n    page_type: str\n    namespace: str\n    confidence: float\n    updated_at: str\n    sources: list\n    score: float<\/code><\/pre>\n<p>The tables:<\/p>\n<table>\n<thead>\n<tr>\n<th>Table<\/th>\n<th>Purpose<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>alfred_wiki_pages<\/code><\/td>\n<td>Shared company knowledge, the governed core<\/td>\n<\/tr>\n<tr>\n<td><code>alfred_wiki_sources<\/code><\/td>\n<td>Provenance rows, unique per <code>(page_id, source_uri)<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>alfred_wiki_revisions<\/code><\/td>\n<td>Full prior-state snapshots, unique per <code>(page_id, revision_no)<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>alfred_wiki_links<\/code><\/td>\n<td>Typed edges between pages<\/td>\n<\/tr>\n<tr>\n<td><code>alfred_memory_candidates<\/code><\/td>\n<td>Proposals awaiting human review<\/td>\n<\/tr>\n<tr>\n<td><code>alfred_personal_memories<\/code><\/td>\n<td>Per-user, unique per <code>(user_email, memory_key)<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>alfred_episodes<\/code><\/td>\n<td>Opt-in task summaries, unique per <code>session_id<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>alfred_memory_feedback<\/code><\/td>\n<td>Thumbs up\/down tied to a page and session<\/td>\n<\/tr>\n<tr>\n<td><code>alfred_memory_retrievals<\/code><\/td>\n<td>Retrieval telemetry, no raw queries<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>The interesting column on <code>alfred_wiki_pages<\/code> is the generated search vector, with field weights:<\/p>\n<pre class=\"wp-block-code\"><code>search_document TSVECTOR GENERATED ALWAYS AS (\n    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||\n    setweight(to_tsvector('english', coalesce(summary, '')), 'B') ||\n    setweight(to_tsvector('english', coalesce(body_markdown, '')), 'C')\n) STORED<\/code><\/pre>\n<p>A GIN index on that column, plus a btree on <code>(status, visibility, namespace, updated_at DESC)<\/code>, is the entire retrieval infrastructure.<\/p>\n<p>Constraints do real work here. <code>page_type<\/code> is a seven-value CHECK. <code>visibility<\/code> is CHECK-constrained to <code>('team','operations','tech','admin')<\/code>. <code>status<\/code> to <code>('draft','published','disputed','archived')<\/code>. <code>confidence<\/code> is <code>NUMERIC(4,3)<\/code> with a 0 to 1 CHECK. Putting these in the database rather than in Python means a future code path cannot invent a fifth visibility tier by accident.<\/p>\n<figure class=\"alfred-diagram\" style=\"margin:2.5rem 0;\">\n<svg viewBox=\"0 0 760 300\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:100%;height:auto;display:block;font-family:Inter,system-ui,sans-serif;\" role=\"img\" aria-label=\"Diagram of the memory lifecycle from agent proposal to pending candidate to human review to published page, with sources and revisions attached\">\n<rect x=\"1\" y=\"1\" width=\"758\" height=\"298\" rx=\"12\" fill=\"#f8f7ff\" stroke=\"#e0e7ff\"\/>\n<text x=\"380\" y=\"42\" text-anchor=\"middle\" fill=\"#4f46e5\" font-size=\"13\" font-weight=\"700\">Postgres full-text search. No embeddings.<\/text>\n<text x=\"380\" y=\"62\" text-anchor=\"middle\" fill=\"#6b7280\" font-size=\"11.5\">Role visibility filtered in SQL before anything reaches the prompt<\/text>\n<rect x=\"26\" y=\"100\" width=\"146\" height=\"60\" rx=\"9\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"99\" y=\"125\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"13\" font-weight=\"600\">propose_memory<\/text>\n<text x=\"99\" y=\"143\" text-anchor=\"middle\" fill=\"#6b7280\" font-size=\"11\">agent tool<\/text>\n<rect x=\"212\" y=\"100\" width=\"146\" height=\"60\" rx=\"9\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"285\" y=\"125\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"13\" font-weight=\"600\">candidate<\/text>\n<text x=\"285\" y=\"143\" text-anchor=\"middle\" fill=\"#6b7280\" font-size=\"11\">status pending<\/text>\n<rect x=\"398\" y=\"100\" width=\"146\" height=\"60\" rx=\"9\" fill=\"#ffffff\" stroke=\"#6366f1\" stroke-width=\"2\"\/>\n<text x=\"471\" y=\"125\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"13\" font-weight=\"600\">human review<\/text>\n<text x=\"471\" y=\"143\" text-anchor=\"middle\" fill=\"#4f46e5\" font-size=\"11\">approve or reject<\/text>\n<rect x=\"584\" y=\"100\" width=\"150\" height=\"60\" rx=\"9\" fill=\"#4f46e5\"\/>\n<text x=\"659\" y=\"125\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"13\" font-weight=\"700\">published page<\/text>\n<text x=\"659\" y=\"143\" text-anchor=\"middle\" fill=\"#c7d2fe\" font-size=\"11\">retrievable<\/text>\n<g stroke=\"#a5b4fc\" stroke-width=\"1.5\" fill=\"none\">\n<path d=\"M172 130 H206\"\/><path d=\"M198 124 L206 130 L198 136\"\/>\n<path d=\"M358 130 H392\"\/><path d=\"M384 124 L392 130 L384 136\"\/>\n<path d=\"M544 130 H578\"\/><path d=\"M570 124 L578 130 L570 136\"\/>\n<\/g>\n<rect x=\"398\" y=\"204\" width=\"146\" height=\"46\" rx=\"8\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"471\" y=\"223\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"12\" font-weight=\"600\">sources<\/text>\n<text x=\"471\" y=\"240\" text-anchor=\"middle\" fill=\"#6b7280\" font-size=\"10.5\">never empty<\/text>\n<rect x=\"584\" y=\"204\" width=\"150\" height=\"46\" rx=\"8\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"659\" y=\"223\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"12\" font-weight=\"600\">revisions<\/text>\n<text x=\"659\" y=\"240\" text-anchor=\"middle\" fill=\"#6b7280\" font-size=\"10.5\">to_jsonb snapshot<\/text>\n<g stroke=\"#c7d2fe\" stroke-width=\"1.5\" fill=\"none\" stroke-dasharray=\"4 3\">\n<path d=\"M471 160 V204\"\/><path d=\"M659 160 V204\"\/>\n<\/g>\n<rect x=\"26\" y=\"204\" width=\"332\" height=\"46\" rx=\"8\" fill=\"#ffffff\" stroke=\"#c7d2fe\"\/>\n<text x=\"192\" y=\"223\" text-anchor=\"middle\" fill=\"#1f2937\" font-size=\"12\" font-weight=\"600\">retrieval score, no threshold<\/text>\n<text x=\"192\" y=\"240\" text-anchor=\"middle\" fill=\"#6b7280\" font-size=\"11\">lex 0.75 + title 0.20 + confidence 0.05<\/text>\n<text x=\"26\" y=\"284\" fill=\"#6b7280\" font-size=\"11\">A floor, not a cutoff: with no lexical match the agent still gets the core pages.<\/text>\n<\/svg><figcaption style=\"margin-top:.75rem;font-size:.9rem;color:#6b7280;text-align:center;\">Nothing writes a page directly. Proposal, review and publication are separate states, and every page keeps its sources and revisions.<\/figcaption><\/figure>\n<h2>Memory begins as a candidate, not a fact<\/h2>\n<p>The most important thing the system does is refuse to write directly.<\/p>\n<p>An agent that notices something useful cannot create a page. It can only propose:<\/p>\n<pre class=\"wp-block-code\"><code>memory_type \u2208 {wiki, personal, procedure}\ntitle      \u2264 200 chars\ncontent    \u2264 12,000 chars\nconfidence 0..1\nsources    \u2264 20 items, each requiring a uri<\/code><\/pre>\n<p>That lands a <code>pending<\/code> row in <code>alfred_memory_candidates<\/code>. A human with the <code>manage_alfred_memory<\/code> capability approves or rejects it.<\/p>\n<p>Provenance is never empty, which took a small amount of deliberate work. If a proposal arrives with no sources, one is synthesised from the conversation it came from:<\/p>\n<pre class=\"wp-block-code\"><code>{\"type\": \"chat\",\n \"uri\": f\"alfred:\/\/sessions\/{session_id}\",\n \"label\": \"Originating Alfred chat\"}<\/code><\/pre>\n<p>And if a human publishes a page directly without citing anything, that is recorded too:<\/p>\n<pre class=\"wp-block-code\"><code>{\"type\": \"human\", \"uri\": f\"human:\/\/{actor}\", ...}<\/code><\/pre>\n<p>&#8220;A person asserted this&#8221; is a legitimate source. &#8220;Unknown origin&#8221; is not, and a knowledge base full of unattributed statements is exactly the failure mode we were trying to avoid.<\/p>\n<h2>Revisions, and a trick worth stealing<\/h2>\n<p>Publishing a page takes <code>SELECT ... FOR UPDATE<\/code>, computes the next revision number, writes the prior state into <code>alfred_wiki_revisions<\/code>, then updates. Standard, except for how the snapshot is captured:<\/p>\n<pre class=\"wp-block-code\"><code>SELECT {PAGE_FIELDS},\n       to_jsonb(alfred_wiki_pages) - 'search_document' AS revision_snapshot\nFROM alfred_wiki_pages WHERE slug = %s FOR UPDATE<\/code><\/pre>\n<p><code>to_jsonb(row)<\/code> serialises the whole row. The <code>- 'search_document'<\/code> subtracts the generated tsvector, which is both enormous and derivable. There is no column list to keep in sync, so adding a column next quarter automatically appears in future snapshots, and the JSONB cast handles type conversion.<\/p>\n<p>There is a test that exists purely to pin this behaviour, with a fake cursor whose comment reads: &#8220;This is what PostgreSQL to_jsonb returns: JSON-safe strings\/numbers, even though the selected row also contains native DB types.&#8221;<\/p>\n<p>One honest wart: <code>restore_revision<\/code> does <strong>not<\/strong> write a revision of the state it overwrites. It is the only unrevisioned mutation in the service, and it copies the snapshot&#8217;s <code>status<\/code> back verbatim, which means restoring an archive-time snapshot silently un-archives the page. That is a bug, not a design.<\/p>\n<h2>Visibility is enforced in SQL<\/h2>\n<p>Role to visibility mapping is a single dict:<\/p>\n<pre class=\"wp-block-code\"><code>WIKI_VISIBILITY_ROLES = {\n    \"team\":       frozenset({\"admin\", \"operations\", \"tech\"}),\n    \"operations\": frozenset({\"admin\", \"operations\"}),\n    \"tech\":       frozenset({\"admin\", \"tech\"}),\n    \"admin\":      frozenset({\"admin\"}),\n}<\/code><\/pre>\n<p><code>allowed_visibilities(roles)<\/code> inverts that into a tuple passed straight into the query as <code>visibility = ANY(%s)<\/code>. Per-user scoping for personal memories and episodes is likewise in the statement itself, never in a Python filter: every mutation carries <code>AND user_email = %s<\/code>.<\/p>\n<p>This is more reliable than asking a model to remember which paragraph it should not repeat to a particular role. The rows never reach the prompt.<\/p>\n<p>A small thing that saves real support time:<\/p>\n<pre class=\"wp-block-code\"><code>def _require_uuid(value: str | None, field: str) -&gt; str | None:\n    \"\"\"Reject non-UUID identifiers before they reach a UUID column (else 500).\"\"\"<\/code><\/pre>\n<p>422 instead of a psycopg cast error in a stack trace.<\/p>\n<h2>Retrieval is one query and three signals<\/h2>\n<p>Here is the core of it:<\/p>\n<pre class=\"wp-block-code\"><code>WITH ranked AS (\n    SELECT p.*,\n           COALESCE(ts_rank_cd(p.search_document,\n                    to_tsquery('english', NULLIF(%s, ''))), 0) AS lexical,\n           CASE WHEN p.title &lt;&gt; '' AND strpos(lower(%s), lower(p.title)) &gt; 0\n                THEN 1.0 ELSE 0.0 END AS exact_title\n    FROM alfred_wiki_pages p\n    WHERE p.status = 'published' AND p.visibility = ANY(%s)\n      AND (p.expires_at IS NULL OR p.expires_at &gt; NOW())\n      AND (p.search_document @@ to_tsquery('english', NULLIF(%s, ''))\n           OR (p.title &lt;&gt; '' AND strpos(lower(%s), lower(p.title)) &gt; 0))\n)<\/code><\/pre>\n<p>and the score:<\/p>\n<pre class=\"wp-block-code\"><code>(lexical * 0.75 + exact_title * 0.20 + confidence::float * 0.05) AS score<\/code><\/pre>\n<p>Top-k is 8 in production, clamped to a maximum of 20. Episodes are 3, clamped to 5.<\/p>\n<h3>The tsquery decision<\/h3>\n<p>The obvious way to turn a user question into a tsquery is <code>websearch_to_tsquery<\/code>. We do not, and the docstring explains why:<\/p>\n<pre class=\"wp-block-code\"><code>_TOKEN_RE = re.compile(r\"[a-z0-9]+\")\n\ndef or_tsquery(query: str, max_terms: int = 12) -&gt; str:\n    seen: list[str] = []\n    for token in _TOKEN_RE.findall(query.lower()):\n        if len(token) &lt; 2 or token in seen:\n            continue\n        seen.append(token)\n        if len(seen) &gt;= max_terms:\n            break\n    return \" | \".join(seen)<\/code><\/pre>\n<p><code>websearch_to_tsquery<\/code> ANDs terms, &#8220;which collapses recall to near zero on real questions.&#8221; A user asking &#8220;why does the Airtel trunk drop on transfers after we changed the dialer config&#8221; produces a conjunction no page will ever satisfy. So lexemes are OR&#8217;d, capped at twelve, de-duplicated, order preserved.<\/p>\n<p>Reducing tokens to alphanumerics is also the injection guard. There is no way to smuggle tsquery operators through it. Empty output is handled on the SQL side with <code>NULLIF(%s, '')<\/code>, so a query made entirely of stopwords degrades to &#8220;matches nothing&#8221; instead of raising.<\/p>\n<h3>What the numbers actually do<\/h3>\n<p>Three things about that scoring formula that only become obvious in production.<\/p>\n<p><code>ts_rank_cd<\/code> is unnormalised and typically returns small values, roughly 0.01 to 0.3. So the <code>0.20<\/code> <code>exact_title<\/code> term dominates ranking whenever it fires. In practice this is a two-tier ranker: pages whose title appears verbatim in the message, then everything else.<\/p>\n<p><code>exact_title<\/code> is <code>strpos(lower(user_message), lower(page_title)) &gt; 0<\/code>. Note the direction. The page title must appear inside the user&#8217;s message, not the reverse. It appears in the <code>WHERE<\/code> clause as an <code>OR<\/code> branch, which makes it a recall widener as well as a booster. A page titled &#8220;Calls&#8221; matches nearly every message ever sent. A page with a long descriptive title effectively never fires this signal. Title length is now a ranking parameter, which nobody intends but everybody who does lexical retrieval eventually discovers.<\/p>\n<p>There is no score threshold. None. A single overlapping lexeme qualifies a page for consideration. This was intentional, and it pairs with the next design choice.<\/p>\n<h3>A floor, not a threshold<\/h3>\n<p>Most retrieval systems add a minimum relevance cutoff. We did the opposite:<\/p>\n<pre class=\"wp-block-code\"><code>if not memories:\n    # Retrieval floor: never hand the agent an empty knowledge base.\n    memories = await self.core_pages(user_roles)<\/code><\/pre>\n<p><code>core_pages<\/code> returns the three highest-confidence pages of type <code>data-contract<\/code>, <code>system<\/code> or <code>glossary<\/code>, ordered by confidence then recency, with <code>score<\/code> forced to <code>0.0<\/code> so the model can see they were not matched.<\/p>\n<p>The reasoning: an agent with zero context confabulates from training data. An agent holding your data contracts and glossary at least fails inside your vocabulary. A floor is cheap insurance; a threshold optimises a metric nobody experiences.<\/p>\n<h3>Memory rides in the uncached tier, for billing reasons<\/h3>\n<p>This is my favourite decision in the codebase because it is a retrieval choice made for cost, not relevance:<\/p>\n<pre class=\"wp-block-code\"><code># Governed memory retrieved for THIS query rides here, not in the\n# cached tier: it is relevance-ranked per turn, so caching it would\n# force a cache-write on every single turn and never hit.<\/code><\/pre>\n<p>Prompt caching charges 1.25x to write and 0.1x to read. Content that changes every turn is the worst possible cache candidate: you pay the premium every time and never collect the discount. So the stable system prompt gets a cache breakpoint and per-query memory deliberately does not.<\/p>\n<p>There is a test asserting the retrieved page is absent from every block carrying <code>cache_control<\/code> and present in <code>blocks[-1]<\/code>. Somebody will eventually try to &#8220;optimise&#8221; this by caching it. The test is there to stop them.<\/p>\n<h2>Telemetry without surveillance<\/h2>\n<p>Every retrieval logs a row. What it does not log is the question:<\/p>\n<pre class=\"wp-block-code\"><code>query_hash = hashlib.sha256(query.encode(\"utf-8\")).hexdigest()\nawait self._fetchone(\n    \"\"\"INSERT INTO alfred_memory_retrievals\n       (session_id, user_email, query_hash, page_ids, latency_ms)\n       VALUES (%s, %s, %s, %s, %s) RETURNING id\"\"\", ...)<\/code><\/pre>\n<p>There is a test that reads the migration file as a string and asserts it contains <code>query_hash TEXT NOT NULL<\/code> and does <strong>not<\/strong> contain <code>query TEXT NOT NULL<\/code>. A regression guard against a future engineer helpfully adding the plaintext column for debugging.<\/p>\n<p>I want to be precise about what this buys, because &#8220;one-way hash&#8221; is doing some work in that sentence. It is an unsalted, unkeyed SHA-256 of the raw user message, stored next to <code>user_email<\/code>. For short predictable questions (&#8220;what is our refund policy&#8221;) it is dictionary reversible in seconds. It genuinely prevents casual browsing of what colleagues asked. It is not a privacy guarantee against a determined party with database access. An HMAC with a server-held key would cost one line and fix that, and it should.<\/p>\n<p>A second cost worth naming: that insert is awaited in the hot read path, so every turn pays a write round trip to log a read.<\/p>\n<h2>Where the design outran the implementation<\/h2>\n<p>The published version of this article described capabilities that the schema supports and the code does not yet exercise. Since this series is meant to be readable by someone who later reads the code, here is the accounting.<\/p>\n<p><strong>TTL and expiry are declared but dead.<\/strong> <code>alfred_wiki_pages.expires_at<\/code>, <code>valid_from<\/code>, and <code>alfred_personal_memories.expires_at<\/code> are all filtered on read. Nothing anywhere writes them. There is no API field, no column in any INSERT or UPDATE. So &#8220;searches only unexpired pages&#8221; is vacuously true, and the <code>expiring_pages<\/code> metric in the evaluation summary is structurally always zero. There is no decay, no recency half-life, no garbage collection.<\/p>\n<p><strong>The <code>disputed<\/code> status is never written.<\/strong> Neither is <code>draft<\/code>; every write path hardcodes <code>status='published'<\/code>. Neither is <code>superseded<\/code> on candidates. There is no contradiction detection of any kind.<\/p>\n<p><strong>Conflict resolution is slug collision plus a prompt instruction.<\/strong> Approving a candidate calls <code>slugify(title)<\/code>, so a new candidate whose title slugifies to an existing slug overwrites that page, with a revision recorded. Two contradictory facts on the same topic resolve to whichever was approved last. What handles the rest is a sentence in the prompt: &#8220;prefer more recent, higher-confidence pages when they conflict,&#8221; backed by the confidence and updated-date annotations attached to every memory entry. Conflict resolution is delegated to the model at read time because the storage layer has no machinery for it. That is a defensible interim position, but it is not the same as governance.<\/p>\n<p>Also: <code>slugify<\/code> truncates to 100 characters, so two long distinct titles sharing a prefix collide into one page.<\/p>\n<p><strong>Redaction is not implemented.<\/strong> The rule that credentials, tokens, private message contents and customer-specific sensitive data must never become memory exists only as English in the system prompt and the tool description. A repo-wide grep for <code>redact|scrub|pii|anonymi<\/code> in the application returns one unrelated hit. There is no pattern matching, no entropy check, no denylist, no reviewer-side scanning.<\/p>\n<p><strong>There is no ingestion pipeline.<\/strong> No connector, crawler, browser snapshot, call transcript or document ever writes into these tables. No queue, no worker, no retry, no backfill. The complete list of writers is: the agent&#8217;s <code>propose_memory<\/code> tool, a deprecated <code>record_knowledge<\/code> tool, three HTTP endpoints, and the migration&#8217;s one-time copy from the legacy flat table. Memory grows because a human approved something.<\/p>\n<p><strong>Approval flattens the reviewer&#8217;s choices.<\/strong> <code>approve_candidate<\/code> hardcodes <code>namespace=\"company\"<\/code>, <code>visibility=\"team\"<\/code>, <code>summary=content[:500]<\/code>, and page type to <code>procedure<\/code> or <code>system<\/code>. A reviewer cannot set visibility or page type. The operations, tech and admin tiers and the seven-value page type enum are only reachable through a direct admin publish endpoint, which has no UI.<\/p>\n<p><strong>There is no UI for most of it.<\/strong> All fifteen memory endpoints exist. The frontend implements personal memories, personal candidates, shared candidates and a metrics line. No page browser, no revision viewer, no restore button, no archive button.<\/p>\n<p><strong><code>alfred_wiki_links<\/code> is unused.<\/strong> The knowledge graph edge table was created, complete with a self-reference CHECK constraint, and is referenced by zero lines of application code.<\/p>\n<p><strong>Episodes are retrieved but barely creatable.<\/strong> They are pulled into every prompt. There is no UI to create one, and <code>propose_memory<\/code> does not accept <code>episode<\/code> as a type. Creating one requires hand-crafting an HTTP call. Their search vector also lacks <code>setweight<\/code>, so every lexeme lands in weight class D and titles do not outrank bodies, unlike wiki pages. And <code>retrieve_episodes<\/code> still uses <code>websearch_to_tsquery<\/code>, the exact AND-semantics function whose recall collapse motivated <code>or_tsquery<\/code> for the wiki path. Episode recall suffers the documented bug that the wiki path was fixed to avoid.<\/p>\n<p><strong>Personal memories bypass relevance entirely.<\/strong> No query, no limit. Every active unexpired personal memory for a user is injected into every turn, in the uncached tier. That is an unbounded prompt growth path per user, and the first person to accumulate two hundred remembered preferences will find it.<\/p>\n<p>Writing that list was uncomfortable. It is also the most useful part of this article, because the gap between a schema that anticipates governance and a system that performs governance is where most &#8220;AI memory&#8221; projects actually live, including ours.<\/p>\n<h2>The recall bug, with its own postmortem in the source<\/h2>\n<p>One more thing, because it is a good illustration of how these systems fail quietly:<\/p>\n<pre class=\"wp-block-code\"><code># OR-match the lexemes (see or_tsquery) and, separately, boost a page whose\n# title appears verbatim inside the user's message. The prior code passed the\n# whole message through websearch_to_tsquery (AND) and tested title LIKE\n# %whole-message%, so both signals almost never fired.<\/code><\/pre>\n<p>Two signals, both broken, in a system that returned results and never errored. Retrieval quality bugs do not page you. They just make the assistant seem a bit dim, and everyone concludes the model is not good enough.<\/p>\n<h2>The organisational lesson<\/h2>\n<p>Building this forced us to make implicit company behaviour explicit. We had to define what counts as a procedure, who owns a data contract, how customer-specific knowledge is separated, when an incident lesson expires, and which teams can approve shared facts.<\/p>\n<p>That work made Alfred better. It also exposed where the organisation had been running on oral history. An AI cannot govern knowledge more clearly than the company governs it, and the schema is where that becomes undeniable, because you have to pick a default for <code>visibility<\/code> and someone has to own that decision.<\/p>\n<p><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-graduated-agency\/\">Next, in Part 4<\/a>: acting on knowledge, which changes the risk profile completely.<\/p>\n<p><em>What knowledge inside your company is searchable today but no longer trustworthy?<\/em><\/p>\n<hr style=\"margin:2.5rem 0 1.5rem;border:0;border-top:1px solid #e5e7eb;\" \/>\n<div class=\"alfred-series-pager\" style=\"display:flex;flex-wrap:wrap;gap:1rem;\">\n<div style=\"flex:1 1 240px;padding:1rem 1.25rem;border:1px solid #e0e7ff;border-radius:10px;background:#f8f7ff;\">\n<p style=\"margin:0 0 .3rem;font-size:.75rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#6b7280;\">Previous<\/p>\n<p><a href=\"https:\/\/dialnexa.com\/blogs\/grounding-ai-agent-company-data\/\" style=\"font-weight:600;\">Grounding: accessing company reality safely<\/a>\n<\/div>\n<div style=\"flex:1 1 240px;padding:1rem 1.25rem;border:1px solid #e0e7ff;border-radius:10px;background:#f8f7ff;\">\n<p style=\"margin:0 0 .3rem;font-size:.75rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#6b7280;\">Next in the series<\/p>\n<p><a href=\"https:\/\/dialnexa.com\/blogs\/ai-agent-graduated-agency\/\" style=\"font-weight:600;\">Graduated agency<\/a>\n<\/div>\n<\/div>\n<p style=\"margin-top:1.5rem;font-size:.95rem;color:#4b5563;\">Alfred runs on <a href=\"https:\/\/dialnexa.com\/\">DialNexa<\/a>, our voice AI platform for businesses. If you are building internal agents and want to compare notes, the comments are open.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.<\/p>\n","protected":false},"author":1,"featured_media":6707,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[717],"tags":[46,3],"class_list":["post-6679","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-business-automation","tag-voice-ai"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>AI Company Memory Without Embeddings: Postgres Full-Text Search and Governance<\/title>\n<meta name=\"description\" content=\"Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"AI Company Memory Without Embeddings: Postgres Full-Text Search and Governance\" \/>\n<meta property=\"og:description\" content=\"Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/\" \/>\n<meta property=\"og:site_name\" content=\"DialNexa\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-27T14:24:19+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-27T14:48:37+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-3-memory-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Aditya Kamat\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Aditya Kamat\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/\"},\"author\":{\"name\":\"Aditya Kamat\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#\\\/schema\\\/person\\\/1af38c86cbe30b471e5c350bfb15926c\"},\"headline\":\"Company Memory Is a Governance Problem, Not a Vector Database\",\"datePublished\":\"2026-07-27T14:24:19+00:00\",\"dateModified\":\"2026-07-27T14:48:37+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/\"},\"wordCount\":2489,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-3-memory-1.png\",\"keywords\":[\"business automation\",\"Voice AI\"],\"articleSection\":[\"Engineering\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/\",\"name\":\"AI Company Memory Without Embeddings: Postgres Full-Text Search and Governance\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-3-memory-1.png\",\"datePublished\":\"2026-07-27T14:24:19+00:00\",\"dateModified\":\"2026-07-27T14:48:37+00:00\",\"description\":\"Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#primaryimage\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-3-memory-1.png\",\"contentUrl\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/alfred-part-3-memory-1.png\",\"width\":1200,\"height\":630,\"caption\":\"Building Alfred part 3: company memory as a governance problem rather than a vector database\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/company-memory-governance-postgres\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Company Memory Is a Governance Problem, Not a Vector Database\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#website\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/\",\"name\":\"DialNexa Blog\",\"description\":\"Voice AI insights, customer communication playbooks, sales automation guides, and contact center operations advice from DialNexa.\",\"publisher\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\",\"name\":\"DialNexa\",\"url\":\"https:\\\/\\\/dialnexa.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/wp-content\\\/uploads\\\/2025\\\/10\\\/cropped-cropped-favicon-300x300-1.png\",\"caption\":\"DialNexa\"},\"image\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#\\\/schema\\\/person\\\/1af38c86cbe30b471e5c350bfb15926c\",\"name\":\"Aditya Kamat\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g\",\"caption\":\"Aditya Kamat\"},\"description\":\"Co-Founder of DialNexa. Expert in voice AI, conversational technology, and enterprise telephony. Building the future of AI-powered customer engagement.\",\"sameAs\":[\"https:\\\/\\\/dialnexa.com\"],\"jobTitle\":\"Co-Founder\",\"url\":\"https:\\\/\\\/dialnexa.com\",\"worksFor\":{\"@id\":\"https:\\\/\\\/dialnexa.com\\\/blogs\\\/#organization\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"AI Company Memory Without Embeddings: Postgres Full-Text Search and Governance","description":"Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/","og_locale":"en_US","og_type":"article","og_title":"AI Company Memory Without Embeddings: Postgres Full-Text Search and Governance","og_description":"Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.","og_url":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/","og_site_name":"DialNexa","article_published_time":"2026-07-27T14:24:19+00:00","article_modified_time":"2026-07-27T14:48:37+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-3-memory-1.png","type":"image\/png"}],"author":"Aditya Kamat","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Aditya Kamat","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#article","isPartOf":{"@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/"},"author":{"name":"Aditya Kamat","@id":"https:\/\/dialnexa.com\/blogs\/#\/schema\/person\/1af38c86cbe30b471e5c350bfb15926c"},"headline":"Company Memory Is a Governance Problem, Not a Vector Database","datePublished":"2026-07-27T14:24:19+00:00","dateModified":"2026-07-27T14:48:37+00:00","mainEntityOfPage":{"@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/"},"wordCount":2489,"commentCount":0,"publisher":{"@id":"https:\/\/dialnexa.com\/blogs\/#organization"},"image":{"@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#primaryimage"},"thumbnailUrl":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-3-memory-1.png","keywords":["business automation","Voice AI"],"articleSection":["Engineering"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/","url":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/","name":"AI Company Memory Without Embeddings: Postgres Full-Text Search and Governance","isPartOf":{"@id":"https:\/\/dialnexa.com\/blogs\/#website"},"primaryImageOfPage":{"@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#primaryimage"},"image":{"@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#primaryimage"},"thumbnailUrl":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-3-memory-1.png","datePublished":"2026-07-27T14:24:19+00:00","dateModified":"2026-07-27T14:48:37+00:00","description":"Why we shipped governed AI memory with zero embeddings: Postgres tsvector retrieval, OR-lexeme queries, role-scoped visibility enforced in SQL, revision snapshots, and the gaps we have not closed.","breadcrumb":{"@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#primaryimage","url":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-3-memory-1.png","contentUrl":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2026\/07\/alfred-part-3-memory-1.png","width":1200,"height":630,"caption":"Building Alfred part 3: company memory as a governance problem rather than a vector database"},{"@type":"BreadcrumbList","@id":"https:\/\/dialnexa.com\/blogs\/company-memory-governance-postgres\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/dialnexa.com\/blogs\/"},{"@type":"ListItem","position":2,"name":"Company Memory Is a Governance Problem, Not a Vector Database"}]},{"@type":"WebSite","@id":"https:\/\/dialnexa.com\/blogs\/#website","url":"https:\/\/dialnexa.com\/blogs\/","name":"DialNexa Blog","description":"Voice AI insights, customer communication playbooks, sales automation guides, and contact center operations advice from DialNexa.","publisher":{"@id":"https:\/\/dialnexa.com\/blogs\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/dialnexa.com\/blogs\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/dialnexa.com\/blogs\/#organization","name":"DialNexa","url":"https:\/\/dialnexa.com","logo":{"@type":"ImageObject","url":"https:\/\/dialnexa.com\/blogs\/wp-content\/uploads\/2025\/10\/cropped-cropped-favicon-300x300-1.png","caption":"DialNexa"},"image":{"@id":"https:\/\/dialnexa.com\/blogs\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/dialnexa.com\/blogs\/#\/schema\/person\/1af38c86cbe30b471e5c350bfb15926c","name":"Aditya Kamat","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/44bc46159de51fb66b83a36901f74a2f90b84ae23178c4a55584b7b2861317ba?s=96&d=mm&r=g","caption":"Aditya Kamat"},"description":"Co-Founder of DialNexa. Expert in voice AI, conversational technology, and enterprise telephony. Building the future of AI-powered customer engagement.","sameAs":["https:\/\/dialnexa.com"],"jobTitle":"Co-Founder","url":"https:\/\/dialnexa.com","worksFor":{"@id":"https:\/\/dialnexa.com\/blogs\/#organization"}}]}},"_links":{"self":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts\/6679","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/comments?post=6679"}],"version-history":[{"count":3,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts\/6679\/revisions"}],"predecessor-version":[{"id":6702,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/posts\/6679\/revisions\/6702"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/media\/6707"}],"wp:attachment":[{"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/media?parent=6679"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/categories?post=6679"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dialnexa.com\/blogs\/wp-json\/wp\/v2\/tags?post=6679"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}