Working memory
The live conversation window: threads, messages, and automatic compaction.
Working memory is pure state. Reading it costs three SQL queries, no embeddings, no model calls. It is what you send to the model as chat history.
Threads
Section titled “Threads”A thread is one conversation.
const thread = await memory.createThread({ dataset: 'user_42', tags: ['support', 'billing'], metadata: { channel: 'web', ticketId: 'T-1094' }, autoCompactThreshold: 30,});| Field | Type | Notes |
|---|---|---|
threadId |
string |
UUID. Persist it with your own conversation record. |
dataset |
string |
Whose memory this feeds. Generated if omitted. |
tags |
string[] |
Free labels. |
metadata |
object |
Arbitrary JSON. Merge-updated by threads.update(). |
autoCompactThreshold |
number | null |
null disables compaction. |
createdAt, lastActivityAt |
ISO string | lastActivityAt bumps on every message. |
lastCompactedAt, lastCompactedSequence |
, | Compaction watermarks. |
Threads never end. threads.end() is a misleading name: it triggers
extraction and the thread stays writable. There is no closed or archived state.
Messages
Section titled “Messages”await memory.addMessage(threadId, { role: 'user', // 'user' | 'assistant' | 'system' | 'tool' content: 'I drive a Honda Civic.', tokens: { input: 12, output: 0, total: 12 }, // optional telemetry model: 'gpt-4o', // optional latencyMs: 640, // optional metadata: { stopReason: 'stop', agentName: 'support-bot' },});Returns:
{ "messageId": "0d0f…", "threadId": "f2cb…", "sequenceNumber": 7, "role": "user", "createdAt": "2026-08-16T09:14:02.114Z", "compacted": false}sequenceNumber
Section titled “sequenceNumber”A per-thread monotonic integer assigned by the server inside a row-locked transaction. It is:
- the pagination cursor (
before) - the compaction watermark (
lastCompactedSequence) - independent of timestamps, so concurrent writes can’t interleave ambiguously
You never supply it.
compacted: true
Section titled “compacted: true”Means this insert crossed autoCompactThreshold and triggered a compaction run.
That run happens inline, so this particular call may take up to 30 seconds
(it makes an LLM call). Most calls return in single-digit milliseconds.
prepare(), the read
Section titled “prepare(), the read”const result = await memory.prepare(threadId, { messageLimit: 20 });{ "threadId": "f2cb…", "dataset": "user_42", "messages": [ { "role": "system", "content": "Earlier: the user is choosing a car…" }, { "role": "user", "content": "what about resale value?" }, { "role": "assistant", "content": "Hybrids hold their value well…" } ], "messageCount": 42, "truncated": true, "compacted": true}| Field | Meaning |
|---|---|
messages |
Ready to spread into a chat-completion request. Oldest first. |
messageCount |
Total un-compacted, non-summary messages in the thread. |
truncated |
messageCount > messageLimit, older messages were left out. |
compacted |
An active compact summary is present and is the first element. |
warning |
Present when messageLimit < autoCompactThreshold. Read it. |
The active compact summary is always included first and never counts against
messageLimit, so shrinking the limit can never drop compacted context.
messageLimit defaults to 20, maximum 100.
Compaction
Section titled “Compaction”Once a thread grows past autoCompactThreshold un-compacted messages, older
messages are folded into a single role: 'system' summary.
before after compaction───────────────────────── ─────────────────────────msg 1 user msg 1 ─┐msg 2 assistant msg 2 │ compactedAt set,… … │ excluded from prepare()msg 29 user msg 29 ─┘msg 30 assistant msg 30 user ← keepLast: 1, kept verbatim msg 31 system ← the summaryKey properties:
- Rolling. Each run folds the previous summary into the new one, so there is always exactly one active summary.
- Non-destructive. Original rows are kept and stamped with
compactedAt.listMessages()still returns them;prepare()does not. - The triggering message is kept verbatim (
keepLast: 1) so the next turn sees the user’s actual words rather than a paraphrase. - Summaries are tagged
metadata.type = 'compact_summary'with the range they cover.
Manual compaction
Section titled “Manual compaction”const result = await memory.compact(threadId);// { threadId, summaryMessageId, compactedCount, fromSequence, toSequence }// or { ok: true, compacted: false, message: 'Nothing to compact' }The messageLimit trap
Section titled “The messageLimit trap”If messageLimit < autoCompactThreshold, messages between the summary and the
retrieved tail vanish from context:
threshold 30, messageLimit 10
[summary covering 1–29] [30] [31] … [42] └── only the last 10 returned ──┘ messages 30–32 lostprepare() returns a warning when it detects this. Set
messageLimit >= autoCompactThreshold. See
Handling long conversations.
Listing raw messages
Section titled “Listing raw messages”prepare() is for the model. listMessages() is for you, it returns full rows
including compacted ones.
const { messages, total, hasMore } = await memory.listMessages(threadId, { limit: 50, // 1–100, default 20 before: 120, // cursor: sequenceNumber less than this order: 'asc', // or 'desc'});Thread stats
Section titled “Thread stats”Counts and token totals for a thread live on the HTTP API rather than the SDK,
they are arithmetic over the tokens you supplied, so the client would only be
handing your own numbers back to you.
GET /v1/memory/working/threads/:threadId/stats{ "threadId": "f2cb…", "messageCount": 42, "tokenUsage": { "totalInput": 8120, "totalOutput": 3310, "totalTokens": 11430, "averagePerMessage": 272 }, "sessionDuration": { "ms": 918000, "seconds": 918 }, "createdAt": "…", "lastActivityAt": "…"}tokenUsage is null unless you supplied tokens on messages, Memory Soda
does not count tokens for you.
Relationship to the other layers
Section titled “Relationship to the other layers”Working memory is the source the others derive from:
messages ──► episode summary (episodic) ──► facts + entities (semantic)Compaction and extraction are independent. Compacting does not delete anything extraction needs, episodes record the sequence range they cover, and extraction reads raw message rows within that range.
- Episodic memory, what happens to a finished chunk of conversation
memory.working, full method reference- Handling long conversations