# Memory Guide

## Brain-first lookup

The graph is the brain, and every turn that needs to know something runs the same five-step loop in order: (1) classify the question (entity, temporal, event, general, or none — the inbound gateway emits this as `retrievalClass`), (2) read the graph with `memory-search` (and `profile-read` when the question is about the operator) as the first tool call of the turn, (3) walk one hop to hydrate a partial hit before calling it a miss, (4) call an external tool only when steps 2–3 confirmed the graph has nothing useful, and (5) write the external evidence back via `database-operator`. The loop is what makes the next turn smarter; an external call whose result is never persisted is a leak in the brain. `retrievalClass = none` (greetings, meta-instructions) is the only exception. Operator-facing doctrine lives in [`.docs/brain-first.md`](../../../.docs/brain-first.md).

## How Memory Works

{{productName}} maintains a graph of everything you've told it. Contacts, conversations, preferences, relationships, business context — all stored as connected nodes in a local Neo4j database on your Raspberry Pi.

When you ask {{productName}} about something, it searches this graph first. It retrieves relevant context before responding, which is why {{productName}} can pick up where you left off even across separate sessions.

The graph lives entirely on your hardware. Nothing is sent to the cloud.

## What Gets Remembered

{{productName}} stores:

- **Contacts** — people, companies, relationships between them
- **Conversations** — key decisions, commitments, follow-ups mentioned in chat
- **Preferences** — things you've told {{productName}} about how you like to work
- **Context** — project status, ongoing threads, background you've shared

{{productName}} remembers details you mention naturally: "I'm meeting with Sarah on Thursday" creates a memory that Thursday has a meeting with Sarah.

## Telling {{productName}} to Remember Something

Just say it naturally:

- "Remember that I prefer morning calls"
- "Note that the Johnson account is on hold until March"
- "My wife's name is Emma, keep that in mind"

{{productName}} will confirm and store it.

## How {{productName}} learns how you work

{{productName}} also learns how you work without you having to teach it deliberately. Six broad areas cover the way most operators run a business — communication, scheduling, decisions, workflow, content, and interaction. Inside each area sits a small set of concrete fields ({{productName}} tracks around 28 in total) such as your preferred channel, quiet hours, workday start time, risk tolerance, content tonality, or address form. {{productName}} tracks which of these specific fields you have spoken into and which are still empty. While any are empty, it folds one organic question per turn into the conversation aimed at the next gap — never a list, never a form, never the same question twice. If you tell {{productName}} a field doesn't apply to you ("I work weekends, weekend availability isn't a thing for me"), it marks that field as covered and never re-asks. Once every field is either set or marked not-applicable, the proactive questions stop and {{productName}} answers what you ask without volunteering more. This is why session 300 should feel sharper than session 3: the longer you work together, the less {{productName}} needs to ask.

## Telling {{productName}} to Forget Something

Be direct:

- "Forget everything about the Johnson account"
- "Remove Sarah's contact record"
- "Clear what you know about my pricing preferences"
- "Delete that pricing guide I uploaded"

{{productName}} will confirm before deleting anything significant. Documents are soft-deleted first (excluded from search but recoverable for 7 days). Say "permanently delete" to remove immediately.

## Managing Documents

### Listing files

Ask: "What files do I have stored?" or "List my attachments"

{{productName}} shows all uploaded files with their ingestion status — whether they've been processed into the knowledge graph.

When you upload something for ingestion, {{productName}} emits a one-line size estimate before it starts: short documents (<5K chars) classify in ~10s; mid-size (10K–20K chars) take ~45–90s; very large (>20K) up to ~3 minutes. If the classifier exceeds its 3-minute ceiling {{productName}} aborts loudly with a "Classifier unavailable — timeout" blocker and writes nothing — you can re-upload or split the document.

### Reading files

Ask: "Show me what's in the pricing guide" or "Read the quarterly report"

{{productName}} returns the full content of text and markdown files, extracted text from PDFs, and metadata for images.

### Editing files

Ask: "Update the pricing in that document" or "Change the introduction paragraph"

{{productName}} reads the file, makes the edit, and prepares it for re-ingestion into the knowledge graph. Only text and markdown files can be edited — PDFs and images cannot.

### Renaming files

Ask: "Rename that file to quarterly-report-q1.pdf"

{{productName}} updates the filename in both the stored metadata and the knowledge graph.

### Deleting documents

Ask: "Delete the old pricing guide"

By default, documents are soft-deleted — they stop appearing in search results but remain recoverable for 7 days. To permanently delete immediately, say "permanently delete" or "force delete".

## Searching Memory

Ask naturally:

