Developing

lnk-link-memory is Link Memory, the memory engine a Link agent uses by default: an MCP server over stdio that searches, reads and lists the agent's files and past conversations, keyless and local. How to use it, and why it works the way it does: Link Memory.

What it reads and writes

It speaks Link's memory engine contract (link_memory_contract), and any harness reaches it as a tool. It reads only its folder, writes only the agent's notes there when Link names them (--notes), and never follows a link out of it.

Build and test

From tools/, in the public workspace beside the core (LINK_CORE for a core in another place):

cargo build -p link-plugin-link-memory
# the sandboxed tests skip without bubblewrap, unless LNK_REQUIRE_SANDBOX=1
cargo test -p link-plugin-link-memory
# one file: the tests are one program, each file a module, tests/main.rs
cargo test -p link-plugin-link-memory --test memory index::

Its tests

memory/tests/agent.rs drives it through Link's own lnk agent memory start, memory/tests/reads_conversations.rs reads a conversation folder as Link Harness keeps one, and memory/tests/notes.rs writes the notes.

PDF and Word text

PDF and Word text comes from Link documents (lnk-link-documents), a plugin of its own that memory runs as a child, with no secrets and with its own resource limits; memory's build compiles no document parser (why). Its command prints programs: ["link-documents"], so its sandbox reads that program where it's installed.

Many agents, one process

lnk-link-memory serve-many serves every agent of one owner on the machine, a session per connection, each answered from its own agent's index alone.

Opening each index

Each agent's index is opened at its first session, under a lock of its own, so one agent slow to open holds up no other, and two sessions arriving at once open it once.

Its embeddings

A search's words are embedded before the agent's index is reached, so a model slow to answer holds up no other session of the agent. A model already served for it is used from the first search; one memory starts itself is found in the background, and searches are by words until then.

Kept off the folder

Where an agent's machine keeps what memory reads off its folder (its conversations, its notes or its memory, in SQLite, a bucket or a box's store, as machine.toml's [storage] says), or its Home in a bucket on a machine that runs agents only while they answer, the session's first line binds the agent (link_memory_contract's Hello with Bound): its owner, its key for this start, and its bridges. The service then serves it by owner and ID, as no other owner's agent of the same ID.

Binding an agent

On a computer, lnk agent memory connect binds it so, with the key this machine's keyring holds, and lnk agent memory service runs the service with --bridges <its folder>/bridges.

On a machine that runs agents only while they answer

Its service binds each agent as it wakes, running lnk-link-memory serve-many <socket> --cache <folder> --stay --peer <its user> --group <a group of both> --files-bridge <socket> --storage-bridge <socket>, its socket shared with --group (mode 660) in a folder no one else enters.

Tree

What memory indexes is behind one seam (memory/src/tree.rs's Tree): a folder on the machine (Folder), with its conversations through the storage bridge where they're kept elsewhere, or Home through the files bridge and conversations through the storage bridge (Bridged), each call proved as the agent.

Listing a bridged tree

A bridged tree is listed when the agent is bound, and two seconds after the files bridge says something under memory/ changed (Control::Changed), never on every call. A skill's file, never indexed, is read by its path from either (Tree::read_unindexed), when read finds nothing indexed there.

Notes kept elsewhere

remember writes its notes through the storage bridge where its instructions are kept elsewhere (notes.rs's Notes::stored), each a write that names the version it read.

A team's memory, bound

A session whose Hello names a team besides its binding is the agent's session of that team's memory (service/bridges.rs), by the team's ID: the team's folder's memory/ through the files bridge as the agent (InTeam), its index's copy beside the folder, cached at <folder>/<owner>/<agent ID>/teams/<team ID>/. The bridge serves the team's index only to a member granted all of the folder, so a member granted part of it is refused at bind; only one writing all of it (writes) writes the index back.

A member that keeps no index

A session without writes embeds nothing, whoever would pay for it: it searches the copy beside the folder, and what changed there since, by their words alone, never by meaning, since a search's meaning is a call to the embedding model too.

Ended with its agent

It writes no notes and reads no conversations. The agent's Flush writes its teams' indexes back with its own, and its End ends them; a Changed naming the team lists that folder again.

The index as a cache

The index is then a cache, mode 0600 (<folder>/<owner>/<agent ID>/ for an agent the service isn't named, else its own index file), of a copy (memory/src/cache.rs): kept with the agent's memory collection where that's elsewhere (memory/link-memory/, through the storage bridge), else beside Home where Home is in a bucket.

Its segments

The file is cut into 8 MiB segments named by their SHA-256, and a manifest naming them, written last and only if it's still the version the cache was read from.

Fetching the copy

A cache with no copy fetches it, each segment checked against its hash. The copy is the owner's to change, so it's then checked whole (SQLite's quick_check) and for memory's own schema alone (no table, index, trigger or view of its own), and one that fails, or misses a segment, is made again and written back. Every index is opened in SQLite's defensive mode with its schema untrusted.

The machine's own bridges

The bridges a binding names must be the machine's own (--files-bridge, --storage-bridge, or the agent's own <folder>/<agent>/storage.sock in --bridges <folder>), else it's refused before its key goes anywhere. A binding is served, even from a cache already there, only once one of them takes its key, and a cache is used only while it's of the copy there now.

Writing it back

A changed index is written back at most once a minute, and at Control::Flush, which the machine sends before the agent sleeps, after embedding what's waiting for up to 20 seconds. A write back refused for its version drops the cache, fetches the copy again and indexes what it lacks.

Control::End

Control::End drops the agent's key, and a session still open answers nothing more; its cache stays, for the machine to remove. Every control names the agent by owner and ID.

An embedding server for many owners

It's given as --embed-url unix:<path>, an OpenAI-style /v1/embeddings on a Unix socket, each bound request carrying Authorization: Link-Binding <agent>:<proof>. A bound endpoint must be Unix: no binding credential is sent over HTTP.

The proof

The proof is HMAC-SHA256 of memory embedding binding and the binding's identity, made with its existing key (link_memory_contract::embedding). A shared engine proves each agent separately. The proof lasts only for that binding and is held as a redacted secret.

The owner

The server resolves the owner from its Unix peer and the current binding; memory sends no owner header.

memory/tests/home.rs

It runs all of the bridged memory against a stand-in files bridge over an encrypted folder, and against a storage bridge over lnk-sqlite.