npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@alineo-labs/memory

v0.3.0

Published

A provider-agnostic memory layer for alineo agents: working memory, semantic recall, and episodic recall over the existing ledger — scoped by a durable `resourceId` that survives past any one sandbox session.

Readme

@alineo-labs/memory

A provider-agnostic memory layer for alineo agents: working memory, semantic recall, and episodic recall over the existing ledger — scoped by a durable resourceId that survives past any one sandbox session.

This package owns memory concepts, scoping, and pipeline (injection, compaction, lifecycle hooks). It does not own a storage backend itself — bring your own IWorkingMemoryProvider / ISemanticMemoryProvider, whether that's the in-memory reference implementations shipped here, the file-based @alineo-labs/sqlite-memory backend, or the shared @alineo-labs/postgres-memory backend.

Why resourceId, not sandboxId

A sandboxId identifies one sandbox session. A resourceId is a durable identity (a user, an account, a project) expected to outlive any number of sandbox sessions. Working and semantic memory are keyed by resourceId because their entire point is surviving past the session they were learned in.

resourceId (and parentSandboxId, for forked sandboxes) ride along in the ledger's existing sandbox.created event payload — the same mechanism SandboxDetails.runId already used — so there's no ledger schema change. Set it when creating a sandbox:

const sb = await client.sandbox({
  image: "node:22",
  resources: { cpu: "500m", memory: "512Mi" },
  resourceId: "user-42", // ties this session's memory to a durable resource
});

resume(), restoreSnapshot(), and sb.fork() all inherit the originating session's resourceId automatically, the same way they already inherit runId.

import type { ResourceRef } from "@alineo-labs/memory";

const ref: ResourceRef = { resourceId: "user-42" }; // teamId?: string for shared/team scoping

Three capabilities, three independent slots

Memory is split by capability, not unified into one adapter — real backends support genuinely different capability subsets, so a monolithic interface would force fake implementations of capabilities a backend doesn't have.

Working memory — required

Structured per-resource key/value facts. Every Memory instance needs at least this.

import { Memory, InMemoryWorkingMemoryProvider } from "@alineo-labs/memory";

const memory = new Memory({ workingMemory: new InMemoryWorkingMemoryProvider() });

await memory.workingMemory.set(ref, "preferredLanguage", "TypeScript");
await memory.workingMemory.get(ref, "preferredLanguage"); // "TypeScript"
await memory.workingMemory.list(ref); // { preferredLanguage: "TypeScript" }
await memory.workingMemory.delete(ref, "preferredLanguage");

Semantic memory — optional

Vector recall over remembered facts. Omitting it is a first-class, typed state: calling remember()/recall() on a Memory with no semantic provider throws MemoryCapabilityError.

import {
  Memory,
  InMemoryWorkingMemoryProvider,
  InMemorySemanticMemoryProvider,
} from "@alineo-labs/memory";

const memory = new Memory({
  workingMemory: new InMemoryWorkingMemoryProvider(),
  semantic: new InMemorySemanticMemoryProvider(myEmbeddingProvider),
});

await memory.remember(ref, { content: "prefers dark mode" });
const facts = await memory.recall(ref, "UI preferences", { topK: 5 });

EmbeddingProvider is a minimal { id, embed(texts) } shape — pass any embedding model you like. @alineo-labs/model-providers ships createNvidiaEmbeddingProvider(), matching this shape structurally with no dependency on this package at all.

Verified memory

Every fact returned by recall()/listAll() carries a computed verified flag: true when remember() was called with a sourceRef pointing at a real ledger entry, false for a free-form fact. This is computed, never caller-set — passing verified: true on a free-form fact's input is silently ignored:

await memory.remember(ref, {
  content: "user confirmed the refund",
  sourceRef: { sandboxId: sb.sandboxId, entryIndex: 42 }, // ties it to a real ledger entry
});

const [fact] = await memory.recall(ref, "refund");
fact.verified; // true — traceable back to that ledger entry

