Normative companion to runtime-boundary.md, which covers
the ownership split and the attachment/observation contracts, and to
the integration guide, which walks a host
through implementing the callbacks. The historical
2026-08-02-managed-fleet-attachment-plan.md covers the attachment model (one current attachment per conversation, lineage,
delegated turns, recovery tier 3). This one covers the two wiring questions a
deployment has to answer: how does a host teach this build about a runtime,
and where does attachment state survive a restart.
ClawConnect stays generic. Nothing here — and nothing in packages/core —
knows a runtime's CLI, transport, pairing, project model, or credentials. Every
mention of a concrete runtime below is an example of what a host supplies.
AgentSessionRuntimeRegistry.register({ id, provider, inspect, continue?, detach? })
(packages/core/src/agent-session.ts). Three functions, each addressing ONE
already-known session; capabilities are derived from which ones were supplied.
There is no list/enumerate callback and no spawn, so there is no surface
through which ClawConnect could sweep a runtime's sessions.
Every callback runs under a deadline (AGENT_SESSION_CALL_TIMEOUT_MS, 60s;
per-call override via opts.timeoutMs) and receives an AbortSignal that is
aborted when the deadline passes. A callback that never answers becomes an
ordinary unavailable status with code <op>_timeout — never a wedged turn,
because terminal recovery awaits these calls.
The factories (createApp, createMcpServer) have always accepted a registry.
That is enough for an embedder, and it was NOT enough for production: the
binaries this repo ships (apps/chatgpt/src/index.ts,
packages/mcp/src/bin.ts) are what actually run, and nothing could hand them
one. The gap is closed with the smallest mechanism that keeps the boundary
intact — the operator names ES modules to load, the same way they already name
~/.clawconnect/agents.json:
CLAWCONNECT_AGENT_SESSION_RUNTIME_MODULES=/opt/acme/runtime-bridge/register.mjs
Each module exports registerAgentSessionRuntimes(registry) (or a default
function of the same shape) and registers whatever it can answer for:
export function registerAgentSessionRuntimes(registry) {
registry.register({
id: "example-runtime",
provider: "example-provider",
inspect: (ref, opts) => /* the host's own transport */,
continue: (ref, request, opts) => /* … */,
});
}loadAgentSessionRuntimes() (packages/core/src/runtime-modules.ts) resolves
comma- or newline-separated specifiers — bare package names, absolute paths, or
paths relative to the process cwd — imports each one, and never throws: a
module that is missing, fails to import, exports no registrar, or throws while
registering is logged and skipped, because every task that does not involve a
If the variable is unset, or every module it names registers nothing, then no
runtime is reachable at all — core ships none — and every attachment reads back
as a precise unknown_runtime result rather than an error.
A worked example lives in examples/local-tmux-runtime/: plain JavaScript, no
build step, no import from @clawconnect/core, loadable by the shipped binary
exactly as documented above.
The outer adapter stays outside. A host's own runtime bridge is one such module: it owns that runtime's CLI/API, pairing, project selection, and credentials, and hands ClawConnect three functions. Specifiers come from the deployment's own environment — operator configuration, at the same trust level as the agent registry, never anything a caller or an agent can influence.
Attachment lineage is durable state the model depends on: which conversation is delegated to which session, and its replacement history. The runtime session routinely outlives the ClawConnect process that attached to it.
| Path | Runtime registry | Attachment store |
|---|---|---|
apps/chatgpt (createApp) |
CLAWCONNECT_AGENT_SESSION_RUNTIME_MODULES via index.ts |
on by default — <jobStoreDir>/<agentId>.attachments.json |
packages/mcp bin (createMcpServer) |
same env var via bin.ts |
CLAWCONNECT_ATTACHMENT_STORE_DIR, default ~/.clawconnect/attachments |
createApp / createMcpServer as a library |
caller passes agentSessionRuntimes |
caller passes a directory; unset = in-memory |
packages/cli |
none | none |
createMcpServer deliberately does not default a store directory: it is also
what tests and embedders construct, and a default would make construction write
files. The shipped bin passes one — that is the deployed path this exists for.
Job persistence stays off there, unchanged: an in-flight job belongs to a live
stdio connection that a restart ends anyway, while an attachment does not.
The CLI is unchanged and wires neither half. It is a one-shot process that makes no managed-session claim — it never registers a runtime and never surfaces an attachment — so there is nothing for it to persist between invocations.
SessionManager.observationSeq resumes above the highest observationToken
found in any reloaded record, current or superseded. The token is durable and
its counter is not, so a process that restarted at zero would mint tokens the
compare-and-set in applyAgentSessionStatus is bound to refuse — every
observation, and every recovery that depends on one landing, would silently do
nothing. Covered by the post-restart observation and recovery tests in
fleet-attachment.test.ts.