- "What do you know about Tom Henderson?"
- "What did I last discuss about the Acme proposal?"
- "Who have I met from the fintech conference?"

## Thinking tools

Three slash commands that apply analysis to what's already in your graph:

**`/challenge <claim>`** — stress-tests an assertion. {{productName}} searches your graph for nodes that contradict or qualify the claim — nodes that assert the opposite, name exceptions, or add significant caveats — and presents the strongest counter-case it finds. If nothing in your graph challenges the claim, it says so rather than inventing one. Results cite node IDs and relevance scores so you can inspect the sources directly.

**`/connect <topic-A> <topic-B>`** — finds the bridge. {{productName}} searches both topics, collects their immediate graph neighborhoods, and looks for nodes they share. If a direct bridge exists it names it in one sentence. If not, it surfaces the closest approach — the two nodes that are semantically nearest across the two sides — and proposes the connection you could draw.

**`/emerge`** — names the unnamed clusters. {{productName}} retrieves your KnowledgeDocument and Section nodes that are not yet connected to a Concept node, groups them by shared theme, and proposes a Concept name for each cluster. You approve or skip each proposal one at a time; nothing is written without your confirmation. Clusters of fewer than three nodes are listed at the end as "too small to cluster."

## Listing and counting

{{productName}} answers relational questions — "list all my people", "how many tasks do I have", "find the person with email X", "show me the 20 most recently created nodes" — via direct read-only Cypher against your Neo4j. This is faster and more precise than semantic search when the question is "the exact set where", not "things similar to".

You can also open a visual view of your graph at any time from the burger menu → **Graph**. Click the **Filter** button in the toolbar to open the filter menu — it lists only the top-level entity types in your schema (Conversation, Person, Task, KnowledgeDocument, …), one row per type, showing your per-type node count and sorted so the most-connected types sit at the top. Child types (messages inside a conversation, sections inside a document), conversation channel variants (admin vs public), message role variants (user vs assistant), and workflow execution plumbing (`ToolCall`, `WorkflowRun`, `WorkflowStep`, `StepResult`) never appear as filter rows — you reach children by clicking the parent and exploring its neighbourhood. Active rows render a force-directed map, coloured by label. Click a node to pivot into its 1-hop neighbourhood; click another node inside that neighbourhood to pivot again. Clicking a Message shows its details in the side panel; the Conversation view stays put — you read sibling messages without losing the chain on canvas. A breadcrumb strip above the canvas shows where you are (`Filter › Conversation › AssistantMessage`). The **Back** control pops one level — three clicks in always undoes with three Back presses; the filter view is the irreducible root. Click the **×** inside the filter menu to clear your chip selection. Type in the search box to highlight matches; submitting a search also widens the filter to include any node types the hits belong to, so relevant matches render instead of disappearing into a "not in current view" banner.

Conversations and Messages carry role/channel sublabels so you can read the chat topology by colour alone — admin vs public conversations and user vs assistant messages render in distinct shades on the canvas. The filter menu intentionally does not split them into separate rows — the base chip is the entry point; you see the variants as colours once you're inside a neighbourhood.

**Save a default view:** once you have the rows you want, click **Set default view** in the filter menu. Next time you open **Graph**, those rows are pre-selected and your data renders immediately. The default is per-admin, per-account — each admin on each account has their own.

**Delete a node:** drag it to the trash icon top-right of the canvas. No confirmation — deletes are reversible for 30 days. To restore, toggle **Show trashed** inside the filter menu and click **Restore** on the node, or ask {{productName}} in chat ("restore the <label> I just deleted"). Deleting a conversation also trashes its messages in the same step, so they reappear together on restore.

**Bulk cleanup of conversations in chat:** when you ask {{productName}} to clean up conversations in bulk ("trash all empty conversations," "clean up the single-assistant tests"), the agent uses a deterministic selector with a fixed set of filter names — it cannot author custom delete queries. The server re-runs the same filter on every candidate before it trashes, so a stale list can't destroy something the filter wouldn't match now. If the filter matches nothing, {{productName}} reports "no candidates" and nothing happens.

The page reads only your own brand's Neo4j — a Maxy device and a Real Agent device share no graph state even when on the same laptop. No credentials are required; the view inherits your admin session.

**Typo-proof cypher.** When {{productName}} runs direct Cypher to answer a relational question, the query is checked against your Neo4j's live label and relationship-type taxonomy before it executes. Cypher that references an unknown name (an edge or label that does not exist in your graph) is rejected for writes and flagged with a warnings header for reads, so {{productName}} never silently acts on a query that targeted the wrong set of nodes. You should not see this — it runs invisibly — but it is the safety net that stops a fabricated edge name from producing "empty" results that are really just unreachable. Before acting on a bulk operation {{productName}} surfaces the result count and a sample; if it ever describes a cypher rejection, that means its first attempt was malformed and it corrected itself.

