@borgee/agents-host
v0.2.126
Published
Minimal local agents host: connects local Claude/Codex/Copilot runtimes to a Borgee agent over @borgee/plugin-sdk
Keywords
Readme
@borgee/agents-host
Minimal local runtime host for Borgee agents.
- Standalone env mode keeps the existing one-process / one-agent flow.
- Local-config mode adds first-step multi-agent local hosting from a host config file plus one config file per agent, with hot reload.
What this is (and isn't)
This package intentionally focuses on the smallest local-hosting loop:
Borgee channel message
→ /ws/plugin (BPP)
→ @borgee/plugin-sdk
→ AgentsHost.handleMessage
→ local claude / codex-acp / copilot runtime
→ BPP reply
→ Borgee channelIn scope:
- standalone env startup
- foreground CLI startup for one hosted agent
- local-config multi-agent startup
- one isolated
AgentsHostper effective agent key - per-channel conversation memory via each provider's native session mechanism
- one message per turn, posted once the turn ends, with what the agent is doing reported on the ephemeral activity rail meanwhile
- hot reload for host config and agent file add / update / remove
Out of scope:
- remote command execution / shell dispatch
- node provisioning
- systemd/service installation
- scheduled / periodic prompts
Running
pnpm --filter @borgee/agents-host dev # tsx, env-var single-agent mode
pnpm --filter @borgee/agents-host build # tsc -> dist/
pnpm --filter @borgee/agents-host start # node dist/index.js, env-var single-agent modeFor the published CLI entry point:
agents-host start <serverUrl> <apiKey> [options]
agents-host start-managed <serverUrl>
agents-host start-managed <serverUrl> <apiKey> [agent-options]
agents-host describe-managed <serverUrl>
agents-host apply-managed <serverUrl> --stdin
agents-host apply-managed <serverUrl> --spec-json <json>
agents-host cleanup-managed <serverUrl> [--purge]
agents-host log <serverUrl> [--lines <n>] [--follow]
agents-host log <serverUrl> --path
agents-host start --config <path-to-host-config> [--debug]
agents-host validate --config <path-to-host-config>
agents-host describe --config <path-to-host-config>
agents-host print-layout --root <dir>
agents-host generate-config --root <dir> --stdin
agents-host generate-config --root <dir> --spec-json <json>
agents-host version
agents-host --version
agents-host updateStandalone env mode
Use the existing env-var flow when you want exactly one hosted agent per process.
BORGEE_BASE_URL=https://your-borgee-server \
BORGEE_AGENT_API_KEY=bgr_xxxxxxxx \
BORGEE_AGENT_NAME="My Agent" \
RUNTIME_PROVIDER=claude \
pnpm --filter @borgee/agents-host devCodex now ships on the same default hosted-provider surface as Claude and Copilot. Select RUNTIME_PROVIDER=codex directly unless you are explicitly rolling that path back with AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=codex-provider.
Shipped internal compatibility defaults and rollback controls
The current shipped internal compatibility surface resolves gates in this order:
- start from the shipped defaults:
copilot-provider-v2,codex-provider,managed-runtime-convergence, andpolicy-audit-enforcement - add any explicit entries from
AGENTS_HOST_INTERNAL_COMPATIBILITY_GATES - subtract any explicit entries from
AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES
The shipped default set now also includes connections-state-layer, context-injection, skill-runtime, localhost-gateway, token-binding, collaboration-outcome-model, task-thread-collaboration-contract, collaboration-capabilities-diagnostics, and provider-idle-shutdown. attention-follow-semantics, collaboration-skill-first, and hosted praestoclaw-provider still remain opt-in.
Managed-runtime settings still preserve the managed rollback rule for connections-state-layer plus token-binding: when managed-runtime-convergence remains enabled, the managed surface forces that pair on; explicitly disabling managed-runtime-convergence removes that pair from the managed surface again unless you also explicitly re-enable them.
Managed runtime precedence stays intentional: detached daemon respawns always reuse the persisted managed-runtime-settings.json snapshot, bare start-managed <serverUrl> and apply-managed treat the caller's current env as an explicit request to update that managed runtime surface, and full start-managed <serverUrl> <apiKey> ... validates the agent upsert against the existing managed-root snapshot before ambient rollback env can reject an otherwise valid existing surface.
Legacy managed roots created before managed-runtime-settings.json existed need one bare agents-host start-managed <serverUrl> migration pass before full per-agent updates can rely on that snapshot-backed precedence.
Rollback and hardening controls stay explicit:
AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=<comma-list>disables shipped gates for rollbackAGENTS_HOST_INTERNAL_PROVIDER_IMPLEMENTATIONS=copilot:v1forces Copilot back to the v1 adapter while its v2 gate stays enabled; staleclaude:v1|v2entries are tolerated as no-ops for compatibilityAGENTS_HOST_INTERNAL_POLICY_MODE=enforcehardens the shippedpolicy-audit-enforcementgate beyond its defaultaudit-onlymodeAGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=provider-idle-shutdownkeeps legacy provider runtime processes resident again;PROVIDER_IDLE_SHUTDOWN_MINUTES=0does the same without touching gates
Or with the CLI:
agents-host start https://your-borgee-server bgr_xxxxxxxx --provider copilot --debugManaged daemon bootstrap mode
agents-host start-managed has two forms:
agents-host start-managed <serverUrl> <apiKey> [agent-options]bootstraps or reuses the per-server managed daemon, then upserts the requested agentagents-host start-managed <serverUrl>restarts or resumes an already-managed runtime for that server without re-supplying the agent API key or options
Unlike foreground start, both forms exit after the managed daemon is ready,
and the full form additionally waits for the requested agent reconcile.
It:
- resolves a managed runtime root under
~/.borgee/agents-host/managed/specific to that normalizedserverUrl - reuses the daemon already serving that runtime root when it is alive
- otherwise starts one local supervisor daemon for that managed root and waits for it to become ready
- for the full
<serverUrl> <apiKey> ...form only, upserts the requested agent and waits for that reconcile before reporting success
The short resume form does not auto-seed a new managed runtime. It fails
clearly unless that serverUrl already has a persisted managed config with at
least one persisted agent entry from an earlier full
start-managed <serverUrl> <apiKey> ... bootstrap. Those persisted agents may
all currently be disabled; the managed runtime is still considered bootstrapped
and resumable in that case.
Each managed runtime root keeps:
.generations/<uuid>/as validated immutable config generations; Unix and macOS publishcurrent,agents-host.yaml, andagents/symlinks, while Windows publishes the active generation through the atomically replaced ordinarycurrent.jsonpointer and resolves the generation internally without requiring Developer Mode or elevationmanaged-runtime-settings.jsonas the persisted managed runtime surface snapshotdaemon.logand its singledaemon.log.1backup as bounded detached managed daemon stdout/stderr logs.state/as the per-agent runtime state basectl.sockas the local control socket
Detached managed launches send stdout and stderr through a separate local log sink into <managed-root>/daemon.log. The sink rotates by closing the current file, replacing daemon.log.1, and opening a new current file; no Host keeps a descriptor to a rotated file. The pair defaults to a combined 20 MiB, including when debug is disabled. Existing oversized files are trimmed to their newest bytes before recording resumes. Console capture includes CLI import failures, native stdout/stderr writes, and inherited child output.
The sink never becomes the Host process: readiness and shutdown still target the actual daemon PID. Contention or a file error makes the sink drain and discard output with capped retry backoff. Initial unavailability prints one fixed warning to the launching command; recovery records one fixed output-loss warning in daemon.log. A live or unidentifiable writer lock is never stolen. After pipe EOF the sink exits; if a descendant retains the pipe after the owning PID is confirmed gone, the sink retires after a short drain window. PID reuse or permission-denied ownership checks remain unknown and never cause another process to be killed. An unexpectedly terminated sink is isolated from ordinary Node console stream errors; explicit native descriptor writes can still fail synchronously.
agents-host log <serverUrl> --follow follows the current path across rotation. This is a bounded recent-output view, not a lossless archive. The console log contains raw process output and is not privacy-filtered like the structured diagnostic JSONL; review it before sharing.
This MVP does not migrate legacy ~/.borgee/agents-host/single-agent state
into the managed runtime root.
Agent identities generated through start-managed are internal and stable for
the same API key within one server runtime root; repeated starts with the same
API key replace that generated agent entry instead of appending duplicates.
Example:
agents-host start-managed https://your-borgee-server bgr_xxxxxxxx --provider copilot
agents-host start-managed https://your-borgee-serverMachine-oriented managed-runtime commands
describe-managed and apply-managed are JSON-only commands for machine callers.
They operate on the same per-server managed runtime root as start-managed, but
they expose full-set managed-runtime state instead of the legacy single-agent
upsert flow.
agents-host describe-managed https://your-borgee-server
printf '%s' '{"host":{"borgeeBaseUrl":"https://your-borgee-server"},"agents":[{"key":"cp1","name":"Copilot","apiKey":"bgr_xxx","provider":"copilot"}]}' \
| agents-host apply-managed https://your-borgee-server --stdindescribe-managedprints one JSON object to stdout only:{ "ok": true, "present": false }{ "ok": true, "present": true, "spec": { ... } }{ "ok": false, "error": { "code": "...", "message": "..." } }
apply-managedaccepts one full-set managed spec, validates that itshost.borgeeBaseUrlmatches the requested runtime binding, rejects empty full-set specs, then prints one JSON object to stdout only- expected managed failures are reported as structured JSON on stdout and exit 0
- malformed CLI usage still follows the normal usage stderr + non-zero exit path
--stdinis preferred because--spec-jsonexposes the JSON in process arguments
Managed daemon logs
Resolve or stream the managed daemon log for one server runtime directly from the managed runtime root helpers:
agents-host log https://your-borgee-server
agents-host log https://your-borgee-server --lines 0 --follow
agents-host log https://your-borgee-server --path- default output prints the last 100 log lines
--followkeeps polling for appended content and resumes after truncation or replacement--lines <n>accepts non-negative integers only;0suppresses the initial tail before follow mode--pathprints only the resolved log path and does not require the file to exist yet
Debug mode
Enable error diagnostics, including lifecycle context needed to explain a failure:
agents-host start https://your-borgee-server bgr_xxxxxxxx --provider copilot --debug
agents-host start https://your-borgee-server bgr_xxxxxxxx --provider codex --debug --level error --debug-log-max-mb 20
AGENTS_HOST_DEBUG=1 agents-host start --config ./agents-host.yaml--level error is the default diagnostic mode. It retains operation start/success records needed to locate missing results; it is not an error-severity-only filter. The level option does not enable debug by itself. --level perf is reserved and currently fails with an explicit unsupported diagnostic message; dedicated performance diagnostics are not implemented.
Notes:
--debugapplies to foregroundstart <serverUrl> <apiKey>,start --config <path>, and managed Host startup throughstart-managedorapply-managedAGENTS_HOST_DEBUG=1is the env fallback for both the CLI andpnpm --filter @borgee/agents-host dev- Managed commands persist explicit diagnostic options separately from Agent configuration. Omitting those options preserves the saved setting;
--no-debugrequests disabling it. A running daemon reports a changed setting as pending until its next startup and does not restart merely to change logging. - host-authored debug logs stay secret-safe: they avoid API keys, raw prompt text, raw replies, and other sensitive tokens
- Copilot child-process stderr is summarized by byte and line counts in debug mode
- local-config mode prefixes debug logs with the managed agent key so interleaved output stays readable
Host debug logging supports Windows, Linux, and macOS.
For Agents managed by remote-helper, append --debug to the existing helper install command. Use --level error --debug-log-max-mb 20 to state the defaults explicitly. The installer retains these options in the background service's arguments, and the helper forwards them through agents-host apply-managed; the helper does not write a separate diagnostic log. Foreground borgee-remote-helper daemon accepts the same options.
# Keep the original server, computer token, and directory arguments.
npx @borgee/remote-helper install --server "<server>" --token "<computer-token>" --dirs "<directories>" --debug --level error --debug-log-max-mb 20These options require supporting helper and Host versions. Managed logging settings are restored on the next Host startup. Applying a change to an already-running Host preserves its current Sessions and reports diagnostics.restartRequired; it does not claim the new setting is active. An older daemon with no diagnostic status reports its effective setting as unknown. To disable saved managed diagnostics, use --no-debug; do not combine it with --debug, --level, or --debug-log-max-mb. The helper and managed CLI require --debug when specifying level or size. The chosen size also sets the managed console pair budget on the next daemon start. --no-debug disables structured diagnostic recording but preserves the saved size for console capture; without a saved setting the console budget is 20 MiB.
The managed daemon's console log is discoverable with agents-host log <serverUrl> --path. Managed diagnostic records go to the bounded JSONL without duplicating them in the daemon console log; ordinary console output remains captured in the separately bounded daemon.log pair. Once debug is active, its startup output lists each Agent's structured diagnostic log path; that JSONL and its .1 backup are the files to share. A pending configuration does not create a log for a daemon whose debug mode is still off.
Remote-helper parses managed command results from stdout separately from stderr. A console-log warning does not turn a successful Host launch into an assignment failure; recognized log-unavailable warnings remain visible without forwarding arbitrary stderr text on success.
Debug mode also writes structured JSONL records to <stateRootDir>/diagnostics/agents-host.debug.jsonl and prints the absolute path at startup. Single-agent state normally lives under ~/.borgee/agents-host/single-agent/agent-<hash>; use the printed path rather than guessing the hash. Local-config agents each use their own state directory and log budget.
--debug-log-max-mb <positive-integer> sets the combined size limit for the current file and its single .1 rotation backup, in MiB (default: 20). Rotation discards older records and reuses the same files across restarts. Each agent state directory must have a single owning Host process; concurrent independent Hosts writing the same directory are unsupported. Each Agent JSONL pair has its own budget, separate from the daemon console pair. For a console budget B and retained Agent diagnostic budgets B₁…Bₙ, the file-pair total is bounded by B + ΣBᵢ after successful writer initialization; with equal budgets this is (N + 1) × B. Disabled or removed Agents can leave retained files, and a smaller new budget does not trim an inactive Agent’s old files until its writer next opens them. Files that cannot be opened or trimmed are reported unavailable; no new output is appended to them. This is not a machine-wide quota or a recording duration. Without debug mode, no diagnostic log is created. A logging failure emits a fixed warning without stopping the Host.
Default Session opening records include a start event and a success or failure event, correlated by operation, agent, and channel IDs. Outcomes include elapsed milliseconds; failures include the last Host stage, the public rejection reason, and safe error metadata. Stages describe Host boundaries such as mapping lookup, context preparation, or session restoration; a restoration failure does not by itself prove which native RPC failed. Raw exception messages, stacks, credentials, prompts, and replies are not diagnostic payloads.
After a temporary structured-log write failure, later local records trigger recovery attempts with backoff from 250 ms up to 2 seconds. Recovery does not require Server diagnostics or a Host restart. Incomplete trailing records are removed before appending resumes. No background retry timer or backlog is retained: records lost while storage is unavailable or backoff is active are not replayed. Server-origin batches remain unacknowledged until their append succeeds.
Error diagnostics follow the actual operation across these boundaries:
| Failure area | Evidence to inspect | Interpretation limit | | --- | --- | --- | | Codex process and handshake | Spawn target, connection identity, process error or exit, initialize result and reported version when available | A PATH command is not proof of the loaded executable image. A version difference alone is not a compatibility failure. | | Saved Session | Host opening stage and operation identity, correlated native request and local validation failures | Recognized failures carry a static reason and evidence label. Unrecognized causes remain unknown. | | Model access | Actual turn errors, native error categories, HTTP status or structured error code when available | Opening a Session does not validate authentication or model access. Debug mode sends no extra model requests. | | Borgee connection and delivery | Connection/authentication and DNS failures, reconnects, inbound request outcomes, response writes and normalized activity write failures | A successful socket write is local transport evidence, not acknowledgment that the server or browser consumed the response. |
Categories use known structured fields, explicit local validation, or recognized native error text templates. Static reasons distinguish configuration, capability, saved-state, missing Session/provider, malformed or identity-mismatched native response, account/model incompatibility, and content policy failures. local-validation identifies a local contract check; text-template identifies a known native message pattern, without copying its text. Unknown errors remain unknown. Default Session opening keeps its public reason separate from the Codex nativeReason, alongside category and evidence. Executable paths and correlation identifiers can identify the local user or workspace and should be reviewed before sharing. Failed native operations retain connection/request identities so concurrent requests can be distinguished from the Host opening operation.
Shared Codex restoration validation also records incomplete turn history, multiple active turns, and a history snapshot that does not stabilize. These failures preserve local-validation evidence and the native reason when surfaced through default Session opening.
Malformed Codex transport output records a static protocol anomaly type without the original bytes. A malformed response matching a pending request rejects it immediately with response_invalid_envelope, rather than recording RPC success or waiting for its timeout. This includes invalid error objects, missing or conflicting result/error fields, and envelopes mixing a method with response fields. Each connection records the first occurrence of each anomaly type immediately and summarizes repeated occurrences at closure, keeping noisy output bounded. Valid unknown method requests and notifications remain compatible. An anomaly establishes invalid transport output, not a particular binary version or its root cause.
Inbound request outcomes retain unconfirmed when the handler cannot confirm the operation. Application responses that exceed the transport limit record application_response_too_large for the error fallback actually sent; the original handler result does not imply successful response delivery.
Normalized activity delivery failures distinguish socket closure, send/write failure, and queue capacity rejection, with connection/event identities and sequence/part metadata for correlation. These records contain no activity bodies. Local queue and write callback evidence leave server receipt and browser delivery unconfirmed; a DNS failure does not establish its network configuration cause.
Normalized admission and encoding rejections retain a separate borgee_normalized_rejected record with local_admission evidence, including unsupported capability, validation, and transport-limit reasons. Early guards omit an event ID; encoding failures retain it only when already generated. A successful fallback or recovery write does not erase the original rejection or prove delivery of the rejected activity.
The executable-not-found category reports a spawn ENOENT, not a filesystem proof that the executable is absent. A required script interpreter or executable loader may be missing; a workspace disappearing after validation can also cause this error.
When both server and Host support server diagnostics, --debug opts this connection into delivery of safe server-side connection and request records. They appear as borgee server diagnostic in the same JSONL and share its size budget. Server request IDs, browser request IDs, server connection IDs, and server instance IDs remain separate from the Host socket ID; use the matching IDs and the original event timestamp to trace a retry across both sides.
The server retains up to 256 events per Agent and 4096 events in total for at most 24 hours, and retries delivery after reconnection or a failed Host append. Its separate journal lives under the configured server logging root, uses coalesced snapshots, and can lose the last unflushed second on a process crash; it is not a cluster-wide log store. A Host acknowledgement confirms that the batch was appended locally; it does not guarantee survival of a sudden power loss. Repeated event IDs still present in the retained files are deduplicated. Retention and rotation can discard older evidence, so missing records do not prove that an operation never ran.
Server evidence requires a valid authenticated connection. A complete network outage defers delivery until reconnect; invalid or revoked credentials cannot retrieve the Agent's history. An older server or a Host without debug continues its normal business protocol without this exchange. The server forwards only fixed structured metadata, never its general log file or another Agent's records. This supplements local diagnostics; it does not establish that the browser rendered a response.
If the optional diagnostic identity lookup fails temporarily, later batches can retry it after a 30-second backoff on the same connection. Only one lookup runs per connection, and business requests remain available while it is pending. Retried delivery still requires the authenticated Agent identity and current connection credentials.
To diagnose a failure:
- Add
--debugto the existing foreground Host launch configuration, reproduce the failure once, and record the retry time and timezone. - Use the absolute log path printed at startup to locate
agents-host.debug.jsonl. Includeagents-host.debug.jsonl.1when it exists so a rotation does not hide the preceding events. - Upload the original Host-generated JSONL files directly with the retry timestamp. These files already contain the filtered diagnostic records needed to trace the operation; review correlation identifiers and local executable paths before sharing.
A working local Agent can read the same files to inspect failures. Elapsed operation times provide error context; they do not constitute dedicated performance profiling. The files remain available independently of the native Session and are not automatically uploaded to the server.
Disk logs retain registered Host messages and safe metadata. Unknown dynamic messages are represented by a hash and byte count; custom console prefixes are represented by a hash. Host startup failures after logger initialization are recorded, but errors before Host construction and SDK output sent directly to console are outside this log sink.
Environment variables / equivalent single-agent agent options
| Variable | CLI flag | Required | Default | Description |
| ---------------------------------- | -------------------------------------- | -------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| BORGEE_BASE_URL | positional <serverUrl> | yes | — | Borgee server base URL |
| BORGEE_AGENT_API_KEY | positional <apiKey> | yes | — | Agent API key from the web UI |
| AGENTS_HOST_DEBUG | --debug | no | off | Extra host/provider lifecycle logs. Set AGENTS_HOST_DEBUG=1 to enable without the CLI flag. |
| AGENTS_HOST_DISABLE_UPDATE_CHECK | — | no | off | Set to 1 to skip the advisory startup update check. |
| BORGEE_AGENT_NAME | --name | no | Assistant | Display name used in prompts |
| RUNTIME_PROVIDER | --provider | no | claude | claude, copilot, or codex |
| PROVIDER_OUTPUT_MODE | --provider-output-mode | no | normalized | Select legacy explicitly to use the Claude, Codex, or Copilot legacy adapter. The selection does not change provider identity. |
| NATIVE_PROVIDER_COMMAND | --native-provider-command | no | pinned native executable | Executable path used only by the normalized provider; not a shell command or an ACP argument string. |
| CLAUDE_COMMAND / CLAUDE_ARGS | --claude-command / --claude-args | no | claude-agent-acp / empty | Legacy Claude ACP adapter command + args. These settings do not select the normalized executable. The shipped bundled claude-agent-acp path requires empty CLAUDE_ARGS; non-empty args are only valid when CLAUDE_COMMAND points at a custom ACP adapter command. Legacy claude --print --permission-mode bypassPermissions is tolerated as a compatibility alias. |
| CODEX_COMMAND / CODEX_ARGS | --codex-command / --codex-args | no | codex-acp / empty | Legacy Codex ACP adapter command + args. The legacy launch resolves the bundled @agentclientprotocol/codex-acp entrypoint. |
| CODEX_CONNECTION_MODE | --codex-connection-mode | no | stdio | Normalized Codex transport: shared attaches to a daemon, stdio owns a native process. Legacy mode keeps its ACP adapter. |
| CODEX_TRUST_SHARED | local YAML codexTrustShared | normalized Codex shared only | unset | Explicit local consent to control the shared Codex daemon with its existing native permissions. |
| CODEX_SHARED_SOCKET_PATH | local YAML codexSharedSocketPath | no | native CODEX_HOME control socket | Absolute Unix socket path for normalized shared Codex. The daemon must already be running; there is no stdio fallback. Requires explicit shared mode; incompatible with default or explicit stdio mode. |
| SESSION_WORKSPACE_ROOT | local YAML sessionWorkspaceRoot | no | unset | Existing absolute directory for explicit independent normalized Session creation. An unset root disables Create only; supported discovery and opening preserve existing native Workspaces. |
| COPILOT_COMMAND / COPILOT_ARGS | --copilot-command / --copilot-args | no | copilot / parsed but ignored | Legacy Copilot CLI command. The host resolves it to an executable and starts the SDK headless JSON-RPC runtime; legacy COPILOT_ARGS remain ignored. Neither setting selects the normalized executable. |
| COPILOT_SESSION_TTL_MINUTES | --copilot-session-ttl-minutes | no | 2880 | Idle TTL for per-channel Copilot SDK sessions |
| PROVIDER_IDLE_SHUTDOWN_MINUTES | --provider-idle-shutdown-minutes | no | 10 | Idle window before the legacy provider runtime process is shut down and re-spawned on the next turn. Applies to legacy Claude, Codex, and Copilot; normalized adapters do not use this timer. 0 keeps the process resident. Must be non-negative and no larger than the Node.js timer limit (~35791.39 minutes); anything else is rejected at startup. |
COPILOT_SESSION_TTL_MINUTES and PROVIDER_IDLE_SHUTDOWN_MINUTES are two different layers and do not replace each other. The first evicts an individual idle Copilot SDK session inside a running runtime process. The second shuts the legacy provider process itself down once every channel it hosts is idle and takes every live session in that process with it. Normalized Codex does not use this timer: shared mode releases only its attachment, while stdio mode owns the native process lifetime. See Conversation memory for what happens to those sessions afterwards.
Choosing legacy or normalized output
Claude, Codex, and Copilot use normalized output when no mode is configured. Claude uses its pinned Agent SDK native runtime; Codex defaults to its private app-server stdio transport; Copilot uses its official SDK stdio runtime. For example, Copilot needs no output-mode flag:
agents-host start https://your-borgee-server bgr_xxxxxxxx --provider copilot
agents-host start-managed https://your-borgee-server bgr_xxxxxxxx --provider copilotUse --native-provider-command /absolute/path/to/copilot only when overriding the pinned normalized executable. --copilot-command, --copilot-args, and the legacy idle timers remain settings for the separate legacy adapter. Select --provider-output-mode legacy to return to that adapter; its original session records remain intact.
An existing Claude, Codex, or Copilot configuration that omits output mode selects normalized output after upgrading. An explicit legacy value in the Agent configuration or host defaults remains legacy. Switching either direction resumes the same recorded native Session, including conversations first created in normalized mode. Both existing stores are checked for identity and current scope before reuse; neither mode may replace an established mapping. The destination representation is saved only after the native Session is confirmed, and normalized adoption also waits for accepted observation recovery. The source record and its auxiliary metadata remain intact. Conflicts, unreadable or damaged records, unsupported or failed restore, workspace or visibility mismatch, and a returned native identity that differs from the mapping stop input admission. Unresolved Copilot background execution state blocks normalized adoption. Only a first input with no mapping in either store creates a Session.
Keep the same Host state root, native home directories, and compatible provider executable settings when switching modes. The legacy adapters also use NATIVE_PROVIDER_COMMAND / nativeProviderCommand for read-only inspection of a normalized mapping; keep this pointed at the same compatible native CLI used in normalized mode. A mismatched or unsupported executable reports a restore error without replacing the saved identity.
Native history comes from resume; switching modes does not replay old prompts or execution accounting. Codex and Copilot Timeline text stays identical to native history, including old Host wrappers and external inputs. Verified delivery links only reconcile local placeholders; they do not replace or hide native message bodies. Claude retains its existing projection behavior.
Normalized Claude uses the pinned Agent SDK's matching native executable, or NATIVE_PROVIDER_COMMAND / --native-provider-command. CLAUDE_COMMAND and CLAUDE_ARGS configure only legacy ACP. If an existing configuration depends on a custom Claude ACP command, set providerOutputMode: legacy explicitly to continue using it after upgrading.
Normalized Codex resolves the native codex executable from PATH, or uses NATIVE_PROVIDER_COMMAND / --native-provider-command. CODEX_COMMAND and CODEX_ARGS configure only legacy ACP; an existing custom ACP command is not reused as a native executable.
For positional start and full start-managed, an explicit CLI flag overrides its environment variable. An omitted flag preserves the environment value. Repeating a full managed start preserves a saved Agent's output mode and native executable unless the flag or environment variable explicitly supplies a replacement. Bare start-managed <serverUrl> resumes the saved configuration and rejects Agent flags.
File-based startup owns its provider settings: start --config rejects single-agent flags, and ambient PROVIDER_OUTPUT_MODE / NATIVE_PROVIDER_COMMAND do not override YAML. Agent entries override host defaults. The same providerOutputMode and nativeProviderCommand fields work in JSON specs supplied to generate-config and apply-managed; neither command needs separate mode flags.
# agents-host.yaml
borgeeBaseUrl: https://your-borgee-server
defaults:
providerOutputMode: legacy# agents/copilot.yaml
key: copilot
name: Copilot
apiKey: bgr_xxxxxxxx
provider: copilot
providerOutputMode: normalizedThe normalized Copilot provider owns its stdio runtime and closes it with the Host lifecycle. Its available controls come from negotiated native capabilities; selecting normalized output does not enable independent native Session management for Copilot.
Normalized Codex connections
Normalized Codex uses stdio mode by default: the Host owns a native codex app-server process without requiring a shared daemon or shared trust. The mode may also be selected explicitly:
agents-host start https://your-borgee-server bgr_xxxxxxxx --provider codex --provider-output-mode normalized
agents-host start-managed https://your-borgee-server bgr_xxxxxxxx --provider codex --provider-output-mode normalized --codex-connection-mode stdioThe environment entrypoint accepts CODEX_CONNECTION_MODE=shared|stdio; local YAML and generated JSON accept codexConnectionMode: shared|stdio in host defaults or Agent entries. CLI flags override environment values, Agent entries override host defaults, and repeated managed startup preserves the saved mode unless explicitly replaced. A mode change restarts that Agent's Host instance while retaining its state root and native session mappings. --native-provider-command /absolute/path/to/codex selects the normalized executable; legacy Codex keeps its separate ACP command and ignores native connection selection.
Both connection modes preserve the configured native approval and sandbox policy. Choosing stdio changes process ownership and transport; it does not grant shared-daemon trust or impose additional permission restrictions. A configured codexSharedSocketPath / CODEX_SHARED_SOCKET_PATH is rejected with default or explicit normalized stdio. Neither socket configuration nor shared trust selects shared mode implicitly; socket-only configurations must add codexConnectionMode: shared or CODEX_CONNECTION_MODE=shared. In mixed-mode local configurations, put shared socket overrides on the shared Agents rather than in host defaults; an Agent cannot clear an inherited socket with null or an empty value.
For shared mode, prepare a daemon with a compatible installed Codex CLI before starting the Host. Select its absolute executable path explicitly; app-server daemon start is available in Codex 0.154.0:
CODEX_BIN=/absolute/path/to/codex
"$CODEX_BIN" app-server daemon start
BORGEE_BASE_URL=https://your-borgee-server.example \
BORGEE_AGENT_API_KEY=your-agent-key \
RUNTIME_PROVIDER=codex \
PROVIDER_OUTPUT_MODE=normalized \
CODEX_CONNECTION_MODE=shared \
CODEX_TRUST_SHARED=true \
node packages/agents-host/dist/index.jsUse the same CODEX_HOME as the local native clients. The default control socket is $CODEX_HOME/app-server-control/app-server-control.sock, with ~/.codex as the native home fallback. Set CODEX_SHARED_SOCKET_PATH to an absolute socket path when using another daemon. Local YAML accepts providerOutputMode: normalized, codexConnectionMode: shared, codexTrustShared: true, and codexSharedSocketPath in defaults or the Agent entry.
Explicit New allocates an exclusive yyMMdd/HHmmss${rand} directory beneath sessionWorkspaceRoot, using the Host machine's local time. Configure this value in local YAML defaults or the Agent entry, or set SESSION_WORKSPACE_ROOT for the environment entrypoint. Discovery is not restricted to this root. Management requires Agent Owner authority, and mutations require a writable channel; independent Sessions never replace the ordinary Messages default. Ownership claims use the existing atomic file-store implementation under independent-sessions/codex.json. Only committed claims survive restart; a native creation whose claim cannot be saved stays unconfirmed. Reopening a Session does not create a fallback identity or submit an initial message, and successful admission alone does not guarantee empty-session persistence across native daemon restart.
Trust grants access under the daemon's existing native approval and sandbox policy. Without explicit local trust the Host rejects shared mode; it never falls back to stdio. Closing a shared attachment leaves the daemon and other clients running. A lost shared socket suspends controls while the provider reconnects and restores the same native Session from authoritative state; uncertain inputs are not replayed. Stdio process loss stops observation and rejects pending operations without automatically replacing the process or resubmitting input. Legacy mode retains its existing process and permission behavior.
Local-config mode
Run one supervisor process from a host config file:
agents-host start --config ./agents-host.yamlValidate the same local-config files without starting agents, the supervisor, watchers, control-plane connections, or provider subprocesses:
agents-host validate --config ./agents-host.yamlvalidate supports local-config mode only and reuses the same
loadLocalConfigSnapshot() validation path as start --config.
Print the canonical default local-config layout for a root directory without writing files or starting any runtime processes:
agents-host print-layout --root ./runtime-rootThis command prints JSON only. It describes the default bootstrap layout:
- host config file:
<root>/agents-host.yaml - agents directory:
<root>/agents
It does not read an existing host config file, so it does not reflect a
custom agentsDir override from agents-host.yaml.
Materialize the canonical default local-config files under that root. Pass the secret-bearing JSON spec on standard input so it is not exposed in process arguments:
printf '%s' '{"host":{"borgeeBaseUrl":"https://borgee.example.com"},"agents":[{"key":"cp1","name":"Copilot","apiKey":"bgr_xxx","provider":"copilot"}]}' \
| agents-host generate-config --root ./runtime-root --stdingenerate-config is machine-oriented:
- it accepts one JSON spec object via
--stdin;--spec-jsonremains only as a compatibility input and exposes the JSON in process arguments - it always writes the canonical default layout from
print-layout - it publishes a complete validated generation atomically; a failed generation leaves the prior active set in place
- concurrent writers for the same managed root are serialized through a root-local lock; a writer waits up to 10 seconds, then fails clearly if the lock remains held
- it retains the active and immediately previous generations and reader-leased generations, then removes older generated generations (including superseded API keys)
- if that post-publication pruning fails, the apply remains successful and its
JSON summary includes a
warningsentry with codePRUNE_SUPERSEDED_GENERATIONS_FAILED - it uses a dedicated managed root: Unix and macOS expose
<root>/agents-host.yamland<root>/agentsas stable links to the active private generation, while Windows uses<root>/current.jsonas the stable publication point and resolves the active generation internally - it replaces stale supported agent config files as part of that atomic full-set publication, never by pruning the active set first
- it creates and normalizes the managed root and generation directories to
mode
0700, and generated host and agent config files to mode0600 - it prints a JSON summary without echoing agent API keys
Use a new empty directory as the managed root. generate-config refuses a
root whose managed links or generation storage have been replaced with
unexpected file types or targets. Existing hand-managed local-config layouts
continue to work with start --config and validate --config; they are not
converted in place by this command.
The spec shape is:
{
"host": {
"borgeeBaseUrl": "https://borgee.example.com",
"defaults": {
"claudeCommand": "claude-agent-acp",
"claudeArgs": [],
"codexCommand": "codex-acp",
"codexArgs": [],
"copilotCommand": "copilot",
"copilotArgs": ["-s", "--no-color", "--allow-all-tools", "--output-format", "text"],
"copilotSessionTtlMinutes": 2880,
"providerIdleShutdownMinutes": 10
}
},
"agents": [
{
"key": "cp1",
"name": "Copilot",
"apiKey": "bgr_xxx",
"provider": "copilot",
"enabled": true
}
]
}host.agentsDir is intentionally not accepted here. Canonical generation
always targets <root>/agents, and validation is re-run after generation using
the same local-config loader as start --config / validate --config. Start
the generated layout with agents-host start --config <root>/agents-host.yaml;
the managed loader treats that path as the stable logical entrypoint even when
Windows does not materialize a filesystem alias there.
The loader preserves that stable path for validation output and publication
watches, while pinning resolved host and agents-directory paths before reading.
Managed snapshots hold a short-lived reader lease until the read finishes, so
each snapshot uses one generation even if publication changes current or
current.json concurrently.
The supervisor loads:
- one host config file
- one agent config file per discovered file under the agents directory
- one isolated
AgentsHostper enabled agent key
Supported file types
Host and agent config files may be:
*.yaml*.yml*.json
Parsing uses the yaml package for all supported file types.
Recommended layout
packages/agents-host/
agents-host.yaml
agents/
support.yaml
triage.jsonHost config
agentsDir defaults to ./agents relative to the host config file. Absolute
paths are also allowed.
borgeeBaseUrl: https://borgee.example.com
agentsDir: ./agents
defaults:
claudeCommand: claude-agent-acp
claudeArgs: []
codexCommand: codex-acp
codexArgs: []
copilotCommand: copilot
copilotArgs:
- -s
- --no-color
- --allow-all-tools
- --output-format
- text
copilotSessionTtlMinutes: 2880
providerIdleShutdownMinutes: 10Host config fields:
borgeeBaseUrl(required)agentsDir(optional, default./agentsrelative to the host config file)defaults.claudeCommanddefaults.claudeArgsdefaults.codexCommanddefaults.codexArgsdefaults.copilotCommanddefaults.copilotArgsdefaults.copilotSessionTtlMinutesdefaults.providerIdleShutdownMinutes
Absent host defaults fall back to the same code-level defaults used by the
standalone env flow, foreground start, and start-managed.
Agent config
key: support-bot
name: Support Bot
apiKey: bgr_support_xxxxxxxx
provider: claude
enabled: trueAgent config fields:
key(required): stable identity used for diffing and reload decisionsname(required)apiKey(required)provider(required):claude,copilot, orcodex(roll back Codex only withAGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=codex-provider)enabled(optional, defaulttrue)claudeCommand/claudeArgs(optional): leave unset to use the shipped bundledclaude-agent-acppath. On that bundled pathclaudeArgsmust stay empty. Non-emptyclaudeArgsare only supported whenclaudeCommandpoints at a custom ACP adapter command.- optional overrides for
claudeCommand,claudeArgs,codexCommand,codexArgs,copilotCommand,copilotArgs,copilotSessionTtlMinutes, andproviderIdleShutdownMinutes
Array overrides are replacement, not concatenation. For example, an agent
claudeArgs value replaces the host default claudeArgs entirely.
Hot reload behavior
The supervisor watches:
- the stable host-config parent, so an atomic
currentpublication triggers a reload - the resolved host-config generation directory
- the resolved configured agents directory
Every rename / change event is treated as a reload trigger. Reloads are
serialized with initial startup and debounced by at least 300ms. Each reload
does a full rescan, validation pass, diff, and apply cycle.
Diff rules:
enabled: falseagents are excluded before diffing- new
key→ start a new isolatedAgentsHost - removed
key→ stop that agent - same
key+ same effective config → no-op - same
key+ changed effective config → stop old runner, then start replacement
Changing an agent key is treated as remove old + add new, so provider-side
session state is not preserved across the key change.
If a reload candidate is invalid (malformed host config, malformed agent config, or duplicate agent keys), the supervisor logs the error and keeps the last successfully applied snapshot running.
Conversation memory
Each Borgee channel keeps its own default provider-native Session. Normalized output uses native persistence handles and observation recovery; see Choosing legacy or normalized output for cross-mode continuity. The following runtime details apply to explicit legacy mode:
- Claude: one persistent
@agentclientprotocol/claude-agent-acpsubprocess is shared by a singleAgentsHost, with one ACP session per Borgee channel. Same-channel turns are serialized; different channels keep isolated ACP sessions. The real ACP session cwd now comes from the resolved channel working folder~/.borgee/channels/<encoded-channel-id>/workspace. Task threads only switch execution onto an explicit existingexecution.local_directory; otherwise they stay discussion-only on~/.borgee/channels/<encoded-thread-id>/scratch/workspace, and ACP permission requests are denied so filesystem and shell tools stay unavailable. The host persists the channel→ACP-session map in its state root, restores same-host continuity with ACPsession/resumeorsession/loadwhen available, preserves the mapped identity if restore fails, and injects the per-channel local context paths through Claude ACP_meta.systemPrompt.append. The bundled adapter advertises a versioned targeted task-stop extension backed by the Agent SDK'sQuery.stopTask(taskId): accepting that request moves only the selected background run tostopping, while its provider lifecycle event decides the terminal state and the parent turn plus sibling workers continue unchanged. Custom ACP commands do not receive this capability and remain fail-closed. - Codex: one persistent
@agentclientprotocol/codex-acpsubprocess is shared by a singleAgentsHost, with one ACP session per Borgee channel. When the current turn has a materialized channel context payload, Codex refreshes a same-directoryAGENTS.mdproject document inside a provider-visible per-channel projection directory and exposes that directory to Codex through ACPadditionalDirectories. The real ACP session cwd now comes from the resolved channel working folder~/.borgee/channels/<encoded-channel-id>/workspace. Task threads only switch execution onto an explicit existingexecution.local_directory; otherwise they stay discussion-only on~/.borgee/channels/<encoded-thread-id>/scratch/workspace, and ACP permission requests are denied so filesystem and shell tools stay unavailable. The host persists the channel→ACP-session map in its state root and attempts ACPsession/resumefirst when the adapter advertises it, otherwisesession/load. - Copilot: one persistent SDK-managed headless Copilot subprocess is shared by a single
AgentsHost, with one SDK session per Borgee channel. Same-channel turns are serialized; different channels keep isolated sessions. The real session cwd comes from the resolved channel working folder~/.borgee/channels/<encoded-channel-id>/workspaceby default. Task threads only switch execution onto an explicit existingexecution.local_directory; otherwise they stay discussion-only on~/.borgee/channels/<encoded-thread-id>/scratch/workspace, and SDK permission requests are denied so filesystem and shell tools stay unavailable. The host persists the channel→session map in its state root and restores same-host continuity through SDK session resume. A replacement human prompt uses the SDK's foreground-only interrupt and keeps background work alive; explicit stop interrupts the foreground, cancels background agents, and cancels active background shell tasks in that session.
We keep no transcript or history store of our own — only the native session id each provider should try to reopen next time. The transcripts themselves live in each CLI's own on-disk session store: ~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl for Claude, ~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-<timestamp>-<threadId>.jsonl for Codex, and ~/.copilot/session-state/<guid>/ for Copilot. Because those stores are on disk rather than in the adapter process, a session can be reopened later by a different process on the same machine — which is what makes conversation memory survive both a host restart and the idle recycle below. Continuity is same-machine and same-$HOME only: another machine, or a different home directory, has no session to reopen.
Idle adapter shutdown
In legacy mode, each of those subprocesses is shut down once every channel it hosts has been idle for PROVIDER_IDLE_SHUTDOWN_MINUTES (default 10), and a fresh one is spawned lazily on the next turn. "Idle" means the host has no active turn, no queued turn, no provider session start in flight, and nothing still being processed for any channel of that agent; a turn that arrives mid-shutdown waits for the teardown and then starts the replacement rather than failing. Only the provider subprocess is recycled — the agent stays connected to Borgee, the loopback gateway keeps running, and the agents-host process itself is untouched.
Conversation memory normally survives the recycle. Before the process goes away the host closes each live provider session, and on the next turn it reopens the persisted session id against the CLI's on-disk store — Claude and Codex through ACP session/resume, Copilot through SDK session resume. This is the same restore path a host restart takes, and its ordinary outcome is that the model still remembers the conversation.
The Host preserves the Claude, Codex, or Copilot mapping when restore fails and reports the failure without sending the input or creating a replacement Session. Re-materialized bootstrap context is not a substitute for native transcript continuity. Restore can be blocked by the following conditions:
- A different machine or a different
$HOME. The session store is local, so continuity never crosses hosts. - A stale Copilot session lock. The Copilot CLI does not reliably unlink its
inuse.<pid>.lockon an unclean exit (github/copilot-cli#3255, still open), so a force-killed process can leave a lock that makes the next restore believe the session is still in use. The idle teardown mitigates this: it gives the CLI a 5-second SIGTERM grace before any SIGKILL — twenty times the grace a fatal teardown uses — precisely so the CLI has time to unlink that lock. This is an internal default, not a knob. - A very large Copilot transcript. Long-running channels can hit upstream resume bugs: slow or memory-hungry resume (#4251), a session that becomes unloadable once
events.jsonloutgrows the V8 maximum string length (#4325), and a resume-triggered compaction hang (#4138). A Copilot resume may also reset the model back to the default (#4397). - Our own mapping rules, which are stricter than the CLI's. We only reuse a persisted session id when the session's working directory and the set of directories exposed to it still match. A changed working folder or provider-visible directory set rejects reuse while retaining the original mapping; it does not allocate a new native identity.
If you would rather keep the adapter process resident than take any of that, set PROVIDER_IDLE_SHUTDOWN_MINUTES=0, raise the window, or roll the behavior back entirely with AGENTS_HOST_INTERNAL_DISABLED_COMPATIBILITY_GATES=provider-idle-shutdown.
Checking the installed version and updating
agents-host version (or agents-host --version) prints the installed package version on stdout and exits successfully. It takes no arguments, works for global installs, project dependencies, and source checkouts, and reads only local package metadata without contacting a registry or starting the runtime.
agents-host versionagents-host update reinstalls the published package:
agents-host updateIt takes no options. The latest published release is resolved with npm view,
and the package is reinstalled with the manager that owns the current global
install (npm install -g, pnpm add -g, yarn global add, or bun add -g).
When the installed version is already at or ahead of the latest release it says
so and does nothing.
The command only acts on a global install. It refuses a source checkout
(update that with git) and a project-local dependency (update it through the
consuming project's manifest); rewriting either behind their owner's back is not
this command's call. Which of the three it is comes from asking each installed
manager where it puts global installs — npm root -g, pnpm root -g,
yarn global dir, bun pm bin -g — and seeing which answer contains this
package, so relocated roots (YARN_GLOBAL_FOLDER, BUN_INSTALL, Windows
%LOCALAPPDATA%\Yarn\Data) and pnpm's virtual store all classify correctly.
npm has to be on PATH for the version lookup even when another manager owns
the install; it ships with Node, and all four managers read the same registry.
Going through npm rather than querying the registry directly is what makes
.npmrc discovery, scoped auth tokens, proxies, custom CAs, and retries work,
so private and mirrored registries need no extra configuration here.
Startup update check
Every user-facing startup path — the env-var entrypoint, start, start --config,
and both start-managed forms — checks for a newer published release first and
prints an advisory notice on stderr:
[agents-host] update available: 0.2.31 -> 0.3.0
[agents-host] run `agents-host update` to install itThe check cannot hold startup back:
- source checkouts are skipped from the package path alone, so
pnpm devnever nags and never spawns a package manager - the answer is cached for six hours in
~/.borgee/agents-host/update-check.json(mode0600), so repeatedstart-managedupserts do not re-query the registry - a failed lookup is cached the same way, so an unreachable registry costs the deadline once per six hours rather than on every start
- the lookup owns a five-second deadline and kills the
npmchild when it passes, which bounds the wait even when the hang is inside DNS resolution - which manager owns the install is only asked once there is a notice to word, so a start with nothing to report never pays for it
- every failure — offline machine, unreachable registry, unreadable cache — is
silent;
AGENTS_HOST_DEBUG=1prints the reason AGENTS_HOST_DISABLE_UPDATE_CHECK=1turns the check off- machine-oriented commands (
describe-managed,apply-managed,describe,print-layout,generate-config,log) never run it
A cached start still runs one local npm config get registry, measured at about
100ms, because the cached answer is only valid for the registry it came from and
npm is the only thing that knows which registry .npmrc resolves to.
Testing
pnpm --filter @borgee/agents-host exec vitest run --testTimeout=10000
pnpm --filter @borgee/agents-host typecheck
pnpm --filter @borgee/agents-host build