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.