Skip to content
KONIGI

AI Assistants / Memory and context / Memory chip

4 of 5

Memory chip

The assistant knows something about the viewer that the viewer never agreed to in this conversation.

Updated September 12, 2026

Problem

An answer arrives in metric units, or mentions a project by name, or assumes a job title nobody stated. The assistant is drawing on something it retained from a previous conversation. The viewer didn’t watch it being saved, can’t see what else is in there, and has no idea what the thing knows about them.

Solution

Make the two moments visible: the moment something is written, and the moment something is used.

The write is the one products skip. A silent save is how a viewer ends up surprised weeks later, and the fix is small: a quiet inline marker at the turn where the memory was created, naming what was stored in the words it was stored in. Saved: prefers metric units. That marker has to be dismissible and reversible on the spot, because the moment of capture is the only moment the viewer has full context to judge whether it should be kept.

The read is the more interesting one and the harder to do without clutter. When an answer leaned on a stored fact, saying so converts an unexplained assumption into a visible inference. Google’s shipped version of this is worth copying as a floor: a viewer can ask Gemini directly whether it used information from past chats. Having to ask is a weak affordance next to marking the turn inline. It’s also far better than nothing, and it concedes the principle that matters: the viewer is entitled to an answer.

Underneath both moments there has to be a plain list. Every stored item, in readable language, individually deletable, with the date it was captured. People almost always want to strike one wrong fact rather than start over, so a store that can only be wiped wholesale gives them nothing to work with. HAX guideline 17 asks for global controls over what the system remembers and learns; the per-item version is what makes the global one usable.

Scope is the failure nobody anticipates until it happens. A detail absorbed from a personal conversation surfacing in a work document, or a client’s name appearing in a draft for a different client, is the kind of error that ends use of a product. Separating memory by project or workspace contains it. So does the simpler move of making memory opt-in per context rather than global by default.

Use when

The assistant is used repeatedly by the same person for related work, and repeating context is a real tax on them.

Don’t use when

Use is occasional, anonymous, or shared across people on one account. Memory built from a shared login models a person who doesn’t exist and produces confidently wrong personalisation.

Trade-offs

Memory makes an assistant feel attentive and makes its behaviour harder to predict, because the same prompt now produces different answers for reasons invisible in the prompt. For anyone doing careful work, that’s a real debugging cost. Surfacing every write is honest and noisy, and the noise lands on the people using the product most. Hiding writes is calm and produces the surprise this pattern exists to prevent. Once memory is a feature it becomes a data surface, with retention, export and deletion obligations that reach well past the interface.

Checklist

  • Is there a visible moment when something is written to memory?
  • Can that write be undone at the moment it happens?
  • Is every stored item listed in plain language, with a date, individually deletable?
  • When an answer used a memory, can the viewer tell?
  • Is memory scoped by workspace or project, or global to the account?
  • What’s the default, and was it opted into?
  • What does deleting a memory actually delete, and by when?
  • Can the viewer see what would change if they turned memory off?
  • Does a shared or team account build one merged profile?
  • Is memory carried into exports, shared conversations, and screenshots?

Compare

ChatGPT keeps an editable list of stored details and marks the turn where a memory is written. The write is visible at the one moment a viewer can still judge it. Gemini ties personalisation to past chats as a whole rather than to extracted facts, and answers the recall question on request, which trades per-item precision for a simpler mental model. Claude leans on projects, so the persistent material is a body of documents the viewer assembled deliberately rather than inferences drawn from conversation. Notion keeps the equivalent state in the workspace itself, where what the assistant knows is whatever the viewer already wrote down and has permission to read.

Conversation history is the other persistence layer and the one this gets confused with. Custom instructions is the version the viewer writes deliberately rather than the system infers. Scoped context is memory bounded to a workspace. First-run state is where the data question gets raised before any of this starts. Citation chip is the same idea applied to sources rather than to stored facts.

Memory chip anatomy A marker at the turn where a detail was written to memory, with undo available in place, a later answer marked as having used a stored fact, and a plain list of stored items each dated and individually removable. Make the write visible, and the read too Saved: prefers metric units undo used a stored detail What it remembers prefers metric units 4 Mar × works on the Rivera account 1 Aug × is a designer, not an engineer 1 2 3 4 1 THE MOMENT OF WRITING A silent save is how someone is surprised weeks later. Undo has to be in place. 2 THE MOMENT OF USING Saying so turns an unexplained assumption into a visible inference. 3 ONE ITEM AT A TIME People want to remove one wrong fact. A wipe-everything button is not a control. 4 SCOPE LEAKS A client name from one project appearing in a draft for another ends product use. Memory makes the same prompt give different answers for reasons invisible in the prompt.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Make the write visible and the read too. A marker at the turn where something was stored, undo available in place, and a plain dated list where each item can be removed on its own.

shadcn
npx shadcn@latest add badge button
npm
lucide-react
Tokens
--card--card-foreground--muted--muted-foreground--border--status-warn--chart-1

The unit is 3.2 m wide and 2.4 m deep, so it clears the doorway with 15 cm to spare. I'll keep to metric from here on.

Saved: prefers metric units

