Decisions

Why lnk measure works the way it does.

The plugin and its command

Why the watcher is a plugin of its own, and its command a group beside lnk agent.

Why is the watcher a plugin of its own?

It measures from outside, around the programs it measures as the sandbox is around them, and never part of them: the harness doesn't report its own memory, and an agent on OpenClaw or Hermes is measured as one on Link Harness is. Neither the harness plugin nor a harness owns that, and the machine is measured with no agent at all. It finds what to measure by asking: the plugins built on it answer its questions, measure.agents and measure.traffic, in its contract's types, so it names none of them.

Why is the command lnk measure, a group of its own?

The machine is measured as well as its agents, and the watcher, its settings and the export are the measure plugin's, not an agent's. So it's a group of its own, beside lnk agent, and lnk agent list shows the two numbers you'd look for there: memory and the day's spend.

What a measure is

What a measure and a unit are, and how they're kept on the machine and sent from it.

Why OTLP?

Every observability tool reads OpenTelemetry's protocol, so Link's measures go to Grafana, Honeycomb or your own collector as they are, with nothing of Link's to install there. It's also how a harness reports what only it can count.

Why is a measure's rollup in its name?

link.memory.used.sum, link.memory.peak.max, link.time.turn.own: whatever reads a measure knows, from its name alone, how a unit's adds up to its parent's (summed, the largest, or not at all), so a dashboard of agents sums to the machine's without knowing Link.

What is a unit?

The machine, an agent, or one of an agent's processes (its sandbox's bwrap or sandbox-exec is the kind sandbox), each by the ID it has: the machine's, the agent's, and <agent id>/<pid> for a process. An agent from before IDs is <machine id>:<name>. A process is listed under its agent, not its parent process: what an agent costs is the sum of them.

An agent's owner and teams

An agent's owner is an attribute of its own (link.owner.id), so a dashboard can group agents by whose they are, and so are its teams (link.team.ids, the IDs of the teams whose files list it, an array since an agent can be in several), so one can group them by team.

Why are teams sent by their IDs, not their names?

A team's name is text a person chose, and the measures carry as little of anyone's words as they can. An ID says which team and nothing more, and a dashboard groups by it as well. OpenTelemetry's conventions have no attribute for teams, so it's Link's own, link.team.ids.

Why is the store on the machine not OTLP?

OTLP's JSON spells each measure's name, unit and kind out at every point, about eight times the disk of the numbers. The watcher keeps each look as one short line of numbers by unit and measure, and speaks OTLP where it crosses programs: from a harness, and to your endpoint. A look keeps the machine and each agent, and each process only when your settings say (processes, off in Link's), since it multiplies the disk by an agent's processes: a trade, so a setting.

Why are a file's values by order, with so much left out?

Each file names its measures once, in its first line. Values follow that order, omitting zeroes, unchanged totals and repeated unit descriptions. A reader carries the omitted state forward, reading from the start or from the checkpoint in day.json. Completed hours and days are compressed with zstd. A value that stays zero all day is shown as absent.

How long is it kept?

A trade, disk against history, so it's the watcher's settings, not a constant: Link's (a look every 10 seconds, each kept a day, a minute's summary kept 30 days) are a file the plugin ships, and yours win where you set them. They're the machine's, not an agent's: every agent here shares one watcher.

Why is the export OTLP over HTTP as JSON?

gRPC and protobuf would add dependencies to Link that every collector already spares: they take OTLP's JSON over HTTP, with headers you set for a hosted one's key. It's one POST of the same data to <url>/v1/metrics.

Where each figure comes from

Who may say each measure, and how each reaches the watcher.

Why does a harness report through a file?

It runs in its sandbox, which writes only its home and reaches no port of this machine's. So lnk agent start names a file in its home (LNK_MEASURES), the harness appends a line of OTLP a turn, and the watcher reads on from where it left off. The harness moves it aside past 1 MiB, so it never grows.

Who may say which measure?

Each measure has one writer, and a file gives only what its writer may say. What's measured from outside (memory, CPU, GPU, disk, network, processes, ports) is the measure plugin's alone. A harness says only what it counts itself, in its home's file: a turn's tokens, cost, calls and times. The harness plugin says only how long the starts and stops it made took, in the agent's settings. So a harness can't claim to have started in no time, nor the harness plugin a turn.

Why are starts and stops timed by the harness plugin?

A harness can't time its own start: it isn't running until it is, and its stop ends it. The harness plugin starts and stops every harness, so it times each, from its service started to its harness answering its health check, and from the stop asked until its main processes are gone. It appends each as a line of OTLP to times.jsonl in the agent's settings (~/.lnk/agents/<name>/), which no harness writes, and the watcher reads it as it reads a harness's file. A file, not a message to the watcher, so a start is timed whether the watcher runs or not.

Cold, warm and the system's starts

A start is from cold when nothing of the agent was timed since the machine started, and warm otherwise. A start the system makes itself, at boot or login, isn't timed, since no lnk runs it and nothing waits for its answer. A stop is waited on 10 seconds at most, so lnk agent stop is never held up by one that hangs; one longer isn't kept.

Why is the file emptied once it's read?

The harness appends measures and the watcher reads them under the same exclusive file lock. Once the watcher has consumed every complete line, it empties the consumed file, bounding disk use without changing the wire format. A partial final line remains for the next read. The lock prevents a writer's new line from being removed between the read and truncation.

Why does network come from the proxy?

An agent's traffic leaves its sandbox only through its proxy, which counts every byte each way for the wire's log already. So it's what crossed the proxy, not a guess from the machine's interfaces, which every program shares. A proxy beside an unconfined harness counts only what passes through it, since that harness can bypass it.