A fact worth trusting more than a hallucinated summary is one you can point at real execution history — no other memory framework can do this because it requires an execution ledger to point at in the first place.

Episodic memory — a function, not a provider

A read-shaped view over the sandbox ledger, reshaped by resourceId. No new storage.

import { episodicRecall } from "@alineo-labs/memory";

const entries = await episodicRecall(adapter, ref, { limit: 100 });

// Include ancestor sessions reached via sb.fork()'s parentSandboxId chain:
const withHistory = await episodicRecall(adapter, ref, { branch: "lineage" });

By default, sessions are resolved by matching SandboxDetails.resourceId (or, for ledger data written before that field existed, by matching the ledger's name against resourceId). Supply resolveSessions to use a different convention entirely.

Branch-true episodic memory

episodicRecall({branch: "lineage"}) flattens fork ancestry into one merged stream — good enough for "what led up to this session," but it can't tell you "what happened on a sibling branch forked from the same point." episodicTree() returns the actual fork tree instead:

import { episodicTree } from "@alineo-labs/memory";

const [root] = await episodicTree(adapter, ref);
root.entries; // this session's own ledger entries
root.children; // sessions forked from it — each with its own .entries and .children

Every resolved session's ancestor chain is pulled in automatically (same as lineage), so a tree with a resolved-but-orphaned node never happens — its ancestors are added as parents even if they weren't in the original resolved set.

Real backends

| Package | Working memory | Semantic memory | Notes | | ------------------------------------ | ------------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | @alineo-labs/memory (this package) | InMemoryWorkingMemoryProvider | InMemorySemanticMemoryProvider | Process-local, non-durable — reference implementations only. | | @alineo-labs/sqlite-memory | SQLiteWorkingMemoryProvider | SQLiteSemanticMemoryProvider | File-based via bun:sqlite, zero external services, survives restarts. Ranks recall with sqlite-vec's native vec0 index when the extension loads (verified on win32/x64); falls back to an in-JS cosine scan otherwise — check .hasVectorIndex. | | @alineo-labs/postgres-memory | PostgresWorkingMemoryProvider | PostgresSemanticMemoryProvider | Shared, multi-process backend. Row-level security isolates teamId-scoped rows. Ranks recall with a pgvector HNSW index when the extension can be installed; falls back to an in-JS cosine scan otherwise — check .hasVectorIndex. |

import { Memory } from "@alineo-labs/memory";
import {
  SQLiteWorkingMemoryProvider,
  SQLiteSemanticMemoryProvider,
} from "@alineo-labs/sqlite-memory";
import { createNvidiaEmbeddingProvider } from "@alineo-labs/model-providers";

const memory = new Memory({
  workingMemory: new SQLiteWorkingMemoryProvider("./alineo-memory.db"),
  semantic: new SQLiteSemanticMemoryProvider("./alineo-memory.db", createNvidiaEmbeddingProvider()),
});

Neither backend package has been run against a live database in this repo's own test suite (no Postgres instance, and the SQLite one is unit-tested with bun:sqlite directly) — they ship type-checked and, for SQLite, tested against a real file; treat the Postgres one as reviewed-but-unverified until it's run against an actual server.

Compaction

A remember()'d fact stays verbatim forever unless pruned. compactSemanticMemory() (or Memory.compactSemanticMemory()) drops old/excess facts, for any provider implementing the optional IPrunableSemanticMemoryProvider capability (listAll/forget — all three semantic providers above support it):

await memory.compactSemanticMemory(ref, { maxFacts: 500, maxAgeMs: 30 * 24 * 60 * 60 * 1000 });

Age-based removal runs first; the count cap is then applied to whatever's left. Throws if the configured provider doesn't support pruning.

Pass summarize to consolidate instead of just deleting — it receives the facts about to be removed (oldest first) and returns replacement contents, written before the originals are dropped:

await memory.compactSemanticMemory(ref, {
  maxFacts: 500,
  summarize: async (facts) => [
    await myLlmCall(`Summarize into one fact: ${facts.map((f) => f.content).join("; ")}`),
  ],
});

summarize is caller-supplied — this package never depends on a concrete model, the same principle as EmbeddingProvider.

Automatic compaction

Configure autoCompact on Memory to run a compaction check after every remember(), instead of remembering to call compactSemanticMemory() yourself on a schedule:

const memory = new Memory({
  workingMemory: new InMemoryWorkingMemoryProvider(),
  semantic: new InMemorySemanticMemoryProvider(embeddings),
  autoCompact: { maxFacts: 500, checkEvery: 10 }, // check once per 10 remember() calls
});

Silently skipped if the configured provider doesn't support pruning; a failed compaction check fails the remember() call it rode along with (not silently swallowed) — raise checkEvery, or call compactSemanticMemory() on a separate schedule instead, to decouple the two.

Context injection

buildContextSnippet(memory, ref, opts) assembles a plain-text block from a resource's working memory and (if a query is given and a semantic provider is configured) its most relevant semantic memories — ready to prepend to a prompt:

import { buildContextSnippet } from "@alineo-labs/memory";

const context = await buildContextSnippet(memory, ref, { query: userMessage, topK: 5 });
for await (const chunk of agent.prompt(context ? `${context}\n\n${userMessage}` : userMessage)) {
  process.stdout.write(chunk);
}

This builds the string only — alineo's Pi bridge has no hook today for prepending to a session's system prompt automatically, so injecting it into an actual conversation is still a call the surrounding app makes.

Structured working memory

IWorkingMemoryProvider is raw, untyped key/value. SchemaWorkingMemory<T> wraps it with a validated, typed profile stored under one key — useful for the common "agent maintains a structured user profile" shape:

import { z } from "zod";
import { SchemaWorkingMemory } from "@alineo-labs/memory";

const ProfileSchema = z.object({
  name: z.string().optional(),
  preferredLanguage: z.string().optional(),
});

const profile = new SchemaWorkingMemory(workingMemoryProvider, ProfileSchema);
await profile.update(ref, { preferredLanguage: "TypeScript" }); // merges + validates
await profile.get(ref); // { preferredLanguage: "TypeScript" }

SchemaValidator<T> is a minimal { parse(data): T } shape — any Zod schema satisfies it directly; this package never depends on a concrete schema library.

Agent-callable memory tools

createMemoryTools(memory, ref) returns a set of tool definitions (name, description, JSON Schema parameters, executor) in the shape most agent tool-calling conventions expect — so a model, not just the surrounding application code, can decide to persist or retrieve a fact mid-conversation:

import { createMemoryTools } from "@alineo-labs/memory";

const tools = createMemoryTools(memory, ref);
// [{ name: "set_working_memory", ... }, { name: "get_working_memory", ... },
//   { name: "remember_fact", ... }, { name: "recall_facts", ... } — last two only if
//   memory.hasSemanticMemory ]

alineo's Pi bridge doesn't yet expose a way to register caller-defined tools into a running Pi session, so wiring these into an actual live agent conversation is left to the caller today — this ships the tool definitions, ready to adapt into whatever surface ends up supporting them.

Forkable memory

Sandboxes already fork copy-on-write via sb.fork(); Memory.fork() gives memory the same property — an independent snapshot copy the child can mutate without ever touching the parent's:

const {
  ref: childRef,
  workingKeysCopied,
  semanticFactsCopied,
} = await memory.fork(parentRef, "child-resource-id");

await memory.workingMemory.set(childRef, "note", "only visible to the child now");
await memory.workingMemory.get(parentRef, "note"); // undefined — the parent is untouched

Working memory is always copied in full. Semantic memory is copied only if the configured provider supports the pruning capability (listAll is what makes enumerating "everything to copy" possible) — semanticFactsCopied is 0, not an error, otherwise.

Alineo.spawn() calls this automatically when the parent agent has .memory configured — a spawned child (a sandbox-level fork under the hood) gets its own independent memory copy with no extra wiring.

Team access control

ResourceRef.teamId isolates data structurally in every backend (it's part of the storage key), but only @alineo-labs/postgres-memory enforces it as an actual access-control boundary via row-level security — a caller for the in-memory or SQLite backends who deliberately passes the "wrong" teamId can still read it. withTeamAccessControl() / withTeamAccessControlSemantic() close that gap for any backend:

import { withTeamAccessControl, withTeamAccessControlSemantic } from "@alineo-labs/memory";

const checker = { canAccess: (teamId: string) => currentUser.teamIds.includes(teamId) };

const memory = new Memory({
  workingMemory: withTeamAccessControl(new SQLiteWorkingMemoryProvider("./mem.db"), checker),
  semantic: withTeamAccessControlSemantic(mySemanticProvider, checker),
});

Every call on a ResourceRef carrying a teamId is checked against checker.canAccess() first, throwing MemoryAccessDeniedError before the wrapped provider is ever touched. A ref with no teamId always passes through untouched. Works alongside Postgres's own RLS for defense-in-depth — it isn't an either/or.

Sandbox lifecycle binding

createMemoryLifecycleHooks(memory, ref) returns a SandboxHooks object (composable via @alineo-labs/core's composeHooks()) that records the most recently active sandboxId and the most recent checkpoint's metadata into working memory — a durable answer to "what was the last checkpoint for this resource" without re-deriving it from the ledger every time. It does not restore sandbox state itself; that's @alineo-labs/core's job.

import { composeHooks } from "@alineo-labs/core";
import { createMemoryLifecycleHooks } from "@alineo-labs/memory";

const sb = await client.sandbox({
  image: "node:22",
  resources: { cpu: "500m", memory: "512Mi" },
  resourceId: ref.resourceId,
  hooks: composeHooks([createMemoryLifecycleHooks(memory, ref)]),
});

Agent wiring

alineo's Alineo class accepts an optional memory on load()/resume()/attach(), and spawn() carries it over to the child automatically:

const agent = await Alineo.start(spec, { adapter, memory });
await agent.memory?.remember(agent.resourceRef, { content: "user prefers concise answers" });

agent.resourceRef defaults resourceId to the agent's own name — the same convention episodicRecall()'s default resolver expects, so no extra wiring is needed to make episodic recall work for an agent's own sandbox sessions.

Alineo.spawn() also calls memory.fork() automatically (see "Forkable memory" above) when the parent has .memory configured — a spawned child gets its own independent memory copy, seeded from the parent, with no extra call needed.

What's intentionally out of scope here

  • Pi tool-call integration — createMemoryTools() produces framework-agnostic tool definitions; actually registering them into a running Pi session needs a change to alineo's Pi bridge that hasn't been made.
  • Automatic prompt injection — buildContextSnippet() builds the string; nothing calls it automatically at session start. That's still an explicit call the surrounding app makes.
  • A ledger-native branch/lane concept — episodicTree() reconstructs the fork tree from parentSandboxId at read time; alineo's ledger itself still has no first-class branch concept the way e.g. Pi's own session storage does. If the ledger ever grows one, episodicTree() would become a thinner read over it instead of doing the reconstruction itself.
  • withTeamAccessControl() is app-layer, not a security boundary of its own — it's a correct-by-construction gate in front of whatever backend, but the TeamAccessChecker you supply is doing all the real work; a buggy or bypassed checker is still a hole. It doesn't replace real infrastructure-level isolation for a genuinely adversarial multi-tenant setup.
  • Restoring sandbox filesystem/process state from a checkpoint — createMemoryLifecycleHooks only records checkpoint metadata into working memory; the actual restore is @alineo-labs/core's resume().

Examples & cookbook

  • examples/memory-basics — every capability on this page, demonstrated standalone. No OpenSandbox, no API key.
  • cookbooks/persistent-agent-memory — the real end-to-end scenario: a Pi agent whose memory survives across separate sandbox sessions entirely, using @alineo-labs/sqlite-memory.