Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Kerux

Kerux (Greek: κῆρυξ — herald, messenger of the gods) is a fast, self-contained AI agent runtime written in Rust.

It runs a full ReAct agent loop with tool execution, streaming responses, persistent memory, and multi-platform messaging gateways (Telegram, WhatsApp) — all in a single static binary with zero runtime dependencies.

Why Kerux?

  • Single binary — no Python, no Node, no runtime. cargo build and run.
  • Fast — Rust core, ~42K LOC, sub-second startup.
  • Self-contained — sessions, memory, todos, cron jobs all persist to disk as JSON.
  • Multi-platform — Telegram long-polling, WhatsApp (Baileys bridge), and Discord/Slack REST adapters built in.
  • Production features — tool approval gates, context compaction, fallback provider chains, voice STT, cron scheduling, subagent delegation.

Crate Layout

CrateDescription
kerux-coreAgent loop, LLM clients, tools, gateway adapters, persistence
kerux-cliCLI/TUI frontend, serve gateway mode, autonomous coding mode

Project Overview

Kerux is a high-performance agentic orchestration framework and terminal harness written from the ground up in Rust.

Quickstart

Build

git clone https://github.com/eikarna/hermes-rs.git
cd hermes-rs
cargo build --release

The binary lands at target/release/kerux.

Configure

Fastest path — the interactive wizard:

kerux wizard

It sniffs provider credentials from the environment, validates the API key with a live model-list call, lets you fuzzy-pick a model (with capability badges), optionally probes it and wires a fallback model, then writes the config and runs a smoke test. kerux model switches the model anytime.

Or copy the example config and fill in your provider:

mkdir -p ~/.config/kerux
cp kerux.example.toml ~/.config/kerux/config.toml

Config lookup order: --config <path> flag first, then ./kerux.toml, ./.kerux.toml, and finally ~/.config/kerux/config.toml (Unix) / %APPDATA%\kerux\config.toml (Windows).

Minimal config:

[client]
provider = "openai"
base_url = "https://api.openai.com/v1"

[agent]
model = "gpt-4o"

Or use environment variables directly:

export KERUX_PROVIDER=openai
export KERUX_MODEL=gpt-4o
export OPENAI_API_KEY=***

API keys resolve from [client] api_key, the OPENAI_API_KEY environment variable, or an OAuth profile (kerux auth login nous, referenced via [client] auth_ref). Run kerux --help for CLI overrides (--api-key, --base-url, --model, …).

Run

Interactive TUI:

kerux chat

Single-shot:

kerux run --query "explain this codebase"

Gateway mode (Telegram + WhatsApp + Discord + Slack):

kerux serve

Verify

cargo fmt --all
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings

Configuration

Kerux is TOML-first. Config resolution order:

  1. --config <path> CLI flag
  2. ./kerux.toml in the current directory
  3. ./.kerux.toml in the current directory
  4. ~/.config/kerux/config.toml (Unix) / %APPDATA%\kerux\config.toml (Windows)

If none exist, built-in defaults are used. State (sessions, memory, todos, cron jobs, run journals) lives under ~/.kerux/, relocatable with KERUX_HOME.

See kerux.example.toml for the full annotated reference.

Key Sections

[client]

LLM provider settings: provider, base_url, api_key, auth_ref, per-provider endpoint overrides ([client.openai], [client.anthropic], [client.ollama], [client.openrouter], [client.gemini]), and timeout_secs. The model itself is set under [agent] model.

[agent]

Agent behavior: model (default gpt-4), max_iterations (20), context_window (128000), stream (true), repo_map_tokens (0 = off), repo_map_max_files (500), edit_format_override, max_repair_attempts, auto_commit (false).

[taste]

Learned coding-style prompt injection: enabled (default true), min_confidence (0.5), and max_items (10). Kerux reads the project profile from .kerux/taste.json; kerux taste push <name> saves it under the KERUX_HOME-aware portable registry, while kerux taste pull <name> merges a registry profile back into the project.

[gateway]

Messaging gateway settings:

