Conversation continuity & memory
Arivie persists conversation history through Mastra Memory backed by Postgres. Every agent.generate() call receives a { thread, resource } pair that scopes the conversation. The same thread ID across requests gives the agent full multi-turn context.
How it works
Section titled “How it works”Mastra Memory stores messages keyed by thread ID and resource ID. The thread groups messages into a conversation; the resource identifies the owner (typically the user). When the agent generates a response, Mastra loads prior messages in the same thread and includes them as context.
The ask() facade
Section titled “The ask() facade”The typed instance.ask() facade handles memory plumbing for you:
const result = await instance.ask({ prompt: "What about last week?", user: { userId: "u1", permissions: ["read"], dbRole: "arivie_reader" }, conversation: { id: "conv-123", resource: "u1", },});conversation.id→ Mastra Memorythread(groups messages)conversation.resource→ Mastra Memoryresource(defaults touser.userId)
If conversation is omitted, a one-shot thread ID is generated per call — no continuity.
HTTP handler
Section titled “HTTP handler”The HTTP handler accepts a conversation object in the request body:
{ "prompt": "What about last week?", "conversation": { "id": "conv-123" }}The handler maps:
conversation.id→memory.threaduser.userId→memory.resource
If no conversation ID is provided, the handler falls back to threadId or generates arivie-<userId>.
Streaming with continuity
Section titled “Streaming with continuity”SSE streaming works the same way — pass conversation.id in the initial request and the agent streams the response while maintaining memory context:
curl -X POST http://localhost:3000/chat \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{"prompt": "How much revenue yesterday?", "conversation": {"id": "mon-day-1"}}'CLI chat with history
Section titled “CLI chat with history”The kitchen-sink example’s CLI chat script (scripts/chat.ts) demonstrates a full conversation history picker:
- Conversations are saved to
workspace/conversation-history.json - The
/newcommand starts a fresh conversation - Previous conversations can be resumed by selecting from a list
- Each conversation maintains its own thread ID for continuity
Memory scopes
Section titled “Memory scopes”Arivie uses two memory scopes within one instance:
| Scope | Thread pattern | Use case |
|---|---|---|
| Personal | conversation.id or arivie-<userId> | Per-user conversations |
| Instance-global | schedule-<id> or __instance__ | Schedules and shared context |
| Channel-scoped | Derived from event metadata | Trigger-driven conversations |
Channel subscriptions can dynamically scope memory using instanceId and resourceId functions that read from the event payload — for example, isolating conversations per GitHub repository.
Lifecycle hooks
Section titled “Lifecycle hooks”Memory is auditable through lifecycle hooks:
defineArivie({ // ... hooks: { onMemorySave: async ({ memory, scope, userId }) => { console.log(`Memory saved for ${userId} in ${scope}`); }, onMemoryDelete: async ({ memory, scope, userId }) => { auditLog("memory_deleted", { scope, userId }); }, },});AsyncLocalStorage context
Section titled “AsyncLocalStorage context”The execute_<source> tools need to know the current user’s DB role without explicit threading. Arivie uses AsyncLocalStorage (pinned to globalThis) to carry UserContext through the agent’s tool calls:
import { runWithUserContext } from "@arivie/core";
await runWithUserContext(user, async () => { // Inside this frame, execute_<source> sees `user.dbRole` const result = await agent.generate(prompt, { memory });});The ask() facade and HTTP handler do this automatically.