car-server-core 0.55.0

Transport-neutral library for the CAR daemon JSON-RPC dispatcher (used by car-server and tokhn-daemon)
Documentation

car-server-core

Transport-neutral library for the Common Agent Runtime JSON-RPC dispatcher.

What it does

Holds the JSON-RPC dispatcher, per-client session state, and the WebSocket channel plumbing. Used by car-server (the standalone WebSocket binary) and by any future embedder that wants to expose CAR's protocol over a different transport (e.g. tokhn-daemon).

The standalone car-server binary is a thin wrapper that loads ~/.car/env, initializes telemetry, spawns the dream loop, binds a TCP listener, and on each connection calls run_dispatch.

The package also builds the developer-only car-wire-schema generator. It writes docs/wire-schema.json and docs/wire-schema.sha256 from the very Rust types the daemon serializes at each covered boundary — the handshake reply, the tool list, the agent rows, the inference result and the journal envelope are all generated from the value the handler returns, so a field added to one of those replies changes the published schema and its digest. Release binaries embed those exact bytes and expose them through server.schema; regenerate with bash scripts/build-wire-schema.sh, never by editing either artifact.

The digested bytes carry no release version. server.schema reports car_version from the running binary instead, so the digest tracks wire shape alone and the release bump commit — which rewrites the workspace version without regenerating this pair — cannot leave the artifact naming the previous release.

Library boundary contract

This library MUST NOT:

  • spawn the dream loop (caller decides),
  • initialize telemetry (caller decides),
  • load ~/.car/env (caller decides).

Those bootstraps stay in the embedder's main. The contract prevents the dual-memgine bug: if the library silently spawned its own dream loop, embedded users would end up with two memgine engines (the embedder's plus the library's).

The same boundary applies to watch-only self-healing detection: ServerState owns the shared detector/ledger service and exposes spawn_selfheal_cadence, but only the standalone car-server starts that task. Embedders may start it explicitly or call selfheal.run. The default ledger is <CAR_HOME>/selfheal/detections.jsonl; it is private and append-only. Each tick also checks only its configured/bounded source candidates for a .git origin matching Parslee-ai/car. A match creates or updates a trusted-tier handoff at <CAR_HOME>/selfheal/issues/<dedup-key>.md; absent or refused candidates stay ledger-only, and no path writes into the checkout. This subsystem has no network, off-machine filing, proposal execution, restart, or remediation path.

Lock primitive

ClientSession.memgine uses Arc<tokio::sync::Mutex<MemgineEngine>> per the "one-wrapper rule" — dispatcher handlers can hold the lock across .await points without risking poisoning, and tokio's Mutex does not poison so a panicking handler does not poison the engine for sibling connections.

Embedder usage

use car_server_core::{ServerState, ServerStateConfig, run_dispatch};

// Build the shared server state.
let state = ServerState::embedded(ServerStateConfig::default()).await?;

// On each accepted WebSocket connection:
tokio::spawn(async move {
    run_dispatch(ws, state.clone()).await;
});

The assistant (car-server-core::assistant)

The flagship batteries-included general agent behind car do — one-shot, REPL, or --serve conversational.

  • A GeneralExecutor (generalizes coder::WorktreeExecutor to any bound Substrate, sharing one bounded run_shell_on) exposing files, a real shell, http_request / web_search, calculate, durable remember / recall (memgine, ~/.car/memory/assistant.json), and macOS-native productivity tools: local EventKit event reads plus approval-gated event mutations, and local Mail.app inbox/search/body reads plus approval-gated drafts and sends. Read-tool descriptions prompt a concrete follow-up offer after guided Calendar and Mail checks. These local paths do not require Parslee sign-in or a connected Microsoft 365 account; m365_task remains the separate cloud path for connected work accounts and broader M365 tasks.
  • Driven through a full Runtime — validator, policy, permission tiers, event log. Sandbox-first (car-sandbox, --network none), with --local / --full-access to widen it.
  • Conversational via the native agent.chat harness (chat::AssistantService, the first native Rust agent-side). Auto-registered in CarHost as parslee-core (car-assistant remains a server-side alias for already-registered installs and raw WS integrators — see assistant::register).
  • Tool descriptions are the single source of truth for what a tool does; the system prompt (assistant::prompt) carries only posture and the rules no def or policy expresses, and never re-enumerates the toolset.
  • A per-turn <runtime-state> block carries live run state — the todo_write task list and the subjects of facts written this run — so state reaches the model whether or not it thinks to recall. It is appended to the last message's content rather than added as a trailing Message::System, because the Anthropic and Gemini handlers fold System messages into the top-level system field and a trailing one would land in the cached prefix and be rewritten every turn. It never enters durable history, and renders nothing when there is nothing to say. Run constants — working directory, approval tier — belong in the system prompt's Environment: sentence instead, which compaction pins; putting them here would re-send them every turn and invite drift from the substrate's own description.