Published snapshots

The provider advertises its existing private snapshot directory in the agent listing, using the measurement contract's type. That keeps collection below the provider's implementation, avoids repeated discovery and command startup, and leaves publication and cleanup with their owner. Each used sample reads once, without a cross-sample cache. Providers without snapshots keep their command protocol, discovered with the existing agent-list refresh.

Tokens

Why every agent's tokens are counted at its proxy, and how, so harnesses compare.

Why do tokens come from the proxy?

Only Link Harness counts its own tokens; OpenClaw, Hermes and DeepSeek Harness don't say theirs. Every agent's model requests already pass through its proxy, which adds the key, and every provider's answer says what it took. So the proxy reads that usage as the answer passes, and every agent's tokens are counted the same way, as the provider counts them, whichever harness it runs. It's read as the answer goes by, never held back: a stream's events one at a time, looked at only when one names usage, and a whole answer scanned for its top-level usage without being kept.

Read inline

Reading runs inline, as the proxy writes each piece to the program: a stream's event is held only until it ends, at most 4 MiB (one past it isn't read, and its request counts as one without usage), and a whole answer only its top-level usage, one value a key.

Why one source for tokens?

Link Harness reports its tokens too, and its requests pass through its proxy: counting both would count each token twice. An agent whose proxy carries its model takes its tokens from the proxy alone, and from its harness everything else (turns, spend, a turn's times). An agent with no sandbox has no proxy, and its harness's count is the one there is. The proxy's is the one kept where both are, so every agent is counted alike. The watcher remembers which agents their proxy counts, as each last ran, so a turn reported after the agent stopped isn't added on top.

What it costs

The cost: an agent counted by its proxy loses the tokens of any model it reaches some other way, such as a hosted API through a plain CONNECT with a key of its own, which the proxy can't read.

Why is a stream without usage counted, not changed?

An OpenAI Chat Completions stream gives its usage only when its request asks. The proxy could add the ask, but a request the harness didn't send could change the answer it gets: a last event it doesn't expect. So nothing is added, and an answer that gave no usage counts as a request without it (link.requests.unmetered.sum), so a gap is seen as one rather than as fewer tokens.

Why are a model's answers asked for uncompressed?

A compressed answer can't be read as it passes without unpacking it, which a proxy that only passes it on shouldn't do. So a request for a model's answer asks for it uncompressed (Accept-Encoding: identity), which providers honor and a harness reads either way; a model's answer is text, and small beside what a harness downloads. One that comes compressed anyway counts as a request without usage.

Are a local model's tokens counted too?

Yes. A local runtime's requests go through the same proxy, by the local model verb, so a local agent's tokens are counted as a hosted one's: OpenAI's format on /v1 and Ollama's own on /api/chat and /api/generate. It also lets the counting be tested without a provider's host.

How are tokens counted, so harnesses compare?

As Link Harness counts them: input is all of the prompt (Anthropic's input_tokens, cache reads and cache writes together; OpenAI's prompt_tokens), and cached is what was read from the provider's cache. Cache writes are a measure of their own, link.tokens.cache_written.sum, since Anthropic charges a write above the price of input and a price made from input alone would be wrong. A model request isn't a turn: for a harness that isn't Link Harness, link.requests.model.sum counts them and turns stay Link Harness's.

Processes and the machine

How the machine and an agent's processes are read, on Linux and on a Mac.

Why is GPU time counted since the last look?

A GPU says how busy it is now, not how much it was used since it started, and NVIDIA's says nothing of a process's time. So the machine's GPU time over a look is how busy its GPUs were at the look before, times the look's length: GPU-seconds, summed as CPU's core-seconds are, with two GPUs at half counting as one. A process's is the busy time its kernel counts on its GPU (Linux's DRM fdinfo), which it grew by since the last look, and an agent's adds its processes'.

Sent as a delta

Each is sent as a delta since the last look, as a proxy's tokens are, and the watcher keeps what each process had at its last look, by its pid and start, so a restart counts none of it twice. On NVIDIA a process's memory is read, not its time.

How is a Mac's machine read?

A Mac has no /proc. Its CPU is the cores' ticks since it started (host_statistics), its network each interface's bytes (netstat -ibn), its ports those listened on (netstat -anp tcp, and for a process, lsof, which sees this user's processes), and its GPU what ioreg says of each accelerator. Each command is run by its full path, at most 5 seconds, and only its numbers are read; what each prints is tested as recorded from a Mac.

Why are disk read and written the control group's?

On Linux, an agent's service's control group counts every process it ever started, even those that exited between two looks, as a turn's tool server does. A Mac has no control group, so there it's the processes running at the look.

How is an agent's memory counted?

An agent is measured from outside, so every process it runs counts, whichever harness it is. On Linux that is its service's control group, which holds every process it starts, and on a Mac the tree under its processes. Memory in use is PSS on Linux, where pages shared between processes are split between them, so a sum doesn't count a shared page twice. On a Mac it is the footprint, as Activity Monitor counts it.

What a system calls a process is what the process chose: a script's file name, which an agent may take from a conversation, or any name a program sets itself. A name taken from it would carry those words into what's shown, kept and sent, and give a harness with no network a way to write into your endpoint. So a process is named only by one of a short list of programs (Link's own, the sandboxes', and what harnesses run on), and any other only by its ID.

Why is a harness's point capped?

What a harness reports of itself is its own say, so it can claim anything: a turn of 1e308 tokens. Two such points sum to infinity, which JSON can't write, and the look or minute holding it would read back as nothing, every agent's with it. So a point past what a turn could be is dropped, and every sum stops at the largest number there is.