## Bi-temporal timeline events

Every factual statement {{productName}} extracts from your conversations is stored as a `:TimelineEvent` node. Each event carries two separate timestamps:

- **`occurredAt`** (valid-time) — when the fact was true in the world. Set from the text itself; can reference a date in the past ("Alice joined in 1990" stores `occurredAt = 1990-01-01`).
- **`learnedAt`** (transaction-time) — when the system ingested this event. Always the wall-clock time of the write; never back-dated.

This distinction lets you ask two qualitatively different questions:

- *"What happened to Alice in 1990?"* — query by `occurredAt`.
- *"What did {{productName}} learn about Alice last Tuesday?"* — query by `learnedAt`.

`memory-compiled-truth-history` returns both fields for every timeline event on an entity under the `timelineEvents` array, alongside the compiled-truth revision history in the `revisions` array.

**Backfill:** Timeline events written before this feature was added have `learnedAt` set to their `createdAt` value by the schema migration. Events without `createdAt` (very old nodes) receive the migration run time as an approximation.

## Write doctrine

Every new node in {{productName}}'s graph is created with at least one connection to an existing node. A contact connects to the conversation or organisation it came from; a task connects to the session that raised it or the entities it will affect; a session summary connects to the conversation it summarises. A node with no connection is noise — it cannot be attributed, traversed, or explained — so the graph refuses to create one. If {{productName}} ever tries to record something without a link, the write is rejected and {{productName}} asks you to clarify where it belongs.

Every node also carries a provenance stamp — which agent wrote it, in which session, via which tool. You never see these fields, but they are how operators trace unusual growth back to the code path that produced it, and why your graph stays clean over time.

**Two write surfaces, one substrate.** General agents write through schema-aware helpers — {{productName}} can record a new contact, a new commitment, a new preference without ever typing a database query, and the helper enforces the connection-and-provenance rule above structurally. The graph-steward role (the specialist {{productName}} dispatches when you ask for graph hygiene — "merge those two duplicate contacts," "wire those four tasks to the meeting," "rename the legacy label across the graph") additionally has a raw Cypher write tool for the multi-step operations the helpers cannot express. The steward role internalises the same connection-and-provenance discipline in its prompt; a post-write audit emits a warning on every breach so the same rules apply to both surfaces. Both paths feed the same hourly orphan trend and the same forensic provenance fields — read-side, you cannot tell the two apart, and that is the point.

## Vertical schemas

On top of the base graph, each brand boots one optional **vertical** — an extra set of entity types tailored to a trade. The vertical is named by `brand.json#vertical` and defined in a `schema-<name>.md` reference; the memory plugin loads it at startup and validates every write against base + the active vertical. Real Agent boots `schema-estate-agent` (Listing, Property, Viewing, Offer). SiteOffice boots `schema-construction`, which adds the building-contractor entities — `Job`, `LineItem`, `Valuation`, `Milestone`, `QuoteDocument`, `VariationNote`, `InboundInvoice`, `SubContractor`, `TimeLog`, `SubInvoice`, `WhatsAppGroup` — grounded in real builder job folders so a job's quote, valuations, variations, supplier invoices, and subcontractor timesheets all hang off one `Job` node. The default Maxy brand boots no vertical (base graph only).

## Public-facing summaries for customer-readable subjects

Some entities in your graph are knowable by people outside your team — companies you work with, projects you've delivered, the business itself. For those entities ({{productName}} treats `:Organization`, `:Concept`, `:Project`, and `:LocalBusiness` this way), {{productName}} maintains two summaries: a private one only you and your specialist agents see, and a customer-facing public one your public agents are allowed to surface.

Whenever {{productName}} updates the private summary on one of these entities, it automatically rewrites the public summary in the same step using a separate prompt that strips operator-voice ("needs follow-up", "action: chase next week"), internal sentiment, and anything that reads like a note-to-self. The two summaries stay in lockstep without you doing anything.

If you want to write the public summary yourself — for instance, because the auto-generated version misses something you want customers to see — just tell {{productName}} the wording you want for the public summary on that entity, and {{productName}} will write it directly. It stays locked in for seven days; after that, the next automatic refresh can take over again, unless you re-pin it.

People entries (`:Person`) are deliberately excluded from this dual-summary system. Notes about contacts are private by definition and never get a public-facing form.

## Privacy

All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.

If you want to wipe everything and start fresh, ask: "Reset my memory graph." {{productName}} will ask for confirmation before doing so.
