Decisions

Why Link Harness works the way it does. Link's own decisions about harnesses are on its Agents Decisions page.

Design

What Link Harness is called, how it's judged, and the seams it's built on, which let any process on any machine take an agent's next turn.

Link Harness, the plain word, no brand. You type lnk agent use --harness link; its program is lnk-link-harness and its plugin link-harness. KNOWN (harness settings) maps the name you type to the plugin, and an adapter from outside Link stays lnk-<name>, found by its own name.

By what each agent costs, idle and per message, as Link measures it (lnk measure). So it gets lighter one bottleneck at a time, the largest first.

For the densest deployment, run at the safest: the agent is always an argument of a turn, and each request names it; settings are read at each turn; every path comes from the agent through one function; and one turn runs at a time per conversation, a lock in the process. Each has one implementation now, one agent per process.

No. Link Harness runs on Link's own services alone: the model proxy, the sandbox, the shared bridge that carries connections, the store and the tools. The hub is only for an agent that runs on, or moves between, another harness, so an agent that is Link's code from end to end pays nothing for it. Link Harness still answers ASP's state methods, so another harness can take an agent from it and give it back.

Nothing that matters when its process dies. It's a small router: each turn reads what it needs through its store, and writes there. The events are the truth, each number written once. Everything else (where the window starts, the copy for people, its settings as last read, each tool's list of calls) is derived, and made again by a process without it. So any process on any machine can take the next turn.

Agents

How one process of Link Harness answers for many agents, or starts ahead for one it doesn't know yet.

