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 buildand 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
| Crate | Description |
|---|---|
kerux-core | Agent loop, LLM clients, tools, gateway adapters, persistence |
kerux-cli | CLI/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:
--config <path>CLI flag./kerux.tomlin the current directory./.kerux.tomlin the current directory~/.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:
| Key | Default | Description |
|---|---|---|
telegram_enabled | false | Enable Telegram long-polling adapter |
telegram_token | — | Bot token |
discord_enabled | false | Enable Discord REST adapter (discord_token) |
slack_enabled | false | Enable Slack REST adapter (slack_token) |
slack_signing_secret | — | Verify Slack Events API signatures; required by /webhook/slack |
whatsapp_enabled | false | Enable WhatsApp adapter (Baileys bridge) |
whatsapp_bridge_url | — | Bridge endpoint (e.g. http://127.0.0.1:3000) |
webhooks_enabled | false | Start the inbound HTTP listener |
webhooks_addr | — | Listener address (e.g. 127.0.0.1:8080) |
streaming_replies | false | Live-edit token streaming with ▌ cursor |
tool_approval | true | Require inline-keyboard approval before dangerous tool execution |
tool_approval_timeout_secs | 300 | Auto-deny approval requests after this long |
context_compaction | true | Summarize 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:
| Variable | Overrides |
|---|---|
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
| Module | Responsibility |
|---|---|
agent.rs | ReAct loop, streaming, cooperative cancellation, approval gate, context compaction |
client.rs | LLM provider abstraction (OpenAI-compatible, Anthropic, Gemini), fallback chain |
gateway.rs | Platform adapters (Telegram long-polling, WhatsApp bridge), markdown conversion, message chunking |
session_store.rs | Per-channel conversation persistence (format v2 with summary) |
persist.rs | Shared atomic JSON write helpers (~/.kerux/) |
approval.rs | Tool approval gate (inline keyboard via Telegram callback_query) |
scheduler.rs | Cron-style job scheduler with disk persistence |
tools/ | Built-in tool implementations |
platform.rs | OS paths (kerux_home(), config/data/sessions dirs) |
Agent Loop
The core execution engine lives in kerux-core/src/agent.rs.
ReAct Cycle
- Build context — system prompt + conversation history (+
[CONTEXT SUMMARY]if compaction active) - Call LLM — streaming or non-streaming via the configured provider
- Parse response — text chunks, reasoning, tool calls (tolerant parsing)
- Execute tools — with optional approval gate; results appended as tool messages
- 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::spawntask 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
| Adapter | Transport | Enable |
|---|---|---|
| Telegram | Long-polling (getUpdates) | [gateway] telegram_enabled + telegram_token |
| Baileys HTTP bridge (drain-queue polling) | [gateway] whatsapp_enabled + whatsapp_bridge_url | |
| Discord | REST 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 |
| Slack | Events 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.
| Endpoint | Method | Purpose |
|---|---|---|
/health | GET | Listener readiness check |
/webhook or /webhook/generic | POST | Generic external trigger |
/webhook/slack | POST | Slack 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:
| Data | Location |
|---|---|
| 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 loginkeeps 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
/connectflows. Kerux should copy the credential separation pattern, not vendor-private auth internals.
Recommended architecture
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.jsonor 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_refis 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:
ApiKeyAuthProviderfor the current OpenAI-compatible behavior.BearerTokenAuthProviderfor 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 providerskerux auth listkerux auth logout <auth-ref>- TUI command/modal equivalent after CLI flow is stable
Login flow order:
- Prefer API key where it is the official provider API path.
- Prefer provider-managed credentials first: Google ADC, GitHub/Copilot CLI keychain, Claude Code setup wizards, AWS/GCP/Foundry credential chains.
- Offer OAuth only for providers with documented third-party, device-code, or ADC flows.
- Use loopback PKCE for native desktop OAuth where supported.
- 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
OpenAIClientor 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
stateand 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 listandkerux 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 providersreports 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
stateand rejects access tokens in query strings or fragments. - Added a loopback callback receiver that binds only to
127.0.0.1on 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
- Agent wants to execute a tool
- Gateway sends a prompt with
[✅ Approve] [❌ Deny]inline buttons - User taps a button → Telegram sends a
callback_query - The query is routed to a per-tool-call
oneshotchannel - Agent proceeds (approve) or skips with a denial message (deny)
- Timeout → treated as denial
Config
[gateway]
tool_approval = true
Implementation
kerux-core/src/approval.rs— approval gate managercallback_queryhandling inTelegramAdapter::poll_updatesanswerCallbackQueryack 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:
| Event | Meaning |
|---|---|
run_started / run_completed / run_cancelled / run_failed | Run lifecycle, with exactly one terminal status |
request_prepared | Full request provenance: model, provider, context composition |
tool_started / tool_completed / tool_failed | Tool execution timeline, correlated by call id |
approval_decision | Human/tool-approval decisions with redacted reasons |
edit_outcome | Edit protocol outcome: format, parse/apply status, pass kind (first_pass vs repair_pass), repair counts, effective routing format |
validator_result | Project-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) | |
|---|---|---|
| Audience | You, on this machine | Other people/machines |
| Fidelity | Full (within redaction/bounds) | Scrubbed re-chain |
| Paths | Absolute home paths | ~-substituted |
| Chain | Original event chain | Own chain v1 + anchors to original |
| Sharing | Never share raw | Designed 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
- After each run, check conversation length against the session cap
- If near cap,
compact_history()sends the oldest N messages to the LLM as a one-shot summarization chat - The summary replaces those messages, embedded as a
[CONTEXT SUMMARY]marker - 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:
- Try the current provider
- If the error is fallback-worthy (network failure, interrupted stream, 429 rate limit, 5xx) → advance to the next provider
- 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.
modelpins 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_streaminguses 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 logsFallback provider chain enabledwith 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-cliviawrap_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_pctof a configured limit. The agent emits oneBudgetAlertevent (actionNone) per run; the run continues. - LimitExceeded — a hard ceiling was crossed. The configured
on_limitaction applies:pause/stop— the agent emits aBudgetAlertwith the action and halts the run withError::BudgetExceeded.downgrade— the agent emits oneBudgetAlertand routes the rest of the run todowngrade_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 abudget_alertrecord when the flight recorder is active. - Telemetry — with the guardrail enabled, each billable
AgentTelemetrycarriesestimated_cost_usdfor 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.rscover each verdict path (warn once, pause halt, stop halt, downgrade once + reset, disabled no-op). crates/kerux-core/tests/cost_guardrail_wiring.rsis 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
- Incoming message with a
voiceattachment detected getFileAPI → download the.ogaaudio via the bot token- POST multipart to
/v1/audio/transcriptions(OpenAI-compatible endpoint, same credentials as the primary client) - 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—Schedulerwith 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:
- A child
KeruxAgentis spawned with a fresh conversation (no parent history) - The child runs its own ReAct loop and returns a final summary
- Only the summary enters the parent conversation — intermediate noise stays out
Guardrails
| Guardrail | Value |
|---|---|
| Default | ON |
| Max concurrent children | 3 (tokio semaphore) |
| Nesting depth | 1 (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.
| Type | Role |
|---|---|
TasteProfile | Portable unit: versioned bag of preferences + metadata. The JSON document is the profile — push/pull needs no conversion step. |
TastePreference | One learned rule: stable key (export style), preferred value (named exports), evidence counters (positive/negative), denormalized confidence in 0.0..=1.0. |
PreferenceObservation | One extracted signal from trajectory history: “the user’s work exhibited (or contradicted) preference X”, with weight and timestamp. |
PreferenceExtractor | Trait: &[Trajectory] -> Vec<PreferenceObservation>. The extraction engine implements this. |
TasteStore / FileTasteStore | Portable 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 exceed0.5. - Saturation —
n / (n + HALF_SATURATION)withHALF_SATURATION = 5. One observation yields~0.17, five yield0.5, twenty yield0.8; confidence grows slowly and never quite reaches1.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):
FileTasteStorepersists one pretty-JSON document per profile name under<data_root>/taste/<name>.json(~/.kerux/taste/,KERUX_HOMEoverrides 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:
| Step | Signal | Key → value |
|---|---|---|
terminal | toolchain commands (cargo test/clippy/fmt/build, pytest, jest, eslint, prettier, go test, make, tsc, …) | test runner, linter, formatter, build tool |
terminal | git add <path> vs git add -A/git commit -am | commit style → staged per-file commits / bulk commits |
terminal after an edit step | tests run right after file_write/patch/edit_block | test discipline → runs tests after edits (once per edit cycle) |
file_write/patch/edit_block | file extension | primary language → Rust, TypeScript, … |
file_write/patch/edit_block | file-stem casing | file naming → snake_case / kebab-case / camelCase / PascalCase |
file_write/patch/edit_block | shallowest space indent or tab dominance of written content | indentation → 2 spaces / 3 spaces / 4 spaces / tabs |
file_write (non-append) vs patch/edit_block | edit granularity | edit 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:TrajectoryPreferenceExtractorimplementsPreferenceExtractor; results fold viaTasteProfile::apply_observations.- System-prompt wiring and
kerux taste push/pullCLI: built onTasteStore+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_visioncapability 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/@metoken verification,/channels/{id}/messagessend, 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|failfailure mode) - Git checkpoint evidence attached to runs; tool approval decisions journaled
- Read-only
kerux runs list|inspect|verifyCLI - 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_filehint; explicit[agent] edit_format_overridealways 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) ortrue(live token streaming with▌cursor, ~900ms throttle) - WhatsApp adapter via Baileys HTTP bridge (
/messagesdrain-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 persistentsummary+ v1 backward-compat- Shared
persist.rshelper (atomic write via tempfile + rename,KERUX_HOMEoverride) - 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_runsderegistration (interrupt race fix) - Char-safe string truncation (no UTF-8 panics on emoji/CJK tool args)
- Headless logging fix (
IsTerminalcheck — logs no longer vanish inservemode)
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) — commit54076ee - 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:
| Phase | Feature | Commits |
|---|---|---|
| 1 | Model-agnostic provider routing + capability tables (OpenAI, Anthropic, Ollama, OpenRouter, Gemini) | aa53ed6 |
| 2 | Tree-sitter AST repo map + PageRank scoring (C/Python/Rust/TypeScript) | b141e7d, bf25a0a |
| 3 | Aider-style SEARCH/REPLACE edit blocks with exact+fuzzy matching | a697180 |
| 4 | Transactional git harness: snapshots, dirty-tree guard, Conventional Commits, /undo | 2e7a75a |
| 5 | Skill & memory lifecycle: curator passes, decay/prune/dedup, distillation, pinning | aa53ed6, 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_visioncapability field exists but no multimodal pipeline yet