Skip to content

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.

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 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 Memory thread (groups messages)
  • conversation.resource → Mastra Memory resource (defaults to user.userId)

If conversation is omitted, a one-shot thread ID is generated per call — no continuity.

The HTTP handler accepts a conversation object in the request body:

{
"prompt": "What about last week?",
"conversation": { "id": "conv-123" }
}

The handler maps:

  • conversation.idmemory.thread
  • user.userIdmemory.resource

If no conversation ID is provided, the handler falls back to threadId or generates arivie-<userId>.

SSE streaming works the same way — pass conversation.id in the initial request and the agent streams the response while maintaining memory context:

Terminal window
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"}}'

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 /new command starts a fresh conversation
  • Previous conversations can be resumed by selecting from a list
  • Each conversation maintains its own thread ID for continuity

Arivie uses two memory scopes within one instance:

ScopeThread patternUse case
Personalconversation.id or arivie-<userId>Per-user conversations
Instance-globalschedule-<id> or __instance__Schedules and shared context
Channel-scopedDerived from event metadataTrigger-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.

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 });
},
},
});

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.