Yes, of one owner, where the machine says so (serve --agents <file>, the contract's shared). Each agent is answered on its own port, as itself, from its own home, files and tools, the ones it would have had alone; a request naming another agent needs that agent's key.

What it costs

Since it keeps nothing that matters in memory, one process for many costs only what each turn reads, and a restart (an agent joining or leaving) is finished like any other. Stopping for one, it takes no new connection, and answers on each it has until its client closes it: a bridge opens one for each message, so a message asked just before the stop is answered, never cut off. Link runs it so only where machine.toml says shared_harness, since the agents' folders then share a sandbox.

An agent asleep

An agent stopped for being idle stays one it serves, so its sleeping and waking restart nothing, and with every agent asleep, it ends.

So a machine that runs an agent only while it answers needn't count a start in the time to answer. It keeps units started ahead, each holding nothing of anyone's: no settings, no key, no port (serve --blank <control>, the contract's blank).

Bound once

Bound to an agent by one line on its control socket, a unit answers as that agent at once, with a key made for that binding alone, the settings it was bound with kept in memory, and its conversations through the store bridge it was given. It's bound once and never reused: there's no unbinding, so nothing of one agent's can be left for the next, and the process ends when its agent sleeps.

What it keeps and reads

What goes through its store, where its prompt's sections come from, and what becomes of an answer nobody took.

What becomes of an answer nobody was there to take?

It's kept for the bridge, as a crashed turn's answer is. A turn runs to its end even when the caller goes, since its calls were made and its answer is in the conversation either way. A caller goes when Link's bridge stops mid-answer, and where one bridge carries every agent, one stop cuts every agent's turn at once.

Handed over once

So an answer whose caller was gone before it was told it is handed over at _link/recover with the crashed turns', and the bridge, asking every half minute while the harness runs, sends it to the chat once. The message is never asked again, so no call is made twice.

Everything it reads and writes: its settings (config.json, written by configure through the store too), its instructions, its conversations and where each one's window starts. One seam per resource, keyed by agent: the store has one implementation, the agent's folders on disk, which keeps each conversation's events one of three ways (below), and the code is the same on every machine.

The workspace and memory

The workspace, the folder an agent's tools work in, is the agent's files folder, named by the same one function. Link Memory reads a log's copy on disk itself, and makes its own of a conversation kept any other way, reading its events as Link harness does: the compact frames, or the driver.

From sections, each with an owner and a most, composed in the order of how often each changes, least first: Link's base, yours for the model it runs on, what its tools say of themselves, the environment (its files folder and connections, at its start), the repository's, its skills, who it is and who you are, whatever the task (instructions/persona.md and owner.md), your standing instructions (instructions/instruction.md, read at each turn, a file like any other), its notes, and now.

Why that order

A provider caches a prompt up to its first change, so what changes least goes first, and the prompt stays the same each turn: the date, the time, the connection and who's speaking change every message, so they head each message instead, kept with it so it reads the same at each later turn.

The time and who's speaking

The time is the machine's local time, where a personal agent's owner is; the scheduler's zone is its own key. Who's speaking is shown only for the owner, since a message from others already says who, quoted.

Each one's source and size

Each one's source and size are opinions, the spec's [instructions]; every section can be replaced, the base included. Everything, a file of yours too, is read through the store, as conversations are, never through a link.

The repository's

The repository's are AGENTS.md, the standard most coding agents read, else CLAUDE.md for a repository written for Claude Code: one per folder, so a CLAUDE.md that only points at AGENTS.md isn't read twice.

Where are a model's own instructions?

In the agent's files, instructions/models/<maker>/<model>/instruction.md, beside the standing instructions, never in its spec. Instructions are long and formatted, so they're markdown files you edit, and a spec only names a file or link or none, never holds the words.

How a model is named

A model is named as OpenRouter names it, its maker then its name (anthropic/claude-opus-5-5, openai/gpt-6-luna), since it names every hosted model and models through it already are; one on a runtime here has no maker Link can know, so it goes under the runtime's name (ollama/qwen3:8b).

Two names for one model

The same model reached two ways can have two names (Anthropic's claude-opus-5-5 and OpenRouter's anthropic/claude-opus-5.5): each is its provider's, never guessed between.

Why it comes second

The section comes second, after the base: it changes only when the model does, and a model changed is a new cache anyway. A fallback model is sent its own file in the first's place, the rest of the prompt the same.

Where does the tools section come from?

From the tools: MCP's initialize answer has a server's own instructions, how its tools work together, which Link's tools write (the scheduler's subagents and teammates, memory's notes) and any MCP server may. The text comes from the tools you added, as their descriptions already did.

How it's listed

Link Harness keeps them with each tool's listing, at most 2 KB each, and lists them under each tool's name, sorted, for the tools the turn offers: a conversation narrowed to no tool of one has none of its words. Kept beside the listing, a tool that fails to start in a new process is still described as it was, so the prompt, and the cache, hold.

From its memory: remember, a tool of Link Memory's, writes memory/notes.md, a line a note, and the composer reads the file into the seventh section at each turn. A tool rather than code in the harness, as every tool is, so any harness with Link Memory takes notes the same way.

Late and small

The section comes late, so a new note changes the prompt from there on and the provider's cache holds up to it. The notes are held small, 4 KB by Link's spec, since they're in every prompt: past it, remember refuses and says to change or forget a note first, as Hermes Agent holds its memory file to a limit for the same reason.

An empty file

The section is the file under "# Your notes", and none when the file is empty, so an empty file costs no tokens.

The prompt lists each skill's name and description. The model reads its instructions through Link Memory's read when needed, following the Agent Skills format. Skill metadata is read through the store at each turn and sorted by name, keeping the prompt stable. Instructions are cached until the skill file changes. A skill missing its matching name or description is omitted, and prompt limits remove whole skills rather than fragments.

Conversations

How a conversation is kept, how Link Harness tells which way, and why it stays in files by default.

A conversation is an append-only event log, selected by the agent spec's [storage] conversations: log, compact or a store driver. A log is events.jsonl, with one JSON event per line. A lock serializes writers; an existing sequence number is refused, and an incomplete final line is removed before appending.

What its events retain

Events retain text, time, model, provider, response ID, tokens, latency and provider-reported cost. Thinking accompanies model calls but is not shown. A Markdown copy supports people and memory (what it holds). Storage is keyed by agent and conversation so backups, moves and harness switches read the same record.

Why is how a conversation is kept the spec's to say?

Because people want different things of it. One opens the folder and reads it; another wants it as small as it can be, and never reads it by hand; a third keeps it in a database, off the machine. So it's an opinion, a key of the spec ([storage] conversations), read by Link harness from its settings as every opinion is; a harness from outside Link keeps its own however it does, so the key is Link harness's, and the spec says so. The rest of what Link harness keeps would be more keys under [storage].

From its folder: an empty store.<driver> says a driver keeps it, events.zst that it's compact, and anything else is the log, a file per event of before it included. The spec says only how a new one starts, so changing it moves nothing: each conversation is read where it is.

Moving the old ones

lnk agent chat convert, by hand and with the agent stopped, moves the old ones: each is written the new way in a folder beside it, put in its place in one rename, what else it held (its tools, an answer still to send) moved along, and the old way's rows forgotten. Never on its own: a key changed by mistake shouldn't rewrite every conversation.

What is a compact conversation?

The same events as the log, a line of JSON each, in zstd frames appended to events.zst, so a write never rewrites what's there; the events since the last frame are lines of events.jsonl, and every 32 of them (or 1 MB) become the next frame, compressed hard (level 19), since a frame is written once per many events.

Its lock and its frames

The tail's lock is the conversation's: a reader holds it shared, so it never sees a frame written and its events still in the tail. A frame is whole on its own, so one cut short by a writer that stopped isn't read, and the next fold cuts it off; its events are still in the tail, emptied only once the frame is on the disk, and an event found in both is read and framed once.

Its first frame

The first frame is one zstd skips, naming the thread, which its folder's name doesn't say whole (telegram-42 for telegram:42).

What compression costs

Compression reduces disk use at the cost of work during writes and reads. A compact conversation has no frame index, so each turn reads the whole conversation, including when it only needs the latest window checkpoint. Use a store driver when efficient access to a long history matters.

Why no copy for people beside a compact conversation?

A copy grows as the conversation does, and as plain text, so it would take most of what compact saves. lnk agent chat show <thread> prints it from the events instead, and Link Memory makes its own.

What is a store driver?

A plugin, lnk-<name>, that keeps conversations for Link harness: it writes an event only if its number is free and the next, reads from a number, and keeps the lease that says a turn runs. That's all a conversation needs, so any process on any machine sharing the driver's store can take an agent's next turn.

SQLite

Link's first is SQLite (lnk-sqlite), local: every conversation of the agent in one file, chats/events.sqlite, so it moves with the agent; each write is a transaction that takes the file's lock first.

Google Cloud Storage

Off the machine, Google Cloud Storage's (lnk-gcs): an object per event, written only if it isn't there (ifGenerationMatch=0), and a lease replaced only if it's still the one read, which is all an append-if-free and a lease need, with no database to run. A write sent again after its answer was lost, that finds its own bytes there, wrote them.

Behind the store bridge

The driver runs outside Link harness's sandbox, behind the agent's store bridge, which Link runs beside the agent: Link harness asks it for each call, proved with the agent's key, so whatever the driver needs to reach its store never enters the sandbox, and Link harness neither knows nor cares which driver it is.

Another driver, and what the folder keeps

A conversation kept by another driver than the spec's runs that driver in Link harness's sandbox, as memory does. A call is stopped past 30 seconds. A conversation's folder keeps only its marker, and what isn't an event: the tools it was narrowed to, a turn asked to end, an answer that finished a crashed turn.

An agent runs in one place at a time, and a move carries its conversations with its files folder, so on your own machines nothing needs a store two machines share. A bucket syncs files, which made a file per event look right; a store off the machine is a database driver instead, which takes appends. Backups already put the files in your buckets.

Why not a bucket of files

Keeping them in the bucket instead, for your Mac and a box to answer in turn, would take each cloud's own API (rclone can't refuse a write whose name is taken, so it can't keep one turn at a time), a bucket tool outside the sandbox, and a copy here for memory.

The copy, groups and forks

What the copy for people holds and why it only grows, how a group is a conversation, and why a fork copies events.

What does a conversation's copy hold?

What was said, up to the agent's last answer: the owner's messages and the agent's answers, never a tool's call or its answer, and never the owner's message while it's being answered. Memory searches the copy, so a search would otherwise find the question it's searching for, and the next search what the last one returned, an echo that grows with each call. The events keep everything, calls and answers included.

Why is the copy added to rather than made again?

The copy holds what was said up to the agent's last answer, so it only grows, and only when the agent answers. Each answer adds what was said since the last one to its end, from the messages the turn already holds, rather than reading every message and writing the whole copy again. When the copy doesn't end with the last answer (it was cut short, or written elsewhere), it's made again from every message.

Each group is a conversation of its own, chats/<connection>-<group id>. Link's bridge takes your mention of the agent out of the message before it goes on, and a mention with nothing else is ignored. Each message is sent to Link Harness alone, and Link Harness adds the conversation before it.

Why do forks copy events rather than visible messages?

A visible transcript omits tool calls and results, provider thinking and signatures, summaries and window marks. Rebuilding a conversation from that transcript changes the model's prompt and loses its cacheable prefix. A fork copies the complete immutable prefix before the edited owner message, preserving timestamps and the source's tool restrictions. Only the new suffix changes. This keeps each provider's normal cache behavior without changing its cache keys or retention settings.

The window

How much of a conversation the model sees, how a long one keeps going, and where a model's window comes from.

How much of a conversation does the model see?

A window: the conversation sent the same way each turn, so the provider's cache holds, as much of it as the model takes. Past that, the oldest turns leave half the window at a time, at points fixed by the conversation itself, so the prompt's start stays the same for many turns between drops. What leaves is summarized, and the window starts with the summary.

How does a long conversation keep going?

By compacting it. Once the conversation fills the share of its room the spec says ([window] compact_at, three quarters in Link's), the window moves on at its next drop point, as it always does, and what leaves is summarized.

The answer first

The turn answers first, with the window it last sent: the room compact_at leaves holds what came since, so the provider's cache holds and the answer comes as soon as any. The answer goes to the chat, and then, still under the turn's lease, the model is asked for the summary as a continuation of that same prompt: the window, its answer, and the request after them, which quotes the start of the first message that stays. So the provider's cache holds for all of it, and the summary costs little more than what it writes.

The summary

The summary is an event of the conversation, written once and never changed, and the window starts with it until the next drop point, when the next summary covers it and what left after it. Nothing is lost that it didn't choose to leave out, and the prompt's start changes only at a drop point. The summary never reaches the chat or the copy for people, which hold the messages it stands for.

A window past its room

A window that has outgrown even that room (a long round of calls) is summarized first, from what leaves alone, at most the walk's budget of it, newest first, so the request never outgrows the window; then it's answered.

The words asking for it

The words asking for a summary are Link Harness's, not a key: they're the mechanism's, as a tool's description is, and what to keep is the model's.

Saving notes first

Where the agent can write notes (memory__remember), the model saves what matters to them first ([window] save_notes), in the same continuation: a summary is for this conversation, and notes are for every conversation after. So it saves only from a conversation its owner spoke in: notes are about its owner, and one of others' messages alone (a teammate's, a schedule's) could otherwise write them.

While it saves

While it saves, it may call nothing else, and with nothing to save, nothing at all; its tools are still offered, as in every round, so the cache holds for them too. The calls it made are kept with the summary.

A note changes the prompt

A note it saves changes the instructions from there on: a turn that summarized before answering composes them again first, and every turn composes them at its start, so the prompt after a save misses the cache from the notes on, as after any note.

When it compacts

When and whether it compacts are the spec's, since each summary is a call that costs: compact_at = "never" lets the oldest turns leave unsummarized. A summary that fails, or is cut short, leaves them unsummarized too, and the next turn tries again.

A message while it summarizes

A message that comes while it summarizes, or until its turn has let its lease go, waits for a turn of its own, since the answer it would have joined is given; and the turn that gave its answer answers nothing after it, its caller gone. A message written by another process sharing the store can't see that, joins as it would any running turn, and is answered in a turn after, in the conversation but not in either reply.

A crash after answering

The answer is kept and sent before the summary is asked, so a turn that fails or crashes after answering loses only the summary: the window's mark stays where it was until a summary is written, so the next turn answers with the same window and summarizes after it. A note it saved before the crash may be saved twice.

Where does a model's window come from?

From what's known of the model, which is data, not code: Link's file of models' facts, refreshed at every release, under yours (~/.lnk/model-facts.toml), which wins field by field, so a model newer than your Link works without a release (lnk model show).

In what order

Link Harness takes the window from your file first, then from what a runtime on the machine says it runs the model with (it knows better than any file), then Link's, and gives a model nobody described the spec's small window ([window] unknown, 32,768 tokens) rather than a guess that overflows.

Ollama's window

Ollama runs a model with a smaller window than it was trained for unless told, so a trained window Ollama reports counts only up to that small one, unless you say otherwise.

How much of it to send

How much of it to send is the spec's. Link's [window] max is held to the model's window; one you or a team set is taken as set, whatever Link knows of the model: the window you set always wins, and the only limit is the provider refusing a prompt as too long, which the harness reports as that, saying the window it sent and where to change it.

The refusal

No provider has a common code for the refusal, so the harness tells it by the message: Anthropic's "prompt is too long", OpenAI's and OpenRouter's "context length", a runtime's "context window".

The conversation's share

The conversation's share is the window less the answer's ([model] answer, else [window] answer's share, at most the model's longest), the instructions and tools, and the margin ([window] margin).

Counting tokens

Tokens are counted roughly before a turn, by characters (four ASCII characters to a token, one for any other, so Chinese or an emoji isn't undercounted), since each provider's tokenizer is its own; the margin covers the difference. After a turn, the provider's own counts are what's kept, measured and priced. The window's drop points stay on the rough count, which never changes for a message once written, so the provider's cache holds.

How does a turn find the window without reading every message?

Each answer records the window's checkpoint: the starting message, its time and the tokens before it. A turn reads the latest checkpoint from the last 32 messages and resumes counting from there, or from an earlier answer or summary when needed. The checkpoint stays with the events that determine it.

Reading the log

Log reads search backwards by sequence number and load only the required lines. A summary counts as one message, while the complete conversation remains available for reconstruction.

Tools

How Link Harness reaches its tools, keeps their calls, and holds their sessions between turns.

As an MCP client of the servers Link's tool bridge already runs outside the sandbox ($LNK_TOOL_<NAME>, memory at $LNK_MEMORY, the scheduler at $LNK_SCHEDULER, and run, file and code at $LNK_RUN, $LNK_FILE and $LNK_CODE), never as code of its own. A tool written once works for every harness, the calls you allow and the ones that ask are the same for each, and Link Harness still runs no code.

Their names

Memory is the first. Each tool is offered to the model as <tool>__<call>, so two tools can have calls of the same name, and memory's are memory__search, memory__read, memory__list and memory__links. run__command runs a program, and file__read, file__write, file__replace and file__find work on files, each call in a sandbox of its own. The tools are listed the same way each turn, so the provider's cache holds.

A command in the background

run__start, run__list, run__read and run__stop run a command in the background, which outlives the turn and the tool's session that started it. Link Harness keeps nothing of it: the command's end comes back as a message from run:<id>, and Link Harness says who's speaking, as for a schedule.

A team's tools

A team's tools come the same way: its memory over the team's folder is $LNK_TOOL_<TEAM>_MEMORY, offered as <team>_memory__search and the rest, one server for every member.

A subagent's tools

A subagent's task can come with a shorter list of tools (Link-Tools), which Link Harness keeps in tools.json beside the conversation's events, or with the conversation in its store when its machine keeps conversations off the machine (tools_narrow, narrowed there in one step): never on a disk that a sleep empties, where losing it would give a woken subagent every tool. A later list narrows it again and never widens it, and a call to a tool the subagent wasn't offered is answered as one there isn't.

How are a tool's calls kept?

As events of the conversation, like any message. The model's answer that makes calls holds them, and each tool's answer is an event of its own after it, written as it happens. A turn cut short leaves the conversation as far as it got. A call left without an answer is answered in the next prompt as unanswered, so the prompt stays valid. With the tools gone, the calls and answers go to the model as text.

Does a turn hold anything of its tools?

Only the sessions of memory, run, file and code, and of the task and file namespaces over them, kept between turns: code's holds its language servers, which index a project once. A turn opens a session with a tool only when the model calls it, so a message that calls no tool starts none. Every other tool's session ends with its turn.

Memory's session

Memory's is kept for the turns after, and ended once none has used it for 10 minutes, or an hour after it started, so what it started without (its model, not up yet) is tried again: ending it with each turn started its process again with every message that called it (its sandbox, its model server and its index), and its embedding ran only inside turns.

run's and file's

run's and file's are kept as long, so a turn that runs a command or reads a file doesn't start a server and list its tools first. So are the task and file namespaces, which reach them, and the scheduler, through Link's namespace server: ended with each turn, every warm turn started each one again, its server's and the namespace's sandboxes with it (a confined warm turn took about twice as long). Kept, they hold their processes until idle: about a dozen, under 20 MB between them, when confined.

One process for every conversation

A kept session is one process for all of an agent's conversations at once, and a tool can keep what a conversation did in it, such as a browser's pages or a shell's folder: memory, run, file, the scheduler and the namespace server are the agent's own in every conversation and keep nothing of one in their session (each call says its conversation, and the namespace server passes that on), but another tool's state from one person's turn mustn't reach another's. The tool and agent namespaces still end with each turn.

Derived from the tools

Each tool's list of calls is kept in the process between turns, and asked again whenever a session starts. Both are derived from the tools: a process that hasn't listed them yet lists all of them at once when its first turn starts, and starts its own sessions. The conversation's events hold everything else the next turn needs, so any process on any machine can take it.

Starting a tool

A tool has 30 seconds to start and list its calls. One that takes longer is given up on and its connection closed, so the tool bridge that starts it can end the start rather than hold a session for nobody; a session it started is ended too. A tool that fails to start after it was listed is still offered, and a call to it is answered as unavailable.

Their order

A server's tools are offered in order of name, whatever order it lists them in. So the tools, and the prompt they start, stay the same, and the provider's cache holds.

tool-lists.json

The lists are kept beside its settings too (tool-lists.json in its home), so a new process does the same with a tool that fails to list its calls when it starts, rather than leaving it out: a restart with one tool down doesn't change the prompt, then change it back.

What the file holds

The file is derived: it's read once a process, written only when a list changes, holds only the agent's tools now (a tool taken away, or moved, isn't offered from it), and a process without it lists the tools as before. It holds each tool's name, a hash of its address (which carries the proxy's secret), and its calls, and each agent a process serves has its own. A tool that was never listed and doesn't answer is left out of that turn only.

How many calls does a turn make?

At most eight rounds. The model is then asked once more and told to call nothing, so a model stuck in a loop still answers. Each answer is kept up to 32 KiB, so one answer can't fill the window. The tools one round calls run at once. A message that steers a running turn is answered even past the last round of calls.