@ayberkaya/memshot
v0.2.1
Published
Tiered, token-budget-aware memory for any LLM agent
Maintainers
Readme
memshot
Your agent's memory shouldn't cost 15,000 tokens before the user says hello.
memshot is a tiered, token-budget-aware memory library for LLM agents. Zero runtime dependencies. No vector DB. No server. Works anywhere JavaScript runs — Node, Deno, Bun, edge functions, the browser.
The problem
Most memory libraries have no selection layer. Inject everything on every call. At 500 stored memories:
naive injection:
████████████████████████████████████████████████ 36,552 tokens
^^^^^^^^^^^^^^^^^^
9.1× your context budgetThe signal-to-noise ratio collapses. Relevant context drowns in noise.
How memshot fixes it
memshot (4000-token budget):
████ 3,957 tokens (62 items selected from 500)
↑ only what's relevant to this promptmemshot selects which memories to inject using three tiers:
┌─────────────────────────────────────────────────────────────────────┐
│ prompt: "we discussed billing last week, what did we decide?" │
└─────────────────────┬───────────────────────────────────────────────┘
│
┌────────────▼────────────┐
│ hot tier │ always injected (once:true = once/session)
│ "User's name is Ayberk"│
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ warm tier │ injected when triggers regex matches prompt
│ billing/pricing rules │ ← /billing|pricing/i matched
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ cold tier │ corpus BM25 + frecency → greedy knapsack
│ top-N relevant history │ until budget exhausted
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ 4000-token budget │
│ injected to LLM │
└─────────────────────────┘| Tier | Selection | Use for |
|------|-----------|---------|
| hot | Always included. once: true injects once per sessionId. | Identity, system facts, permanent instructions |
| warm | Included when any triggers regex matches the prompt. No triggers = always warm. | Domain knowledge, project context |
| cold | Corpus-aware BM25 keyword relevance + frecency decay, selected greedily by score/token ratio. | Conversation history, decisions, event log |
Install
npm install @ayberkaya/memshot
# or: bun add @ayberkaya/memshot
# or: npx memshot (CLI, no install required)Zero runtime dependencies. Optional: npm install gpt-tokenizer for exact token counts.
Quickstart
import { Memory, fileStore } from "@ayberkaya/memshot"
const mem = new Memory({ budget: 4000, store: fileStore("./memories") })
// Add memories to tiers
await mem.add({ content: "User's name is Ayberk. Prefers TypeScript.", tier: "hot" })
await mem.add({ content: "Billing: ship Stripe subscriptions first, add metering later.", tier: "warm", triggers: [/billing|pricing/i] })
await mem.add({ content: "Meeting 2026-06-20: decided to delay enterprise tier until Q3.", tier: "cold" })
// Resolve against the current prompt — returns only what fits in budget
const { text, tokensUsed, tiersUsed } = await mem.resolve(userPrompt, { sessionId: "abc123" })
// Prepend to your system prompt
const response = await openai.chat.completions.create({
messages: [
{ role: "system", content: `${text}\n\n${yourSystemPrompt}` },
{ role: "user", content: userPrompt }
]
})fileStore persists to disk as JSON files. For in-process use, tests, and edge functions: swap in memoryStore().
Benchmark
500 memories (5 hot, 45 warm, 450 cold), 4000-token budget, billing-related prompt:
memshot benchmark — 500 memories, 4000-token budget (gpt-tokenizer cl100k)
─────────────────────────────────────────────────
naive memshot savings
items 500 62 -87.6%
tokens used 36,552 3,957 -89.2%
─────────────────────────────────────────────────
reproduce: npm run benchmarkRun it yourself:
git clone https://github.com/ayberkaya/memshot
cd memshot && npm install
npm run benchmarkAPI Reference
new Memory(config)
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| budget | number | yes | — | Max tokens to inject per resolve call |
| store | Store | yes | — | fileStore(dir) or memoryStore() |
| tokenizer | Tokenizer | no | heuristic | Plug in gpt-tokenizer for exact counts |
| ledger | SessionLedger | no | in-memory | Tracks once: true per session |
mem.add(item, opts?)
await mem.add({ content, tier, triggers?, once?, tags? })
// Dedup: skip if near-duplicate already exists (Jaccard ≥ 0.85)
await mem.add({ content, tier }, { dedupe: true })
await mem.add({ content, tier }, { dedupe: { threshold: 0.9, strategy: "update", scope: "all" } })| Field | Type | Description |
|-------|------|-------------|
| content | string | Text to store |
| tier | "hot" \| "warm" \| "cold" | Selection tier |
| triggers | RegExp[] | Warm: inject when one of these matches the prompt |
| once | boolean | Hot: inject only once per sessionId |
| tags | string[] | Arbitrary labels on retrieved items |
mem.resolve(prompt, opts?)
const result = await mem.resolve(prompt, {
sessionId?: string,
now?: number, // override Date.now() for tests
trace?: boolean // include per-item score breakdown
})Returns ResolveResult:
| Field | Type | Description |
|-------|------|-------------|
| text | string | Ready-to-inject block; prepend to system prompt |
| items | MemoryItem[] | Selected items in tier order |
| tokensUsed | number | Total tokens consumed |
| tiersUsed | { hot, warm, cold: number } | Items per tier |
| dropped | { warm, cold: number } | Items excluded by budget |
| trace? | ResolveTrace | Per-item breakdown (only when trace: true) |
mem.resolve with trace
const { trace } = await mem.resolve(prompt, { trace: true })
for (const entry of trace.entries) {
console.log(entry.tier, entry.included ? "✓" : "✗", entry.reason)
if (entry.scores) {
console.log(" bm25:", entry.scores.bm25Normalized.toFixed(3),
"frecency:", entry.scores.frecencyNormalized.toFixed(3),
"composite:", entry.scores.composite.toFixed(3))
}
}Trace scope: items that entered budget allocation (all included + budget-dropped). Items filtered before scoring (unseen once:true hot, trigger-miss warm) are not yet traced.
mem.stats()
const stats = await mem.stats()
// {
// total: 500,
// byTier: { hot: 5, warm: 45, cold: 450 },
// tokens: { total: 52000, byTier: {...}, average: 104 },
// oldest: { id: "...", createdAt: 1719000000000 },
// newest: { id: "...", createdAt: 1719400000000 },
// cold: { totalAccesses: 1230, averageAccessCount: 2.7 }
// }mem.update(id, patch)
await mem.update(id, { content: "Updated decision: defer until Q4." })
await mem.update(id, { tier: "hot", once: true })Patchable fields: content, tier, tags, triggers, once. Engine-managed fields (createdAt, accessCount, lastAccessedAt) are excluded to preserve frecency integrity.
mem.delete(id) / mem.clear()
await mem.delete(itemId)
await mem.clear()CLI
$ npx memshot add "User prefers TypeScript strict mode." --tier hot
added 1782466602860-g6e4cu [hot]
$ npx memshot add "Billing: ship Stripe first, metering later." --tier warm --trigger "/billing/i"
added 1782466603120-h7f2ab [warm]
$ npx memshot list
ID TIER TOKENS CONTENT
────────────────────────────────────────────────────────────────────────────────
1782466602860-g6e4cu hot 6 User prefers TypeScript strict mode.
1782466603120-h7f2ab warm 8 Billing: ship Stripe first, metering later.
$ npx memshot resolve "what are the billing rules?" --budget 4000 --trace
tokens used: 14 / 4000
tiers: hot=1 warm=1 cold=0
dropped: warm=0 cold=0
selected:
1782466602860-g6e4cu [hot] User prefers TypeScript strict mode.
1782466603120-h7f2ab [warm] Billing: ship Stripe first, metering later.
ID TIER INC TOKENS COMPOSITE REASON
──────────────────────────────────────────────────────────────────────────────────────────
1782466602860-g6e4cu hot ✓ 6 — always injected
1782466603120-h7f2ab warm ✓ 8 — trigger matched, fit budget
$ npx memshot stats
total: 2 memories
hot 1 items 6 tokens
warm 1 items 8 tokens
cold 0 items 0 tokens
tokens: total=14 avg=7.0
oldest: 1782466602860-g6e4cu (2026-06-26T...)
newest: 1782466603120-h7f2ab (2026-06-26T...)--trace prints a table with id, tier, ✓/✗, tokens, composite score, and reason for every considered item.
Persistence: MEMSHOT_DIR env var (default ./.memshot).
Web playground
Or open playground/index.html locally — no build step, no server, no npm install.
Integration recipes
OpenAI
import OpenAI from "openai"
import { Memory, fileStore } from "@ayberkaya/memshot"
const mem = new Memory({ budget: 4000, store: fileStore("./memories") })
const openai = new OpenAI()
async function chat(userMessage: string, sessionId: string) {
const { text } = await mem.resolve(userMessage, { sessionId })
return openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: `${text}\n\nYou are a helpful assistant.` },
{ role: "user", content: userMessage }
]
})
}Anthropic
import Anthropic from "@anthropic-ai/sdk"
import { Memory, fileStore } from "@ayberkaya/memshot"
const mem = new Memory({ budget: 4000, store: fileStore("./memories") })
const anthropic = new Anthropic()
async function chat(userMessage: string, sessionId: string) {
const { text } = await mem.resolve(userMessage, { sessionId })
return anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
system: `${text}\n\nYou are a helpful assistant.`,
messages: [{ role: "user", content: userMessage }]
})
}LangChain
import { ChatOpenAI } from "@langchain/openai"
import { SystemMessage, HumanMessage } from "@langchain/core/messages"
import { Memory, fileStore } from "@ayberkaya/memshot"
const mem = new Memory({ budget: 4000, store: fileStore("./memories") })
const model = new ChatOpenAI({ model: "gpt-4o" })
async function chat(userMessage: string, sessionId: string) {
const { text } = await mem.resolve(userMessage, { sessionId })
return model.invoke([
new SystemMessage(`${text}\n\nYou are a helpful assistant.`),
new HumanMessage(userMessage)
])
}Adapters
Express
import { memshotMiddleware } from "@ayberkaya/memshot/adapters/express"
app.use(memshotMiddleware(mem, {
getPrompt: (req) => req.body.messages.at(-1)?.content ?? "",
getSessionId: (req) => req.headers["x-session-id"] ?? ""
}))
app.post("/chat", (req, res) => {
const { text } = req.memshot // already resolved
// use text as system prompt prefix
})Next.js route handler
import { withMemshot } from "@ayberkaya/memshot/adapters/next"
export const POST = withMemshot(mem, async (req, { memshot }) => {
const systemPrefix = memshot.text
return Response.json({ ok: true })
})Claude Code hook (UserPromptSubmit)
import { createClaudeHook } from "@ayberkaya/memshot/adapters/claude-hook"
const hook = createClaudeHook(mem)
await hook.run()Register in .claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{ "type": "command", "command": "node ./hooks/memory.js" }
]
}
}Why not X?
This table is honest. "~" means partial or depends on configuration.
| | memshot | mem0 | basic-memory | Zep | |---|---|---|---|---| | Zero runtime deps | ✓ | ✗ | ✗ | ✗ | | Token-budget-aware | ✓ | ✗ | ✗ | ? | | No vector DB | ✓ | ✗ † | ✓ | ✗ | | TypeScript-native | ✓ | ✗ | ✗ | ✗ | | No server needed | ✓ | ~ ‡ | ✓ * | ✗ | | Edge / Serverless | ✓ | ✗ | ✗ | ✓ |
† mem0 OSS uses ChromaDB (in-memory by default, optional persistence). Hosted platform uses Qdrant.
‡ mem0 OSS Memory() runs in-process with in-memory Chroma — no separate server for dev. Production hosted mode requires the mem0 Platform server.
* basic-memory's primary integration is uvx basic-memory mcp which runs a separate MCP server process. Direct Python import works without a server.
? Zep token budget control is not documented; marked unknown rather than ✗.
✗ Zep Cloud (@getzep/zep-cloud) is fetch-based and runs in edge environments; the self-hosted Zep server is a separate Go service.
memshot's actual differentiator: it is the only library in this list that (1) runs in pure JavaScript with zero deps, (2) exposes an explicit token budget with greedy knapsack selection, and (3) ships a tier model that maps naturally to how agent context works. If you need semantic search, graph memory, or a managed service, the others are better tools.
License
MIT — see LICENSE.
