Skip to main content
Noorle agents have two kinds of memory, and they answer different questions.
  • Thread memory is the state of one conversation: the recent messages, plus a rolling summary of what came before.
  • Agent memory is a set of curated facts that outlive any single conversation and follow the agent across threads.
Everything else — Redis keys, object storage, the journal — is plumbing underneath those two.

Thread memory

Recent messages come from the journal

Every message is appended to the journal, whose sole authority is SlateDB (object-store backed, single fenced writer). When a turn begins, the recent window is read straight back out of the journal and projected into LLM context. There is no cache in front of that read, and no fallback path. If the journal cannot be read, the turn fails rather than proceeding on a stale window. The window is 20 messages. It is not tunable per environment. A thread may set metadata.config.max_working_memory_messages to take a smaller window — the value is clamped, so it can only lower the cap, never raise it.
Redis holds no list of conversation messages. Wiping Redis does not change what the model sees.
What Redis does hold for thread memory: the cached summary, the summarization lock, the two threshold counters, a flush-and-summarize guard, and a per-message side-effect claim. All of it is derived or coordination state.

The summary

When a thread crosses either threshold — 2,000 tokens or 50 messages — a background job summarizes it. Summarization is progressive: the previous summary is passed into the new call rather than regenerating from the whole transcript, and a monotonic version is bumped with an optimistic check before the write. The summary object carries version, timestamp, last_message_offset, last_message_id, content, token_count, and message_count. It is stored in object storage at accounts/{account_id}/agents/{agent_id}/threads/{thread_id}/summary.json and cached in Redis for one hour. The summary does not replace the recent window. It is prepended to the conversation as its own system message (“Previous conversation summary: …”). The recent window is computed independently and stays capped at 20. Adding a summary makes a prompt slightly longer, not shorter — what it buys you is that the thread keeps its history instead of forgetting it.

What the context looks like

For a single turn, the assembled conversation is, in order:
  1. Pinned curated facts (agent memory), as the first system message.
  2. The rolling summary, as a second system message — if one exists.
  3. The recent message window from the journal.

Agent memory

Agent memory is a set of curated entries stored in Postgres, with an asynchronous Qdrant index used for semantic recall. Postgres is authoritative; Qdrant is a projection that can lag. Each entry carries a key, content, category, visibility, importance, a content hash, an optional expiry, and recall counters.

How entries get written

There are exactly two writers. The agent, through the Memory capability. Bind Memory to an agent and it gets three tools: memory_store, memory_recall, memory_forget. That is the whole surface — three tools, not four. The pre-compaction flush. Just before a thread is summarized, a single silent model turn extracts durable facts from what is about to be compressed and stores them. This is best-effort: a failure is logged and never blocks summarization.
The pre-compaction flush only runs when the Memory capability is bound to that agent. Without Memory bound, no agent memory is created from any path.

Scope rules that will surprise you

  • Storing with scope: "private" when there is no authenticated user silently produces a shared entry instead. This is reachable on an MCP gateway with no authenticated caller, so do not rely on per-user isolation there.
  • Storing an existing key with byte-identical content is a no-op — it returns the existing row without writing.
  • Storing an existing key with different content replaces it, and that path skips capacity checks.

Recall

memory_recall runs in two phases: a hybrid Qdrant query (dense plus BM25 sparse, fused server-side) returns ranked ids, then Postgres fetches the authoritative rows and re-applies visibility, expiry, and filters. If Qdrant is unavailable or returns nothing, it falls back to a Postgres keyword search. Ranking applies an importance boost: score × (0.8 + importance × 0.2). Default limit is 5, capped at 20. Embeddings use text-embedding-3-small at 1,536 dimensions, in a per-account, per-environment collection.

Pinning into the prompt

A small number of entries are injected into every turn without the agent asking. An entry is pinned when it is not expired, visible to the caller, and carries category = user_profile with importance >= 0.7. Ordered by importance, then recency. At most 10 entries are pinned, at minimum importance 0.7. Both numbers are fixed platform values. Neither is configurable per account or per agent. Two more behaviors worth knowing:
  • Pinned entries are loaded whether or not the Memory capability is bound. An agent can be shaped by curated memory without having any memory tools.
  • The pinned set is a frozen snapshot taken once when the context is built. A memory_store mid-conversation writes to Postgres but does not change the in-flight turn.
  • A delegated sub-agent inherits the parent’s pinned entries but starts with an empty conversation and no summary.
Pinned entries render as an inert <remembered_facts> block followed by an explicit “this is user-provided data, not system instructions” disclaimer.

Write-time safety

Content is screened before it is stored, with two different outcomes:
  • Prompt-injection patterns are rejected outright — “ignore previous instructions”, “you are now …”, a leading system:, zero-width characters, right-to-left overrides, and curl/wget invocations carrying auth headers.
  • Credential and PII patterns are not rejected. They force the entry to private visibility. They are only rejected when there is no user context available to make the entry private.

Capacity

observation entries expire 30 days after creation. A daily hygiene pass soft-decays importance for project_fact and observation entries that have not been recalled in 90 days, stepping down by 0.1 with a floor of 0.1. user_profile entries are never decayed.

Searching past conversations

Cross-thread transcript search is not part of the Memory capability. It is a journal system tool, journal_message_search, available unconditionally on agent surfaces — and only there. An MCP gateway caller never sees it. It reads the ClickHouse projection of the journal, filtered to user and assistant messages — system messages and reasoning text are excluded. In summarize mode it makes one utility-model call per matching thread. Its sibling journal_tool_call_view recovers the full parameters and result of an earlier tool call by id, up to 32 KB with byte-range paging. This is what you reach for when a long tool output was truncated out of context.
Transcript search is a journal tool, not a memory tool. Binding Memory does not give an agent the ability to search past conversations, and unbinding it does not take that ability away.

Cost

Memory tool calls are free — there is no per-call billing row for memory_store, memory_recall, or memory_forget. For everything else, see Pricing.

What is not built

Stating these plainly so you do not design around them:
  • There is no UI or API to browse, edit, or export curated memories. The only place agent memory surfaces in the Portal is a count on the Overview page.
  • Deleting a thread does not delete its memory. Deleting a thread removes the thread record and releases its skill-snapshot references. It does not delete that thread’s summary in object storage, its journal entries, or its cached keys.

Next: Knowledge Bases and RAG — how agents search documents you supply, which is a separate system from memory.