Harness Hub keeps conversations connected
link-harness-hub serves a harness that speaks ACP over stdio to
Link's bridges, as Link Harness serves its own: ACP over a WebSocket at
the endpoint's /acp, opened with the agent's headers and its endpoint
key. It's a package each adapter's relay is built on, in the adapter's
own serve, never a process of its own.
The harness behind it
One harness runs behind the hub, started by the adapter's Start on
the first message and again whenever its output ends. It answers
/health once the harness has agreed on ACP, waiting a little for one
still starting. A permission the harness asks for is given with the
first option that allows, since Link's sandbox is what confines it.
A harness that can't start
A start that fails, or never agrees on ACP, is tried again only after
a wait: 1 s after the first failure, doubling with each one in a row,
at most a minute. Until then /health and every message are refused
with the last failure's reason, so health checks can't restart a
broken harness every second. A harness that answered once and then
stopped starts again at the next message, with no wait.
Threads and sessions
A bridge names a session by its thread (telegram:42), as Link Harness
does; the harness makes sessions of its own. The hub keeps which
session is each thread's in a file, so a thread goes on where it was
after a restart: taken up with the harness's session/resume, or
session/load when that's all it has, else started anew. A thread's
turns run one at a time, and each session's updates go back to the
bridge under the thread's name.
What a prompt carries
A prompt is told taken (_link/taken) as it's forwarded, so a
webhook's sender is answered then. When its _meta.link says it's from
someone other than the owner, the hub writes who into its text, a line
of its own with their name and hops, each of their lines quoted, so a
harness that reads no _meta still knows; a sender that isn't one is
refused before the harness sees it. The _meta goes on to the harness
as it came.
A command by another name
The owner's command goes to the harness by a name it offers, as its
session's latest available_commands_update lists them. A prompt that is one
command, an action of Link's namespaces or one of its aliases
(/chat:create, /new), goes as the first of the action's names the
harness offers (/clear to Claude Code); one it offers, or one no
action has, goes as it came. The names are the namespaces' plugins'
actions.toml's.
The first command
A known command waits for its session's command list, including a list
published after session/new answers. If no list arrives within the
harness call timeout, the command fails before forwarding. Normal
messages do not wait for command discovery.
Link's tools
Link's tools and memory go with each session the hub makes, resumes or
loads, as its mcpServers: each an MCP server over HTTP at its address
on Link's proxy. An adapter's configure keeps them (Tools::keep)
and its serve hands them to the hub. A harness that says it takes no
MCP server over HTTP (mcpCapabilities.http) is given none, and its
adapter writes Link's tools into the harness's own settings instead.
Agent state
The hub answers the agent state of
ASP for the
harness behind it, each session named by its thread both ways. A
harness that advertises agent state answers it itself. Otherwise the
adapter's Store reads and writes the harness's own files, and the
sessions a store doesn't give are replayed through the harness's own
session/list and session/load, when it has them. Either is asked
only between turns: an export or import waits up to two minutes for
the turns running to end, and is refused if one still runs.
What comes in
An import's sessions each become a thread: under its old name when that's a thread's and the hub keeps none by it, else a new one. The hub keeps the session the harness made as that thread's, so the bridge's next prompt there goes on where the conversation was. A harness with neither answers that it takes none, kind by kind.
Its endpoint
The hub refuses a connection without the key, or naming an agent that
isn't its own, and proves it holds the key before it's sent it, as the
contract
asks; Hub::serve sets TCP_NODELAY on each connection it takes.
Why it works this way
The hub's decisions explain its thread mapping, native state boundary and why it shares code inside each adapter's endpoint.
Link-owned namespace commands
Kept tool delivery also holds the agent's proved action endpoints and names. The hub runs these owner commands through their routes before forwarding a prompt to the native harness, and returns the typed answer through ACP. It adds them to the harness's advertised command list. A message from another sender cannot invoke them. Per-harness conversation commands continue to use the native session's advertised names and aliases.