Thread Lifecycle
Thread Lifecycle
Section titled “Thread Lifecycle”Every conversation in Vitamem is a thread — an object that moves through four lifecycle states. This lifecycle is the core innovation: it models how real conversations work — they start, pause, resume, and eventually end — rather than treating all interactions as a flat stream of messages.
The Four States
Section titled “The Four States”Active → Cooling → Dormant → Closed 💬 ⏳ 🧠 📦Active
Section titled “Active”The thread is in an ongoing conversation. Messages are stored in full. No embeddings are computed yet — that would be wasteful while the conversation is still evolving.
Entry conditions: Thread just created, or reactivated from Cooling when a new message arrives.
Cooling
Section titled “Cooling”The conversation has paused. A configurable timer starts (default: 6 hours). During this window, if the user sends a new message, the thread is automatically returned to Active — the conversation resumes seamlessly.
Entry conditions: No messages for coolingTimeoutMs (default: 6 hours).
Exit conditions:
- New message arrives -> back to Active
- Cooling timer expires -> forward to Dormant
Dormant
Section titled “Dormant”The key transition. The thread runs the embedding pipeline:
- LLM extracts facts from all conversation messages
- Each fact is embedded into a vector
- New facts are deduplicated against existing user memories (cosine similarity >= 0.92)
- Unique facts are saved to the memory store
After this, the raw messages no longer need to be processed again. The compressed, deduplicated facts represent everything worth remembering from the conversation.
If reflection is enabled (enableReflection: true), Vitamem runs an additional validation pass during this transition to catch contradictions and enrich vague facts before they are saved.
Memories stored during dormant transitions also participate in active forgetting — older, unretrieved memories naturally decay in relevance over time, keeping the memory store focused on what matters most. During sweep operations, shouldArchive() is available for cleanup of memories that have decayed below the configured threshold.
Entry conditions: Cooling timer expires, or triggerDormantTransition() called explicitly.
Closed
Section titled “Closed”Terminal state. The thread is fully archived. The extracted memories remain in the memory store and are searchable indefinitely. No further processing happens.
Entry conditions:
closeThread()called explicitly (only valid from Dormant)sweepThreads()auto-closes threads that have been dormant forclosedTimeoutMs(default: 30 days)
State Transition Diagram
Section titled “State Transition Diagram” new message ↓ ┌────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │ active │───▶│ cooling │───▶│ dormant │───▶│ closed │ └────────┘ └─────────┘ └─────────┘ └────────┘ ▲ │ │ reactivate │ └─────────────┘ (new message)All invalid transitions throw an InvalidTransitionError.
Valid Transitions
Section titled “Valid Transitions”| From | To | Trigger |
|---|---|---|
active | cooling | No messages for coolingTimeoutMs |
cooling | active | New message arrives (automatic in chat()) |
cooling | dormant | Cooling timer expires, or triggerDormantTransition() |
dormant | closed | Explicit closeThread() call, or sweepThreads() auto-close after closedTimeoutMs |
sweepThreads()
Section titled “sweepThreads()”The sweepThreads() method handles all lifecycle transitions in a single call. It is the recommended way to drive the lifecycle in production.
await mem.sweepThreads();On each invocation, it performs three passes:
-
Active -> Cooling: Finds all
activethreads wherelastMessageAtis older thancoolingTimeoutMsand transitions them tocooling. -
Cooling -> Dormant: Finds all
coolingthreads wherecoolingStartedAtis older thandormantTimeoutMsand transitions them todormant. The embedding pipeline runs for each, extracting and saving memories. -
Dormant -> Closed: Finds all
dormantthreads wheredormantAtis older thanclosedTimeoutMsand transitions them toclosed.
Scheduling sweepThreads()
Section titled “Scheduling sweepThreads()”Vitamem does not run timers internally. Your application is responsible for calling sweepThreads() on a schedule. Common patterns:
// Using node-cronimport cron from "node-cron";cron.schedule("*/15 * * * *", () => mem.sweepThreads());
// Using setIntervalsetInterval(() => mem.sweepThreads(), 15 * 60 * 1000);
// In a serverless environment (e.g., Vercel cron job, AWS Lambda on schedule)export async function handler() { const mem = await createVitamem({ /* ... */ }); await mem.sweepThreads();}Auto-Close with closedTimeoutMs
Section titled “Auto-Close with closedTimeoutMs”The closedTimeoutMs config option (default: 30 days) controls how long a dormant thread lives before sweepThreads() automatically closes it. This prevents dormant threads from accumulating indefinitely.
const mem = await createVitamem({ provider: "openai", apiKey: process.env.OPENAI_API_KEY, storage: "ephemeral", closedTimeoutMs: 90 * 24 * 60 * 60 * 1000, // 90 days before auto-close});Set closedTimeoutMs to a very large value if you want dormant threads to remain open until explicitly closed.
Closing a thread does not delete its extracted memories. Memories remain in the store and are searchable indefinitely via retrieve().
Reactivation
Section titled “Reactivation”When chat() is called on a cooling thread, it automatically reactivates to active before processing the message. This means application code never needs to manually handle the cooling -> active transition — it just works.
// Thread is in 'cooling' stateconst { reply } = await mem.chat({ threadId: thread.id, message: "I have a follow-up question.",});// Thread is now back to 'active' — no manual reactivation neededThis reactivation pattern is especially important for applications with session-based interaction patterns, where users often return after gaps of days or weeks.
Configuring Timeouts
Section titled “Configuring Timeouts”const mem = await createVitamem({ provider: "openai", apiKey: process.env.OPENAI_API_KEY, storage: "ephemeral", coolingTimeoutMs: 2 * 60 * 60 * 1000, // 2 hours (default: 6 hours) dormantTimeoutMs: 6 * 60 * 60 * 1000, // 6 hours (default: same as coolingTimeoutMs) closedTimeoutMs: 30 * 24 * 60 * 60 * 1000, // 30 days (default)});| Config | Default | Description |
|---|---|---|
coolingTimeoutMs | 21600000 (6 hours) | Inactivity before active -> cooling |
dormantTimeoutMs | Same as coolingTimeoutMs | Time in cooling before cooling -> dormant |
closedTimeoutMs | 2592000000 (30 days) | Time in dormant before sweepThreads() auto-closes |
Why This Design?
Section titled “Why This Design?”The lifecycle states map directly to how real conversations work:
- A support session is active while the user is engaged
- The gap after a coaching check-in or tutoring session is cooling — the conversation may resume
- An extended break (days, weeks, or longer) triggers dormant — memories are compressed
- When a conversation thread has run its course — a project wraps up, a patient is discharged, a course ends — it’s closed and the record is preserved
This means Vitamem’s architecture aligns with natural conversation patterns out of the box, without requiring custom logic.