What the assistant learns

Three mechanisms, and they are not the same thing — conflating them is how an agent gets described as "learning" when it only remembers.

  1. Durable facts about the userremember / recall (assistant::memory), model-initiated, persisted to ~/.car/memory/assistant.json and re-ingested at startup. Recall runs memgine's full retrieval path, not Fast: skill lookup, PPR fact scoring, inline repair, known-unknowns. On car do --serve it mirrors into the synced Knowledge oplog so facts converge across devices.
  2. Reactive trajectory pressure — the per-turn proactive-memory pass (agent_loop::maybe_apply_assistant_proactive_memory), host-side and deterministic. It folds the last 32 events of the durable receipt journal into proactive facts and may inject one reminder before the next model turn, so high-value memory reaches the model whether or not it thinks to recall. Its derived facts live in the in-memory graph only — they are recomputed from the journal each process rather than accumulating, which is why the journal being durable is what makes this cross-session at all.
  3. Learned tool repairsassistant::tool_memory, the "gets better over time" half and the one that was missing. A tool fails, the same tool succeeds within RECOVERY_WINDOW_TURNS, and the call that recovered is captured against a coarse failure signature (shell::missing_target). A later run hitting that signature gets it back as a ## Learned Repairs hint on the turn after the failure. Repairs are memgine skills, so a lead that keeps not working auto-degrades and stops being offered; they persist in their own file (~/.car/memory/assistant-repairs.json), deliberately NOT in the note store, whose format is shared with the MCP server and whose contents the user's own recall reads. Off unless a surface sets AssistantConfig::tool_memory; car-bench leaves it unset on purpose.

What is still not wired on this path, so nobody has to re-derive it: MemgineEngine::reflect() (user corrections → high-confidence facts) has no assistant call site — it needs inference attached to an engine that deliberately has none — and the self-evolution governor operates on the daemon's shared session engine, not this one.

Verified goals

Live surfaces: car do --until, car do --infer-until, agents.chat { goal }, goal.suggest, goal.set / status / clear, declarative-agent goal { check, max_iterations }, and goal_evaluated events. (The macOS Chat verified-goal strip and iOS push-to-talk both ride this path for chat-capable agents.)

The goal loop requires met && grounded. Final-summary operational claims like "tests passed" are cross-checked against same-run tool receipts: an unmatched claim fails a model-judge-dependent verdict closed, while on a deterministic pass it only annotates the reply with a [claim check] note — completion is decided by the receipts, not the prose.

Declarative agents

They advertise capabilities: ["chat"] and are reachable through agents.chat in-daemon, with no spawned child process. Host agents.chat* is WS-only; agent-side FFI exists via registerChatHandler / register_chat_handler and chatEvent / chat_event.

Provisioned coder benchmark

The deterministic native scorecard runs in ordinary tests. Comparing it with a real external foreman spends CLI quota and therefore requires both an exact opt-in and a pinned ready engine:

CAR_CODER_BENCH_LIVE=1 CAR_CODER_BENCH_ENGINE=codex \
  cargo test -p car-server-core coder::bench::foreman_vs_native_live \
  -- --ignored --exact --nocapture

The task table and thresholds are fixed in source. The lane fails when the pinned engine is unavailable or the threshold is missed and emits a car.coder.foreman-bench.v1 JSON receipt.

Where it fits

The full method reference is in docs/websocket-protocol.md — that document describes the wire format produced by this crate's dispatcher.