The Rivera figures spreadsheet gives the floor area in square metres, so the totals below are in m² as well.

What it remembers

  • prefers metric units4 Mar
  • works on the Rivera account1 Aug
  • is a designer, not an engineerpersonal19 Aug

MemoryChip.tsxThe write marker with undo, the read marker, and the list where each item carries its date, its scope and its own remove.

import { X } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

/**
 * A stored item is text, a date and a scope. The date is what lets the viewer
 * judge it later; the scope is what stops a client's name from one project
 * turning up in a draft for another.
 */
export type Memory = {
  id: string;
  /** In the words it was stored in. "prefers metric units", not a key. */
  text: string;
  savedAt: Date;
  /** Workspace or project the item belongs to. */
  scope: string;
};

const day = (d: Date) => d.toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" });

/**
 * The moment of writing. Sits on the turn that created the memory and names
 * what was stored. Undo is here rather than in a settings page because this
 * is the only moment the viewer has the context to judge it.
 */
export function SavedMarker({ memory, onUndo }: { memory: Memory; onUndo: (id: string) => void }) {
  return (
    <div className="flex items-center gap-3" role="status">
      <Badge variant="secondary" className="rounded-md bg-chart-1/20 font-normal text-chart-1 hover:bg-chart-1/20">
        Saved: {memory.text}
      </Badge>
      <Button variant="link" size="sm" className="h-auto p-0 text-xs text-muted-foreground" onClick={() => onUndo(memory.id)}>
        undo
      </Button>
    </div>
  );
}

/**
 * The moment of using. An answer that leaned on a stored fact says so, which
 * turns an unexplained assumption into an inference the viewer can check.
 */
export function UsedMarker({ memory, onShow }: { memory: Memory; onShow?: (id: string) => void }) {
  return (
    <button type="button" className="text-xs text-muted-foreground underline-offset-2 hover:underline" onClick={() => onShow?.(memory.id)}>
      used a stored detail
    </button>
  );
}

/**
 * The plain list. Every item, dated, removable on its own. An item from
 * another scope is labelled, because seeing it here is how a leak is caught.
 */
export function MemoryList({ memories, scope, onRemove }: {
  memories: Memory[];
  /** The scope the viewer is in now. */
  scope: string;
  onRemove: (id: string) => void;
}) {
  return (
    <div className="rounded-lg border bg-muted p-3">
      <p className="text-[11px] uppercase tracking-wide text-muted-foreground">What it remembers</p>
      {memories.length === 0 ? (
        <p className="mt-2 text-xs text-muted-foreground">Nothing yet.</p>
      ) : (
        <ul className="mt-1 text-xs">
          {memories.map((m) => (
            <li key={m.id} className="flex items-center gap-2 py-1">
              <span className="text-card-foreground">{m.text}</span>
              {m.scope !== scope && (
                <span className="rounded-sm border border-status-warn px-1 text-[10px] text-status-warn">{m.scope}</span>
              )}
              <span className="ml-auto tabular-nums text-muted-foreground">{day(m.savedAt)}</span>
              <Button variant="ghost" size="icon" className="size-5 text-muted-foreground" aria-label={`forget "${m.text}"`} onClick={() => onRemove(m.id)}>
                <X className="size-3" />
              </Button>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

demo.tsxHow it is called: three stored items, one just written, one just used, one from another scope.

import { useState } from "react";
import { MemoryList, SavedMarker, UsedMarker, type Memory } from "./MemoryChip";

/**
 * Two turns and the list beneath them. The first turn wrote a memory and
 * carries the marker with undo; the second read one and says so. The third
 * stored item came from a personal chat and is labelled as out of scope.
 */
const STORED: Memory[] = [
  { id: "m1", text: "prefers metric units", savedAt: new Date("2026-03-04T12:00:00Z"), scope: "Rivera" },
  { id: "m2", text: "works on the Rivera account", savedAt: new Date("2026-08-01T12:00:00Z"), scope: "Rivera" },
  { id: "m3", text: "is a designer, not an engineer", savedAt: new Date("2026-08-19T12:00:00Z"), scope: "personal" },
];

export default function Demo() {
  const [memories, setMemories] = useState(STORED);
  const forget = (id: string) => setMemories((ms) => ms.filter((m) => m.id !== id));
  const saved = memories.find((m) => m.id === "m1");
  const used = memories.find((m) => m.id === "m2");

  return (
    <div className="rounded-lg border bg-card p-4 text-sm text-card-foreground">
      <p>The unit is 3.2 m wide and 2.4 m deep, so it clears the doorway with 15 cm to spare. I'll keep to metric from here on.</p>
      {saved && <div className="mt-3"><SavedMarker memory={saved} onUndo={forget} /></div>}

      <div className="mt-4 grid gap-5 border-t pt-4 sm:grid-cols-[0.8fr_1fr]">
        <div>
          <p>The Rivera figures spreadsheet gives the floor area in square metres, so the totals below are in m² as well.</p>
          {used && <div className="mt-2"><UsedMarker memory={used} /></div>}
        </div>
        <MemoryList memories={memories} scope="Rivera" onRemove={forget} />
      </div>
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.