KeyDefaultDescription
telegram_enabledfalseEnable Telegram long-polling adapter
telegram_token—Bot token
discord_enabledfalseEnable Discord REST adapter (discord_token)
slack_enabledfalseEnable Slack REST adapter (slack_token)
slack_signing_secret—Verify Slack Events API signatures; required by /webhook/slack
whatsapp_enabledfalseEnable WhatsApp adapter (Baileys bridge)
whatsapp_bridge_url—Bridge endpoint (e.g. http://127.0.0.1:3000)
webhooks_enabledfalseStart the inbound HTTP listener
webhooks_addr—Listener address (e.g. 127.0.0.1:8080)
streaming_repliesfalseLive-edit token streaming with ▌ cursor
tool_approvaltrueRequire inline-keyboard approval before dangerous tool execution
tool_approval_timeout_secs300Auto-deny approval requests after this long
context_compactiontrueSummarize oldest messages near context cap
stt_model—Voice note transcription model (enables STT)

[[client.fallback]]

Fallback provider chain (default OFF): array-of-tables entries (provider, optional base_url, api_key, model, timeout_secs) tried in order when the primary provider hits transient failures (network errors, interrupted streams, 429, 5xx). Auth failures and bad requests propagate immediately. See Fallback Provider Chain for operational guidelines.

[budget]

Cost guardrails (default disabled): estimated-spend ceilings computed from the [telemetry] cost rates, enforced in the agent loop after every LLM response. enabled (false), per_run_limit (0 = off), daily_limit (0 = off), warn_threshold_pct (80), on_limit (pause | downgrade | stop), downgrade_model (required when on_limit = "downgrade"). Invalid policies fail config load. See Cost Guardrails for enforcement semantics.

[validation]

Deterministic project validators (default disabled): enabled, fail_fast, plus [[validation.validators]] entries (name, command, required, timeout_secs). Executed by the validation engine with outcomes journaled as evidence.

[recorder]

Flight recorder policy: enabled, record_content, record_reasoning, max_payload_bytes, failure_mode (warn | fail). Journals every agent run to a hash-chained store under ~/.kerux/runs/.

[autonomous]

Autonomous coding mode: todo_path (default TODO.md), status_path (default autonomous-status.toml), test_command (default cargo test --workspace), interval_secs (300), git_remote (origin), git_branch (agent-dev), command_timeout_secs (900), max_failures_per_state (3).

Environment Variables

Selected fields have env overrides applied after the TOML is parsed:

VariableOverrides
KERUX_PROVIDER / OPENAI_BASE_URL[client] provider / base_url
OPENAI_API_KEY[client] api_key
KERUX_AUTH_REF[client] auth_ref
KERUX_MODEL / KERUX_STREAM[agent] model / stream
KERUX_MAX_ITERATIONS / KERUX_MAX_HEALING_ATTEMPTS[agent] iteration/healing knobs
KERUX_TOOL_TIMEOUT / KERUX_REQUEST_TIMEOUT / KERUX_CONTEXT_WINDOW[agent] timeout/window knobs
KERUX_SYSTEM_PROMPT[agent] system_prompt
KERUX_AUTONOMOUS_* (INTERVAL, TODO, STATUS, TEST_COMMAND, GIT_REMOTE, GIT_BRANCH, COMMIT_MESSAGE, COMMAND_TIMEOUT, MAX_FAILURES)[autonomous] fields
KERUX_LOG_LEVEL[logging] level
KERUX_SKILLS_DIR[skills] root_dir

Not env-overridable: everything else (edit kerux.toml). KERUX_HOME relocates the state root (~/.kerux by default) but is not a config-file search path.

Architecture Overview

┌─────────────────────────────────────────────────────┐
│                    kerux-cli                         │
│  TUI (ratatui) │ serve (gateway) │ autonomous mode  │
└────────────────────────┬────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────┐
│                   kerux-core                         │
│                                                      │
│  ┌──────────┐  ┌──────────┐  ┌───────────────────┐  │
│  │  Agent   │  │  Client  │  │      Gateway      │  │
│  │  (ReAct) │  │  (LLM)   │  │ Telegram│WhatsApp │  │
│  └────┬─────┘  └──────────┘  └───────────────────┘  │
│       │                                              │
│  ┌────▼─────────────────────────────────────────┐   │
│  │  Tools: file, patch, terminal, code_exec,    │   │
│  │  web, memory, todo, sub_agent, mcp, skills   │   │
│  └──────────────────────────────────────────────┘   │
│                                                      │
│  ┌──────────────────────────────────────────────┐   │
│  │  Persistence: sessions, memory, todos, cron  │   │
│  │  (~/.kerux/ — atomic JSON writes)            │   │
│  └──────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────┘

Core Subsystems

ModuleResponsibility
agent.rsReAct loop, streaming, cooperative cancellation, approval gate, context compaction
client.rsLLM provider abstraction (OpenAI-compatible, Anthropic, Gemini), fallback chain
gateway.rsPlatform adapters (Telegram long-polling, WhatsApp bridge), markdown conversion, message chunking
session_store.rsPer-channel conversation persistence (format v2 with summary)
persist.rsShared atomic JSON write helpers (~/.kerux/)
approval.rsTool approval gate (inline keyboard via Telegram callback_query)
scheduler.rsCron-style job scheduler with disk persistence
tools/Built-in tool implementations
platform.rsOS paths (kerux_home(), config/data/sessions dirs)

Agent Loop

The core execution engine lives in kerux-core/src/agent.rs.

ReAct Cycle

  1. Build context — system prompt + conversation history (+ [CONTEXT SUMMARY] if compaction active)
  2. Call LLM — streaming or non-streaming via the configured provider
  3. Parse response — text chunks, reasoning, tool calls (tolerant parsing)
  4. Execute tools — with optional approval gate; results appended as tool messages
  5. Loop — repeat until the model produces a final text response with no tool calls

Cooperative Cancellation

Every iteration checks an Arc<AtomicBool> cancel flag — at loop boundaries, between SSE chunks, and before each tool execution. On cancel, repair_conversation_after_cancel() fixes any dangling assistant/tool message pairs so the conversation stays valid.

Event Emission

The agent emits events (ToolStart, ToolEnd, TextChunk, RunProgress) via a bounded channel using non-blocking try_send() — a slow or dead event pump can never deadlock the ReAct loop.

Context Compaction

When the conversation approaches the session cap, compact_history() summarizes the oldest messages via a one-shot LLM chat. The summary is stored in the session file (format v2) and injected as a [CONTEXT SUMMARY] marker, keeping recent messages intact.

Gateway & Adapters

The gateway (kerux-core/src/gateway.rs) connects the agent to messaging platforms. Started via kerux serve.

Design

  • Mixed transports — Telegram uses long-polling while Slack and external automation enter through an optional axum HTTP listener.
  • Parallel adapters — each adapter runs in its own tokio::spawn task so one platform’s polling never starves another’s.
  • Interrupt-on-new-message — an incoming message cancels the active run for that channel and starts a fresh one.

Adapters

AdapterTransportEnable
TelegramLong-polling (getUpdates)[gateway] telegram_enabled + telegram_token
WhatsAppBaileys HTTP bridge (drain-queue polling)[gateway] whatsapp_enabled + whatsapp_bridge_url
DiscordREST API (discord_api_base, default https://discord.com/api/v10): token verification via /users/@me, send via /channels/{id}/messages, message-create event parsing[gateway] discord_enabled + discord_token
SlackEvents API inbound webhook + REST replies[gateway] slack_enabled + slack_token + slack_signing_secret + webhooks

Discord remains a minimal REST integration without a websocket gateway connection. Slack Events API requests are accepted at /webhook/slack, verified using Slack’s HMAC-SHA256 signature and five-minute replay window, then routed through the Slack adapter for replies.

Inbound HTTP webhooks

Set webhooks_enabled = true and webhooks_addr = "127.0.0.1:8080" under [gateway], then run kerux serve.

EndpointMethodPurpose
/healthGETListener readiness check
/webhook or /webhook/genericPOSTGeneric external trigger
/webhook/slackPOSTSlack Events API challenge and event callbacks

Generic triggers accept JSON:

{
  "message": "Inspect the latest CI failure",
  "source": "ci",
  "target": "telegram:12345",
  "metadata": { "build": 42 }
}

message is required. source becomes the synthetic sender ID. target is optional; when present it must use platform:channel format and routes progress/final output through that registered platform adapter. Without target, the run is fire-and-forget. The listener returns 202 Accepted after queueing a valid trigger.

Shared UX

  • Markdown conversion per platform: MarkdownV2 for Telegram (stdlib converter: special-char escaping, fenced code blocks, --- → Unicode separator, tables → bullet lists, blockquotes), a dedicated WhatsApp converter (markdown_to_whatsapp)
  • Message chunking at 3500 chars on line boundaries with code-fence tracking
  • Live message editing for status updates (🤔 Thinking..., 🔧 Tool, heartbeats)
  • Two reply modes: normal (final edit) or streaming (streaming_replies = true, token streaming with ▌ cursor, ~900ms throttle)
  • Inline-keyboard tool approval (Telegram) via callback_query
  • Voice note STT via getFile → download → /v1/audio/transcriptions
  • Explicit HTTP timeouts everywhere (connect 5s; read 30–45s) — no half-open hangs

Persistence

All channel state survives restarts:

DataLocation
Conversations~/.kerux/sessions/<platform>_<channel>.json (format v2)
Memory~/.kerux/memory/memories.json
Todos~/.kerux/todos/todos.json
Cron jobs~/.kerux/scheduler.json

All writes are atomic (temp file + rename). Set KERUX_HOME to relocate the state root.

OAuth and provider authentication design

Goal

Add provider authentication that can eventually support official browser login flows while keeping the current OpenAI-compatible API-key path intact.

This is a design checkpoint, not a runtime behavior change.

Provider reality check

  • OpenAI: public OpenAI-compatible API access continues to support API keys. OpenAI also documents ChatGPT/Codex auth for Codex clients, including browser login, device/headless login, access-token injection, and auth-cache reuse. Kerux should model this as a separate OpenAI/Codex account-auth capability, not silently treat Codex tokens as generic OpenAI API keys.
  • Google: Gemini supports API keys and OAuth/Application Default Credentials. Direct desktop OAuth requires a Google OAuth client ID; ADC via gcloud auth application-default login keeps token creation, refresh, and storage outside Kerux.
  • GitHub Copilot: official Copilot CLI authentication supports OAuth device flow, supported GitHub token types (COPILOT_GITHUB_TOKEN, GH_TOKEN, GITHUB_TOKEN), OS keychain storage, and GitHub CLI fallback. Kerux should reference external tokens first and only run Copilot login after provider-specific client behavior is defined.
  • Anthropic: Claude access is not one single OAuth path. Documented routes include Claude.ai / Claude Code account login, Anthropic Console API keys, Team/Enterprise accounts, and cloud-provider routes such as Google Vertex AI, Amazon Bedrock, and Microsoft Foundry. Kerux should keep these as separate capabilities because Vertex/Bedrock/Foundry require provider-specific request/auth behavior, not just a bearer token swap.
  • OpenCode comparison: OpenCode stores provider credentials outside project config and exposes /connect flows. Kerux should copy the credential separation pattern, not vendor-private auth internals.

1. Keep project config non-secret

kerux.toml should continue to describe provider selection, base URLs, and model defaults. It should not become the default storage location for OAuth access tokens or refresh tokens.

Recommended future config shape:

[client]
provider = "openai-compatible"
base_url = "https://api.openai.com/v1"
auth_ref = "openai-default"

auth_ref points to an entry in local credential storage.

2. Store credential metadata locally; store secrets safely

Recommended metadata path:

  • Windows: %APPDATA%/kerux/auth.json
  • macOS: ~/Library/Application Support/kerux/auth.json
  • Linux: ~/.local/share/kerux/auth.json or config-dir equivalent from existing platform helpers

Rules:

  • Never write tokens to repo-local files.
  • Never log token values.
  • Bind credentials to the endpoint stored in the auth profile, and reject repo-local base URL overrides when an auth_ref is active.
  • Require explicit base URLs for non-OpenAI profiles until provider-specific clients own their official endpoints.
  • Prefer OS credential storage for long-lived secrets and refresh tokens.
  • Recommended implementation: use platform credential storage (Windows Credential Manager, macOS Keychain, Linux Secret Service/libsecret) behind a small Kerux abstraction before persisting OAuth refresh tokens. Until that exists, keep tokens in environment variables or provider-managed stores such as Google ADC / GitHub CLI or Copilot CLI keychain.
  • If OS credential storage is not implemented yet, keep long-lived secrets in environment variables or explicit config only; do not silently migrate them into plaintext JSON.
  • If a plaintext fallback is ever added, it must be opt-in, clearly warned, and protected by best-effort owner-only file permissions.
  • Store non-secret provider id, auth type, created/updated timestamps, expiry, and refresh metadata in auth.json.

Sketch:

{
  "version": 1,
  "profiles": {
    "openai-default": {
      "provider": "openai",
      "method": "api_key",
      "base_url": "https://api.openai.com/v1",
      "secret_ref": "env:OPENAI_API_KEY"
    },
    "google-default": {
      "provider": "google-gemini",
      "method": "oauth_pkce",
      "scopes": ["provider-documented scopes for this flow"],
      "expires_at": "2026-01-01T00:00:00Z"
    }
  }
}

3. Add an auth provider boundary

Introduce a small internal provider-auth abstraction before adding provider-specific flows.

#![allow(unused)]
fn main() {
trait AuthProvider {
    fn id(&self) -> &'static str;
    fn supported_methods(&self) -> &'static [AuthMethod];
    async fn resolve_headers(&self, profile: &AuthProfile) -> Result<HeaderMap>;
}
}

Initial implementations should be minimal:

  1. ApiKeyAuthProvider for the current OpenAI-compatible behavior.
  2. BearerTokenAuthProvider for official OAuth/ADC access tokens where the provider accepts bearer tokens.

Provider-specific request formats should stay separate from auth. Kerux currently has an OpenAI-compatible client; OAuth should not imply that every provider can use /v1/chat/completions.

4. CLI/TUI flows

Future commands:

  • kerux auth login <provider>
  • kerux auth set-api-key <provider>
  • kerux auth set-bearer-token <provider> --env <ENV_VAR> --base-url <URL>
  • kerux auth providers
  • kerux auth list
  • kerux auth logout <auth-ref>
  • TUI command/modal equivalent after CLI flow is stable

Login flow order:

  1. Prefer API key where it is the official provider API path.
  2. Prefer provider-managed credentials first: Google ADC, GitHub/Copilot CLI keychain, Claude Code setup wizards, AWS/GCP/Foundry credential chains.
  3. Offer OAuth only for providers with documented third-party, device-code, or ADC flows.
  4. Use loopback PKCE for native desktop OAuth where supported.
  5. Support no-browser mode only through provider-documented flows such as device-code auth or external tools like gcloud auth application-default login --no-browser; do not invent copy/paste auth-code handling.

5. OAuth implementation constraints

Do not add OAuth until these are decided:

  • Token storage format and permission model.
  • Provider allowlist and scopes.
  • Refresh behavior and expiry handling.
  • How non-OpenAI-compatible providers map into OpenAIClient or a new client abstraction.
  • How product-specific account auth maps to runtime endpoints, especially OpenAI Codex/ChatGPT auth and Anthropic Claude account auth.
  • Whether adding OAuth crates is acceptable, or whether to implement PKCE/loopback using existing dependencies.
  • Whether the project will use OS credential storage crates or keep OAuth behind external helper tools until secure storage exists.

Security requirements:

  • Use PKCE for public/native clients.
  • Bind redirect to loopback only (127.0.0.1), random port.
  • Validate state and provider issuer/token endpoint.
  • Loopback callbacks must accept only authorization codes plus validated state; never accept access tokens from query strings.
  • Redact auth headers in logs.

Phased implementation plan

Phase 1: auth profiles, no OAuth

  • Add local auth metadata store module.
  • Add kerux auth set-api-key <provider> to create a profile that references an environment variable or explicitly configured key source; do not silently persist the secret itself.
  • Move current API-key resolution behind auth profile lookup while preserving env/config behavior and precedence.
  • Add kerux auth list and kerux auth logout.
  • Tests: redacted list output, env precedence, missing-secret error, permission best-effort for metadata file.

Phase 2: Google OAuth / ADC-compatible bearer auth

  • Add Google as the first official OAuth-capable provider.
  • Support existing ADC token discovery or explicit token helper before implementing full browser flow.
  • Use provider-documented scopes per Gemini API vs Vertex AI flow; do not hardcode a single scope globally.
  • Tests: expired token rejection/refresh boundary with mocked token provider.

Implemented Phase 2a:

  • kerux auth set-bearer-token <provider> --env <ENV_VAR> --base-url <URL> stores metadata for externally managed OAuth/ADC bearer tokens.
  • Kerux still does not run browser OAuth or refresh tokens itself.
  • Bearer credentials use the same endpoint binding protections as API-key profiles.

Implemented Phase 2b:

  • kerux auth providers reports provider aliases, documented auth methods, Kerux-supported environment sources, and implementation notes for Google, GitHub Copilot, OpenAI, and Anthropic.
  • OpenAI Codex/ChatGPT auth and Anthropic Claude account/cloud-provider auth are documented as distinct capabilities instead of being collapsed into generic API-key or bearer-token auth.
  • kerux auth login <provider> prints provider-specific external setup guidance and intentionally fails without creating credentials until secure token storage and provider-specific runtime clients are available.

Phase 3: browser PKCE flow

  • Add loopback OAuth helper.
  • Add no-browser flow only for providers with a documented device-code or external-tool path.
  • Tests: state validation, callback parsing, token exchange mock server, cleanup of local listener.

Implemented Phase 3a:

  • Added provider-neutral PKCE/state helpers.
  • Authorization URLs require http://127.0.0.1:<port>/... loopback redirects.
  • Callback parsing accepts only authorization codes with matching state and rejects access tokens in query strings or fragments.
  • Added a loopback callback receiver that binds only to 127.0.0.1 on a random local port and accepts one GET callback.
  • Added provider-neutral authorization-code token exchange helper for PKCE flows. Token endpoints must use HTTPS; tests use loopback HTTP only through private test plumbing.
  • Kerux still does not launch browsers or refresh/store OAuth tokens itself.

Phase 4: provider-specific clients

  • Add provider client abstraction only when the first non-OpenAI-compatible provider needs it.
  • Keep OpenAI-compatible behavior unchanged.

Non-goals for the first OAuth PR

  • Reverse-engineered ChatGPT/Codex login beyond documented OpenAI flows.
  • Reusing Claude Code private credential formats without documented support.
  • Storing tokens in repo-local kerux.toml.
  • Supporting every provider in one change.

Tool Approval

Interactive approval gate for tool execution via Telegram inline keyboards.

How It Works

  1. Agent wants to execute a tool
  2. Gateway sends a prompt with [✅ Approve] [❌ Deny] inline buttons
  3. User taps a button → Telegram sends a callback_query
  4. The query is routed to a per-tool-call oneshot channel
  5. Agent proceeds (approve) or skips with a denial message (deny)
  6. Timeout → treated as denial

Config

[gateway]
tool_approval = true

Implementation

  • kerux-core/src/approval.rs — approval gate manager
  • callback_query handling in TelegramAdapter::poll_updates
  • answerCallbackQuery ack so the button press registers in the Telegram UI

Flight Recorder & Proof Capsules

Kerux can journal every agent run into a tamper-evident local flight recorder: a hash-chained event log you can inspect, verify, and export as a portable, offline-verifiable proof capsule.

The recorder answers one question after any incident: what exactly did the agent do, in what order, and can I prove this log was not edited?


Enabling the recorder

The recorder is on by default. Tune it in your config:

[recorder]
enabled = true                  # default; set false to disable journaling
# max_payload_bytes = 65536     # per-event payload cap (redacted, bounded)
# record_content = true         # default; assistant/tool bodies (still redacted)
# record_reasoning = false      # default; reasoning bodies: metadata only
# failure_mode = "warn"         # "warn" = log & continue, "fail" = abort run

With failure_mode = "warn" a journal I/O problem never breaks your session; the run continues and the incident is logged. Use "fail" when an unrecorded run is unacceptable (e.g. compliance contexts).

What gets recorded

Each run creates a directory under $KERUX_HOME/runs/<run_id>/ containing a versioned manifest and an append-only NDJSON event stream. Every event carries a SHA-256 hash chained to its predecessor, so any edit, deletion, or truncation breaks verification.

Main event vocabulary:

EventMeaning
run_started / run_completed / run_cancelled / run_failedRun lifecycle, with exactly one terminal status
request_preparedFull request provenance: model, provider, context composition
tool_started / tool_completed / tool_failedTool execution timeline, correlated by call id
approval_decisionHuman/tool-approval decisions with redacted reasons
edit_outcomeEdit protocol outcome: format, parse/apply status, pass kind (first_pass vs repair_pass), repair counts, effective routing format
validator_resultProject-validator evidence: command digest, exit code, bounded/redacted output

Git checkpoint metadata (repository HEAD, dirty-tree patch hash) is attached to the manifest at snapshot points.

All payloads are redacted and size-bounded before they touch disk, independently of the [recorder] caps. Raw provider reasoning bodies are only stored if record_reasoning = true; otherwise just metadata.

Inspecting runs — read-only by construction

kerux runs commands never execute anything. They open journals through a strictly read-only reader that verifies the hash chain while parsing and detects a crash-truncated tail instead of failing mysteriously.

kerux runs list                 # newest first, human-readable
kerux runs list --json          # machine-readable, no ANSI, ever
kerux runs inspect <run_id>     # manifest + full event timeline
kerux runs verify <run_id>      # re-verify the chain, modify nothing

Example --json shapes (abbreviated):

{ "ok": true, "runs_root": "/home/me/.kerux/runs", "runs": [
  { "run_id": "01J…", "status": "completed", "events": 42,
    "model": "…", "provider_kind": "openai", "surface": "cli",
    "replayability": "…", "tail": "complete" } ] }

Failed commands emit stable machine-readable reason codes — run_not_found, corrupt_event_line, chain_verification_failed, incomplete_tail, … — so scripts can branch on them without parsing prose.

Proof capsules — the shareable form

A journal is local, full-fidelity, and private. To share evidence, export a capsule:

kerux runs export <run_id>                 # writes <run_id>.capsule.html
kerux runs export <run_id> --out proof.html --json

Export verifies the source chain first and refuses to produce a capsule from a broken journal. The capsule is a scrubbed re-chain:

  • home-directory paths replaced with ~,
  • payloads re-redacted and re-bounded,
  • its own self-consistent SHA-256 chain (capsule version 1),
  • per-event anchor hashes back to the original journal,
  • packaged as a single self-contained HTML file.

Anyone can re-verify a capsule offline — no Kerux install, no network, no keys. Open it in any HTML-capable viewer or feed it back to the verify tooling.

What this is — and what it is not

The recorder’s guarantees are easy to overstate, so here are the four distinctions that matter.

Hash chain ≠ signature

The SHA-256 chain gives tamper evidence: any modification after the fact is detectable. It does not give authenticity or non-repudiation. Anyone with filesystem access can rewrite a journal and re-chain it; nothing in the file proves who wrote it. Cryptographic signatures (keys, identity) are explicitly out of scope for v1. Treat chain verification as integrity checking, not proof of origin.

Replay ≠ deterministic reproduction

The journal records observations for causal debugging. The manifest’s replayability field marks whether inputs were captured well enough to attempt a reproduction — it is not a VM snapshot. Replaying against a live provider can legitimately diverge (sampling non-determinism, wall clock, external side effects like git state or network). Evidence first; deterministic reproduction is a separate, harder problem.

Git worktree harness ≠ sandbox

The transactional git harness (pre-run snapshots, checkpoints, /undo) operates on your real working tree — it protects against mistakes, not against malicious code. Validators run in a lexically confined working directory with output caps, but confinement is not a security sandbox against hostile programs. Accordingly: capsules never execute anything, and running validators from an imported run requires explicit user action, every time.

Local journal ≠ shareable capsule

Journal ($KERUX_HOME/runs)Proof capsule (.capsule.html)
AudienceYou, on this machineOther people/machines
FidelityFull (within redaction/bounds)Scrubbed re-chain
PathsAbsolute home paths~-substituted
ChainOriginal event chainOwn chain v1 + anchors to original
SharingNever share rawDesigned for sharing

Even though capsules are redacted, treat both artifacts as sensitive: pattern-based redaction is best-effort, not a data-loss guarantee.

Schema stability

Consumers (scripts, dashboards, future importers) rely on:

  • versioned manifest schema and CAPSULE_VERSION = 1,
  • stable machine-readable reason codes from kerux runs,
  • read-only readers that never mutate journals,
  • additive evolution: new event kinds may appear; existing kinds keep their payload shape.

Breaking changes bump versions rather than mutating meanings.

Reproducible demo

End-to-end, five commands:

# 1. enable recording, then run anything through Kerux
#    (a scripted chat turn works fine as a fixture)

# 2. find the run
kerux runs list --json | jq -r '.runs[0].run_id'

# 3. walk the timeline
kerux runs inspect "$RUN_ID"

# 4. prove integrity
kerux runs verify "$RUN_ID"

# 5. export and re-verify the capsule offline
kerux runs export "$RUN_ID"

Leakage check for the fixture: the exported capsule must contain no absolute home paths and no seeded secret strings — search the HTML for both before treating the pipeline as trusted.

Context Compaction

Rolling summarization of old conversation turns to stay within context limits.

How It Works

  1. After each run, check conversation length against the session cap
  2. If near cap, compact_history() sends the oldest N messages to the LLM as a one-shot summarization chat
  3. The summary replaces those messages, embedded as a [CONTEXT SUMMARY] marker
  4. Recent messages stay intact — only the tail is compressed

Session Format v2

Compaction summaries persist across restarts via the session file format v2:

{
  "version": 2,
  "summary": "User asked about X; we decided Y...",
  "messages": [...]
}

Format v1 (bare message array) is still readable — backward compatible.

Config

[gateway]
context_compaction = true

Fallback Provider Chain

Automatic failover to backup LLM providers on transient errors.

How It Works

FallbackChainProvider wraps the primary provider plus an ordered fallback list. On each request:

  1. Try the current provider
  2. If the error is fallback-worthy (network failure, interrupted stream, 429 rate limit, 5xx) → advance to the next provider
  3. Deterministic failures (401 auth, 400 bad request, context overflow) propagate immediately — no pointless retries

The classifier (is_fallback_worthy) branches on typed error variants only — it never sniffs free-form error text for “429-ish” strings. A provider that reports failures as unstructured prose is treated as a deterministic failure on purpose: guessing from prose risks silently downgrading you to a worse model. All built-in adapters (OpenAI-compatible, Anthropic, Gemini) emit typed Error::Http { status, body } on non-success responses, so 429/5xx classification is reliable across every provider.

Config

Default OFF (empty fallback list = primary only; the client stays locked to the single configured provider unless you opt in). Enable via [[client.fallback]] array-of-tables entries:

[client]
provider = "openai"

[[client.fallback]]
provider = "openrouter"
model = "anthropic/claude-sonnet-4"

[[client.fallback]]
provider = "gemini"
model = "gemini-2.5-pro"

Each entry supports provider (required), optional base_url, api_key, model (defaults to the primary model), and timeout_secs.

Operational Guidelines

  • Order matters. Entries are tried strictly in declaration order; the first success wins. Put your cheapest/most-reliable backup first.
  • Model override per entry. model pins that fallback to a specific model regardless of the primary’s [agent] model. Omit it to reuse the primary model name on the fallback provider (only sensible for providers that share model naming, e.g. OpenRouter passthrough).
  • Streaming falls through too. chat_streaming uses the same classifier; a stream that dies mid-SSE (IncompleteSseMessage) advances to the next provider.
  • Capabilities describe the primary. Planning (context window, edit format, tool support) always uses the primary model’s capability row — fallbacks are a last-resort degradation, not the planning target.
  • Chain exhaustion returns the last error. If every provider fails, you get the final fallback’s error, not the primary’s.
  • Watch the logs. Every failover emits a warn! with the fallback index, target model, and the triggering error; startup logs Fallback provider chain enabled with the entry count.
  • Auth is per entry. A fallback with a bad key fails its own attempt and the chain moves on; a 401 on the primary does not trigger failover (deterministic).

Verification

The chain is covered by unit tests (client/fallback.rs: classification boundaries, fallthrough, no-fallback on 401, exhaustion) and an integration/soak suite (kerux-core/tests/fallback_chain.rs) driving FallbackChainProvider against real mockito HTTP servers: cross-provider fallthrough (OpenAI/Anthropic/Gemini) on 429/5xx, streaming fallthrough with SSE drain, deterministic-401 no-fallback, model-override wire check, chain exhaustion, network-error recovery, plus soak runs (200 sequential iterations under a permanent rate limit, 200 stable-primary iterations, 50 streaming iterations, 64-task concurrent burst).

Implementation

  • kerux-core/src/client/fallback.rs — FallbackChainProvider + FallbackEntry + is_fallback_worthy() detection
  • Wired in kerux-cli via wrap_with_fallbacks() around the runtime client

Cost Guardrails

Estimated-spend ceilings enforced inside the agent loop. The guardrail is off by default; enabling it applies per-run and/or daily cost ceilings on top of the [telemetry] token prices.

Configuration

[telemetry]
input_cost_per_million = 3.0   # token prices drive the cost estimates
output_cost_per_million = 15.0

[budget]
enabled = true
per_run_limit = 2.0            # estimated-spend ceiling per agent run (0 = off)
daily_limit = 20.0             # estimated-spend ceiling per rolling day (0 = off)
warn_threshold_pct = 80        # warn once at this % of a configured limit
on_limit = "downgrade"         # pause | downgrade | stop
downgrade_model = "gpt-4o-mini" # required when on_limit = "downgrade"

Invalid policies fail config load (warn_threshold_pct > 100, negative limits, unknown on_limit, or downgrade without downgrade_model).

Enforcement semantics

After every LLM response the agent records the turn’s usage (record_tokens) and evaluates the verdict:

  • Ok — within all ceilings; nothing happens.
  • Warn — estimated spend crossed warn_threshold_pct of a configured limit. The agent emits one BudgetAlert event (action None) per run; the run continues.
  • LimitExceeded — a hard ceiling was crossed. The configured on_limit action applies:
    • pause / stop — the agent emits a BudgetAlert with the action and halts the run with Error::BudgetExceeded.
    • downgrade — the agent emits one BudgetAlert and routes the rest of the run to downgrade_model. The downgrade applies once; the run continues on the cheaper model.

Costs are estimates computed from the [telemetry] rates and the provider-reported usage (token estimates when a provider reports no usage).

Surfaces

  • Agent events — AgentEvent::BudgetAlert { action, reason, current_run_cost, daily_cost, downgrade_model } is journaled as a budget_alert record when the flight recorder is active.
  • Telemetry — with the guardrail enabled, each billable AgentTelemetry carries estimated_cost_usd for the turn (provider quotes still win when present).
  • Gateway — budget alerts render as a ⚠️ Budget: <reason> status message in the chat channel.
  • TUI — budget alerts appear in the Activity panel with run/day cost and the downgrade target.

Run and daily accounting

Per-run state (run cost, warn/downgrade once-flags, model override) resets at the start of every run(). The daily accumulator survives across runs on shared agents (gateway, TUI). Autonomous mode builds a fresh agent per tick but seeds it with the shared daily accumulator and snapshots the updated totals back afterwards, so the daily ceiling spans ticks.

Testing

  • Unit tests in crates/kerux-core/src/agent.rs cover each verdict path (warn once, pause halt, stop halt, downgrade once + reset, disabled no-op).
  • crates/kerux-core/tests/cost_guardrail_wiring.rs is the end-to-end wiring test: a scripted provider run crosses the per-run limit, the remaining turn routes to the downgrade model, one downgrade alert is emitted, and billable telemetry carries the per-turn cost.

Voice Note STT

Telegram voice notes are transcribed to text before hitting the agent.

How It Works

  1. Incoming message with a voice attachment detected
  2. getFile API → download the .oga audio via the bot token
  3. POST multipart to /v1/audio/transcriptions (OpenAI-compatible endpoint, same credentials as the primary client)
  4. Transcript injected as the user message text

Config

OFF unless stt_model is set:

[gateway]
stt_model = "gemini/gemini-2.5-flash"

Any model the provider supports on the transcriptions endpoint works.

Cron Scheduler

Recurring jobs that fire agent prompts on an interval.

Commands

/cron add <interval> <prompt>                 — schedule an interval job
/cron add cron "<5-field-expr>" <prompt>       — schedule a 5-field cron job
/cron add once <timestamp> <prompt>           — schedule a one-shot job
/cron add agent <interval|cron|once> <task>   — schedule a full agent task run
/cron list                                    — show all jobs
/cron pause <id>                              — pause
/cron resume <id>                             — resume
/cron remove <id>                             — delete

Schedule Syntax

  • Intervals: 30m, 2h, 1d, 1h30m (minimum 60s)
  • Cron: 5-field expression minute hour day month weekday (e.g. */5 * * * *, 0 9 * * 1-5)
  • One-shot: ISO-8601 UTC timestamp (e.g. 2026-08-28T19:30:00Z) or epoch seconds

Behavior

  • Jobs persist to ~/.kerux/scheduler.json (atomic writes)
  • Background ticker in Gateway::run() checks due jobs each tick
  • Downtime burst protection: if multiple fires were missed while the process was down, only ONE catch-up run fires
  • Job output is delivered to the channel that created it

Implementation

  • kerux-core/src/scheduler.rs — Scheduler with atomic JSON persistence

Subagent Delegation

The agent can delegate focused tasks to isolated child agents.

How It Works

SubAgentTool registers a delegate_to_sub_agent tool. When invoked:

  1. A child KeruxAgent is spawned with a fresh conversation (no parent history)
  2. The child runs its own ReAct loop and returns a final summary
  3. Only the summary enters the parent conversation — intermediate noise stays out

Guardrails

GuardrailValue
DefaultON
Max concurrent children3 (tokio semaphore)
Nesting depth1 (child registry is empty — children cannot delegate further)

Config

[tools.delegation]
enabled = true           # default: the tool only costs tokens when called
max_concurrent = 3       # shared semaphore across the process

Children share the parent’s configured provider and model — there is no separate delegation provider setting.

Implementation

  • kerux-core/src/tools/sub_agent_tool.rs

Taste/Style Profile Learning

Kerux learns portable, confidence-scored coding-style preferences from your trajectory history and injects them into the system prompt — so the agent writes code the way you do, across every project.

Inspired by CommandCode’s taste registry: preferences like export style: named exports (confidence 0.85) travel with the user, not the repo. Design reference: research/product-research-t_2be9e216.md (ide #9).

Data model

Everything lives in crates/kerux-core/src/taste.rs.

TypeRole
TasteProfilePortable unit: versioned bag of preferences + metadata. The JSON document is the profile — push/pull needs no conversion step.
TastePreferenceOne learned rule: stable key (export style), preferred value (named exports), evidence counters (positive/negative), denormalized confidence in 0.0..=1.0.
PreferenceObservationOne extracted signal from trajectory history: “the user’s work exhibited (or contradicted) preference X”, with weight and timestamp.
PreferenceExtractorTrait: &[Trajectory] -> Vec<PreferenceObservation>. The extraction engine implements this.
TasteStore / FileTasteStorePortable storage contract + default file-backed implementation (one JSON document per profile name).

Supporting enums: PreferenceCategory (naming, formatting, architecture, tooling, language, documentation, testing, workflow, other) and PreferenceSource (extracted, inferred, manual).

Confidence scoring

Every preference counts supporting (positive) and contradicting (negative) observations. Confidence combines two factors (compute_confidence):

  • Consistency — positive / (positive + negative). A preference that is contradicted half the time can never exceed 0.5.
  • Saturation — n / (n + HALF_SATURATION) with HALF_SATURATION = 5. One observation yields ~0.17, five yield 0.5, twenty yield 0.8; confidence grows slowly and never quite reaches 1.0.
confidence = (positive / total) * (total / (total + 5))

No evidence scores 0.0 — nothing is injected until something is actually observed. The score is stored denormalized on the preference and recomputed whenever evidence changes (recompute_confidence); the raw counters are the source of truth.

Storage format

The portable JSON document is the profile itself (version field guards forward compatibility, missing fields get defaults so old documents stay readable):

{
  "version": 1,
  "name": "kerux",
  "created_at": 1787700000,
  "updated_at": 1787766000,
  "preferences": [
    {
      "key": "export style",
      "category": "language",
      "value": "named exports",
      "positive": 20,
      "negative": 1,
      "confidence": 0.8,
      "source": "extracted",
      "first_observed_at": 1787700000,
      "last_observed_at": 1787766000
    }
  ],
  "metadata": { "source_project": "kerux" }
}

Two locations, same format:

  • Store (portable registry): FileTasteStore persists one pretty-JSON document per profile name under <data_root>/taste/<name>.json (~/.kerux/taste/, KERUX_HOME overrides the root). Names are sanitized for the filesystem.
  • Project-local: <project_root>/.kerux/taste.json (project_taste_path) — commit it to version control to share a house style with your team via PR.

Writes are atomic (temp file + rename, via the shared persist helpers); missing or corrupt files read as “start fresh”.

Push/pull semantics

Push = load the project profile, save it into a TasteStore under a name. Pull = load from the store, TasteProfile::merge into the project profile. Merge rules, per matching key:

kerux taste push team
kerux taste pull team

Both commands use the current directory as the project root. Push requires an existing .kerux/taste.json; pull creates it when absent and atomically writes the merged profile.

  • Same value: evidence counters add, the observation window widens (min first / max last), confidence recomputed.
  • Conflicting values: the side with more total evidence wins (ties go to the more recently observed one).
  • Manual source propagates: an explicitly stated preference (source: manual) wins over learned sources.
  • Keys present on only one side are copied over; metadata keys missing from the target are filled in without overwriting.

Prompt injection

TasteProfile::render_prompt_block(min_confidence, max_items) renders the top preferences (highest confidence first, at most one entry per key, capped) as a markdown block:

## Learned Coding Style Preferences

Learned from past sessions. Follow these unless the user instructs otherwise.

- export style: named exports (confidence 0.80)
- naming: snake_case files (confidence 0.75)

Returns None when nothing clears the threshold or max_items == 0 — callers omit the block entirely rather than injecting an empty section. retain_confident(min) prunes weak preferences from a profile outright.

At runtime, [taste] enabled = true injects this block from the current project’s .kerux/taste.json. min_confidence defaults to 0.5 and max_items defaults to 10; setting enabled = false disables injection.

Extraction engine

crates/kerux-core/src/taste_extraction.rs implements PreferenceExtractor as TrajectoryPreferenceExtractor: a deterministic, LLM-free miner over recorded Trajectory steps (the canonical action record; messages mirror the same tool calls and are skipped to avoid double-counting). It emits supports = true evidence only — counter- evidence is reserved for explicit/manual signals.

Signals mined per trajectory step:

StepSignalKey → value
terminaltoolchain commands (cargo test/clippy/fmt/build, pytest, jest, eslint, prettier, go test, make, tsc, …)test runner, linter, formatter, build tool
terminalgit add <path> vs git add -A/git commit -amcommit style → staged per-file commits / bulk commits
terminal after an edit steptests run right after file_write/patch/edit_blocktest discipline → runs tests after edits (once per edit cycle)
file_write/patch/edit_blockfile extensionprimary language → Rust, TypeScript, …
file_write/patch/edit_blockfile-stem casingfile naming → snake_case / kebab-case / camelCase / PascalCase
file_write/patch/edit_blockshallowest space indent or tab dominance of written contentindentation → 2 spaces / 3 spaces / 4 spaces / tabs
file_write (non-append) vs patch/edit_blockedit granularityedit style → full file rewrites / targeted patches

Retry-loop guard: identical (key, value) observations are capped per trajectory (max_repeats_per_trajectory, default 3), so one stuck session cannot saturate a preference. Confidence scoring stays in taste.rs — the extractor only emits evidence, folded via TasteProfile::apply_observations.

Division of labor

  • taste.rs (this design): schema, scoring math, storage, merge, prompt rendering.
  • taste_extraction.rs: TrajectoryPreferenceExtractor implements PreferenceExtractor; results fold via TasteProfile::apply_observations.
  • System-prompt wiring and kerux taste push/pull CLI: built on TasteStore + render_prompt_block + project_taste_path.

Roadmap

Where Kerux is heading. For what already shipped, see history.md.


Now (v0.2.x)

Stabilization of the gateway era:

  • Harden kerux serve — soak testing, reconnect/backoff edge cases, multi-channel load
  • Docs polish — flesh out feature pages with real config examples and screenshots
  • CI screenshot previews — automated TUI/docs screenshots on every release

Next (v0.3)

  • Vision input — supports_vision capability is already plumbed; needs the multimodal pipeline (image preprocessing, per-provider request construction). Anthropic + Ollama first
  • Repo map optimizations — early-return discovery once the 500-file cap is hit; size-threshold guard for tree-sitter on >1MB files
  • Webhook transport — optional HTTP listener alongside long-polling (for platforms without polling APIs)
  • Session branching — fork a conversation from any point in history

Later (ideas, unscheduled)

  • Plugin system for custom tools beyond MCP
  • Multi-agent routing (different models per channel/task type)
  • Local embedding-based memory search (currently keyword)
  • Voice output (TTS replies)

Done (summary)

  • ✅ Aider integration phases 1–5 (providers, repo map, edit blocks, git harness, curator) — see history.md
  • ✅ Gateway: Telegram long-polling + WhatsApp bridge, MarkdownV2 conversion, streaming replies
  • ✅ Persistence: sessions (v2 + compaction summaries), memory, todos — all atomic JSON
  • ✅ Tool approval, fallback provider chain, voice-note STT, cron scheduler, subagent delegation
  • ✅ Discord & Slack REST adapters alongside Telegram long-polling and the WhatsApp bridge
  • ✅ Flight recorder & proof capsules — hash-chained run journals, read-only inspection (kerux runs), offline-verifiable export — see features/flight-recorder.md
  • ✅ Deterministic project validators + journaled validation passes (engine done, CLI wiring pending)
  • ✅ Rebrand to kerux + mdBook docs at kerux.eikarna.dev

Development History

Last Updated: 2026-08-22 Current Version: v0.2.0 (main branch) + post-release hardening on main Status: ✅ Stable — all CI workflows green


Timeline

Post-0.2.0 hardening (2026-08-20 → 2026-08-22)

52 commits since the v0.2.0 tag, still unreleased:

Gateway additions

  • Discord adapter: REST-based (/users/@me token verification, /channels/{id}/messages send, message-create parsing) — [gateway] discord_enabled
  • Slack adapter: REST-based (slack_api_base, send_message, update parsing) — [gateway] slack_enabled
  • Webhook settings fields (webhooks_enabled, webhooks_addr) exist in config but no listener is implemented yet

Flight recorder & proof capsules (Tasks 1.x)

  • Hash-chained append-only run journals with [recorder] bounded policy (record_content, record_reasoning, payload cap, warn|fail failure mode)
  • Git checkpoint evidence attached to runs; tool approval decisions journaled
  • Read-only kerux runs list|inspect|verify CLI
  • Offline-verifiable, scrubbed HTML proof capsule export
  • Windows storage fixes: per-event-file locking, staging-dir publish, no append-mode event files

Edit-protocol hardening (Tasks 2.x)

  • Task 2.1: deterministic project validators ([validation] config, [[validation.validators]] specs)
  • Task 2.2: validator execution engine (run_validation_pass) — workspace-confined, symlink-safe, bounded/redacted output capture, fail-fast, outcomes journaled as evidence (CLI wiring pending)
  • Task 2.3: bounded repair policy ([agent] max_repair_attempts), first-pass vs repair outcome tracking
  • Task 2.4: edit-protocol outcome metrics journaled per run
  • Task 2.5: static edit-format fallback ladder — classified edit-application failures promote a one-way search_replace → patch → full_file hint; explicit [agent] edit_format_override always wins

v0.2.0 — Gateway Era (2026-08-18 → 2026-08-20)

The project grew from a CLI/TUI agent into a full messaging-gateway runtime, then shed its upstream-derived name.

Gateway & messaging

  • Wired the gateway into the CLI (kerux serve) with Telegram long-polling — no webhook server needed
  • Markdown → Telegram MarkdownV2 converter (stdlib only): special-char escaping, code fences, blockquotes, tables → bullet lists, --- → unicode separator, auto-fallback to plain text
  • Message chunking at 3500 chars with code-fence tracking (no more message is too long)
  • Event-driven UX: live status edits (🤔 Thinking…, 🔧 Tool, heartbeats), final reply replaces the status message
  • Dual reply modes: streaming_replies = false (default) or true (live token streaming with ▌ cursor, ~900ms throttle)
  • WhatsApp adapter via Baileys HTTP bridge (/messages drain-queue polling, WhatsApp-specific markdown converter)
  • Parallel per-adapter polling tasks (Telegram long-poll no longer starves WhatsApp)

Persistence

  • SessionStore: per-channel conversation history as atomic JSON files (~/.kerux/sessions/), format v2 with persistent summary + v1 backward-compat
  • Shared persist.rs helper (atomic write via tempfile + rename, KERUX_HOME override)
  • File-backed persistence for memory (memories.json) and todos (todos.json)

Reliability sweep (zero-bug audit)

  • Cooperative cancellation (Arc<AtomicBool>) checked across the agent loop, SSE chunks, and tool execution; conversation repair after cancel
  • Non-blocking event emission (try_send) — no more deadlocks when the event channel fills
  • Explicit HTTP timeouts on Telegram/WhatsApp clients (connect 5s, read 30–45s)
  • Identity-checked active_runs deregistration (interrupt race fix)
  • Char-safe string truncation (no UTF-8 panics on emoji/CJK tool args)
  • Headless logging fix (IsTerminal check — logs no longer vanish in serve mode)

Feature set

  • Tool approval via Telegram inline keyboards ([✅ Approve] [❌ Deny], callback-query routing, per-call oneshot channels)
  • Rolling context compaction: one-shot LLM summarization of oldest turns near the session cap, [CONTEXT SUMMARY] marker
  • Fallback provider chain (FallbackChainProvider, transient-error detection) — toggleable, default OFF
  • Voice-note STT: Telegram audio → POST /v1/audio/transcriptions
  • Cron scheduler: interval jobs with atomic JSON persistence, /cron add|list|pause|resume|remove
  • Subagent delegation tool: isolated child agents, max 3 concurrent (semaphore), depth 1

Rebrand & docs

  • Full rebrand hermes-rs → kerux (crates, binary, env vars, data dirs, context files) — commit 54076ee
  • mdBook documentation site at kerux.eikarna.dev, deployed via GitHub Actions
  • Root cleanup: design docs moved into docs/, stale files removed

v0.1.x — Aider Integration (2026-08-07 → 2026-08-17)

See roadmap.md for the original plan. All five phases shipped:

PhaseFeatureCommits
1Model-agnostic provider routing + capability tables (OpenAI, Anthropic, Ollama, OpenRouter, Gemini)aa53ed6
2Tree-sitter AST repo map + PageRank scoring (C/Python/Rust/TypeScript)b141e7d, bf25a0a
3Aider-style SEARCH/REPLACE edit blocks with exact+fuzzy matchinga697180
4Transactional git harness: snapshots, dirty-tree guard, Conventional Commits, /undo2e7a75a
5Skill & memory lifecycle: curator passes, decay/prune/dedup, distillation, pinningaa53ed6, a309db0, 2a4485a

Also merged from contributors: Nous Portal OAuth for core (#49) and CLI (#50).


Test & CI Status

  • kerux-core: 424 tests passing
  • kerux-cli: 110 tests passing
  • CI: fmt, clippy (-D warnings), rustdoc (-D warnings), build, test, release, docs — all green on GitHub Actions

Known Issues & Debt

  • Huge repos (>6k files): discovery scans the full tree before capping; early-return optimization pending
  • Tree-sitter on >1MB files: parser may stall; size-threshold warning not yet implemented
  • Vision input: supports_vision capability field exists but no multimodal pipeline yet