- Go 99.3%
- Shell 0.7%
Bubble Tea catches panics inside its own event-loop goroutine and
reports only "program was killed: program experienced a panic" —
the panic value, stack trace, and even WHICH method panicked all
get swallowed. The top-level recover in Run() never fires because
the panic doesn't propagate that far.
Wrap Update() and View() in their own deferred recover() that:
1. Writes the panic value + full goroutine dump to
~/.jarvis/logs/jarvis-tui.log via the existing capturePanic
helper.
2. Re-panics so Bubble Tea still exits cleanly (we can't safely
keep going after a model-mutating panic).
The operator sees the same "program experienced a panic" message
on stdout, but ~/.jarvis/logs/jarvis-tui.log now has the actual
panic trace — exactly the existing CLAUDE.md instruction:
`cat ~/.jarvis/logs/jarvis-tui.log` to recover panic details.
Restructured Update from one method to two:
- Update(msg) is the wrapper with the recover.
- update(msg) is the unchanged switch-statement body.
View() didn't need the split — its single defer at the top works.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| cmd | ||
| deploy | ||
| docs | ||
| internal | ||
| pkg | ||
| scripts | ||
| .gitignore | ||
| CLAUDE.md | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
| SECURITY.md | ||
Jarvis
A Go daemon that hosts many autonomous AGI-style agents. Each agent can write its own prompts, memory, Go tools (compiled to per-tool binaries), schedules, watchers, task boards, and sub-agents.
What works today
- Daemon (
jarvisd) runs as a systemd service. Boots agentless and stays healthy when no agents are configured. Restores persisted agents on startup. - CLI / TUI (
jarvis) — Bubble Tea dashboard + per-agent attach view. No args opens the TUI. Subcommands:info,ps,spawn,tell,attach,pause,resume,stop. Dashboard flexes to terminal width with columns for state, live activity verb (thinking/reasoning/→ tool_name/idle), resolved model id, heartbeat, live task counts (doing/open/blocked), cron count, last-activity, parent. - Attach-view reasoning panel — when the model is mid-thought,
its chain-of-thought streams into a pinned panel above the task
list (header
◆ reasoning… (length: 1.2k), body indented under the⎿tree connector). Panel grows with the reasoning, capped only by available terminal rows. On stream end the panel commits to the transcript as one markdown-rendered block and clears. Toggle visibility with^r. Works for both inline model reasoning (GLM, gpt-oss, R1 distills) and thereflectkernel tool's internal LLM call. - HTTP bridge (
jarvis-bridge-http) — alternate front for the daemon:POST /inbox/<agent>accepts an envelope,GET /sse/<agent>streams outbound events. Lets you wire Telegram/WhatsApp/web UIs in without modifyingjarvisd. - Reasoning loop — full LLM-driven session runner with streaming,
parallel tool fan-out, multi-turn iteration, MaxTurns guard
(configurable per agent), per-turn + cumulative token tracking. The
loop also injects a session-close nudge: if the model is about to
stop while owning a task in
doing, one synthetic turn forces it to eithertask_completeor move the task back before the final reply is shown to the user. - Lean tool catalog + global registry — each agent's prompt sees
only kernel tools (memory_, task_, cron_, watch_, agent_,
reflect, llm_, heartbeat_*) plus two meta-tools (
tool_search,tool_invoke). Everything else —http_get,web_fetch,web_search,now, and every tool any agent has authored — lives in a single canonical registry at~/.jarvis/tools/<name>/and stays out of the prompt until an agent explicitly searches for it and invokes it by name. Result: small prompt budget, zero version drift, agents discover capabilities on demand. Tool metadata recordsauthor_agent,maintained_by,content_hash,keywords[],recent_edits[]— full design indocs/tool-registry.md. task_queryanalytics with multi-preset bundling: passpresets:["stuck_in_doing","overdue","upcoming"]to run all three in one call. Presets: blocked, stuck_in_doing, overdue, upcoming, with_notes_not_done, orphans, longest_chain, all.- Instructive errors throughout — wrong task id lists active
candidates, missing required args enumerate valid values, unknown
tool names point at
tool_search, etc. Designed so small models self-correct on the next turn. - LLM providers — Anthropic (streaming + tool use with
408/425/429/5xx retry + backoff), Ollama (local, /api/chat),
OpenAI-compatible (LM Studio / vLLM / OpenAI / Together / any
endpoint with
tool_callsstreaming — incl. Huawei ModelArts MaaS via configurablechat_pathfor non-/v1/deployments), mock (offline tests). Generic loop in the daemon picks up any[providers.<name>]block inconfig.tomlso adding a new hosted endpoint is a config edit, no code change. Reasoning-model streams (reasoning_contentdeltas) are surfaced as separate events so the TUI's reasoning panel can render them live. Per-provider circuit breaker on the OpenAI-compat path: 5s dial timeout, opens after 3 consecutive dial failures with exponential backoff capped at 5 minutes — a dead LM Studio fails in microseconds instead of burning 30s per heartbeat. Aliases (smart,fast,cheap,local, etc.) configurable; the dashboard resolves and displays the concrete model id, not the alias. - State machine —
running↔paused↔stopped→terminated. All transitions persist toconfig.jsonand survive daemon restarts.stoppedagents fully suspend their cron scheduler + watcher goroutines so they stop polling external endpoints, not just stop firing LLM sessions.pausedkeeps watchers polling so external state accumulates in the inbox for replay on resume. - Inflight session cap — automatic triggers (heartbeat / cron /
watcher) past
max_concurrent_sessions(default 3) are dropped with a warn-level log event. Prevents inference overruns when a session's wall-clock exceeds the heartbeat interval and would otherwise stack inferences unboundedly. Message triggers bypass the cap by design — the user is the back-pressure for those. - Task board — per-agent durable kanban + burndown with cross-agent
assignment via Valkey streams. Tasks have status (open / doing /
blocked / done / cancelled), free-form lanes, recursive DAG
dependencies (
blocked_by, including<id>@<agent>cross-agent refs), notes timeline, due dates. Status validated server-side against the allowed set so a small model can't corrupt the board with invented values. - Cron scheduler —
robfig/cron/v3with second-precision; entries persist tocrons/crons.jsonand reload on daemon boot. Suspended when the owning agent is stopped. - Watchers — passive monitors that wake the agent only on
triggers, so an agent can observe external state without burning
LLM tokens. Built-in kinds:
http_poll,file_watch,valkey_watch,cmd_watch,process_watch,tcp_probe, plus custom Go watchers compiled viapkg/watchio. Trigger DSL evaluates inside the watcher (no LLM cost per poll); actions arewake/tool:<name>/message:<agent>/task:<lane>/silent. Suspended when the owning agent is stopped. - Self-reflection —
reflect({scope, focus?})runs a templated LLM critique over recent actions/tools/sessions and writes structured outputs back into memory (lessons), tool stats (verdicts), task board (followup tasks), and prompt-update proposals. - Memory — per-agent sandboxed markdown + JSON store with a
single-writer serializer for concurrent reasoning sessions.
memory.searchdoes literal-substring + regex via ripgrep with a pure-Go fallback; v1 is "no vector DB" by design — the interface is tech-agnostic so backends can swap later. - Tool quality stats — per-tool rolling latency histograms (p50,
p95, p99, max), failure-rate tracking, verdict (healthy / watch /
degraded), tied into
reflectso agents can audit their own tools. - Event bus — every action the daemon takes (thoughts, tool calls
with args + results, memory ops, state changes, build outcomes,
token usage, task changes, watcher fires, cron fires) flows through
a fan-out bus. Persisted to
agents/<id>/logs/events.jsonlfor forensics + replayed to new attaches from an in-memory ring buffer so the TUI doesn't open blank. Events carryid+caused_byfor the timeline view's causal-tree rendering. - TUI attach view — single chronological transcript: streamed
prose interleaved with timestamped tool calls, memory ops, and
lifecycle markers. Word-wrap, inline markdown rendering (
**bold**,`code`, headers), syntax-highlighted Go fortool_write.source(chroma / monokai). Tab toggles focus between input and transcript;^cinterrupts the in-flight session;^gtoggles transcript ↔ timeline. Mouse capture is OFF so native click-drag-to-copy works. - Agent lifecycle —
agent.spawnwithttl+terminate_on;agent.destroyarchives to~/.jarvis/graveyard/<id>-<ts>/or removes outright;agent.self_destructposts achild_terminatedevent back to the parent. - Starter tool library —
http_get,web_fetch,web_search(Brave Search API backend), andnowlive in the global tool registry from daemon start. They're NOT pre-loaded into any agent's prompt — agents find them viatool_searchwhen needed.http_get+web_fetchsend Chrome-like headers by default to avoid bot-block pages on auto/retail/Cloudflare-fronted sites. Starter sources are content-hashed; an embedded-source change triggers an in-place rebuild of the registry binary on next daemon boot. - Security audit (partial) — agent-id charset validation, scrubbed
env on tool subprocesses (no daemon credentials reachable from a
compiled tool by default; explicit allowlist for
BRAVE_API_KEYused byweb_search),GOPROXY=offso agent tools can't pull remote modules, per-host max-agents cap, Unix socket perms, Anthropic key redaction on every error path, instructive error paths to teach small models the rules they keep breaking.
What's planned but not yet built
- Remote access via WebSocket bridge — the existing HTTP bridge
exposes SSE for outbound events; bidirectional WebSocket support
- bearer-token auth is the next step. Intended deployment target
is Kubernetes (single replica StatefulSet with a PVC for
~/.jarvis/, ingress handles TLS + auth).
- bearer-token auth is the next step. Intended deployment target
is Kubernetes (single replica StatefulSet with a PVC for
- Linux namespaces / seccomp sandboxing for compiled tool subprocesses (v1 is in-code path checks only).
- Embedding-based memory search (current
memory.searchis regex + ripgrep; interface is tech-agnostic so a vector backend can drop in later). - Distributed daemon / multi-host (single host only).
- Encrypted memory at rest.
- HTTP-level retry for provider 429s (the openai_compat circuit
breaker handles dial failures fast; rate-limit-aware backoff with
Retry-Afterparsing is the next gap).
Full architecture and design rationale:
/home/wesley/.claude/plans/i-d-like-to-build-mutable-pearl.md.
Security model: SECURITY.md.
Build
go build -o bin/jarvisd ./cmd/jarvisd
go build -o bin/jarvis ./cmd/jarvis
go build -o bin/jarvis-bridge-http ./cmd/jarvis-bridge-http
Requires Go 1.25+.
Install as a service
bash scripts/install-service.sh # asks for sudo once
$EDITOR ~/.jarvis/jarvisd.env # set ANTHROPIC_API_KEY etc.
sudo systemctl restart jarvisd
The install script copies deploy/jarvisd.service into
/etc/systemd/system/, symlinks bin/jarvis(d) into /usr/local/bin,
seeds ~/.jarvis/jarvisd.env (gitignored), and enables the unit.
For ongoing updates after a git pull:
bash scripts/update-service.sh
The update path needs only a small NOPASSWD entry for
systemctl restart|reload|status jarvisd.service — see the install
script for details.
Run
jarvis # TUI dashboard
jarvis ps # list agents
jarvis spawn alice # create + start an agent
jarvis tell alice "hello" # send a message
jarvis attach alice # tail the event stream
jarvis pause alice # suspend heartbeat (state persists)
jarvis resume alice
jarvis stop alice # stop crons + watchers too
Configuration
~/.jarvis/config.toml (gitignored, outside the repo):
[daemon]
socket = "~/.jarvis/daemon.sock"
data_dir = "~/.jarvis"
max_agents_per_host = 64
[providers.anthropic]
api_key = "${ANTHROPIC_API_KEY}"
concurrency = 16
[providers.ollama]
host = "${OLLAMA_HOST:-http://127.0.0.1:11434}"
[providers.lmstudio]
host = "http://192.168.2.100:1234"
concurrency = 8
[aliases]
smart = "claude-opus-4-7"
fast = "claude-haiku-4-5"
cheap = "llama3.1:latest"
local = "qwen/qwen2.5-coder-14b"
# Brave Search API key for the web_search starter tool.
# The daemon plumbs this through to compiled tools via BRAVE_API_KEY.
[search.brave]
api_key = "${BRAVE_API_KEY:-}"
${VAR} and ${VAR:-default} expand from the environment at load time.
A fresh install Just Works with defaults — aliases auto-remap to mock
when their configured target is unreachable.
Runtime requirements
- Go toolchain on
$PATH(the daemon shells out togo buildto compile agent-written tools). - Optional: Anthropic API key for the highest-quality model tier.
- Optional: a local LM Studio / Ollama server for self-hosted models —
any OpenAI-compatible
/v1/chat/completionsendpoint withtool_callsstreaming works via the openai_compat provider. - Optional: Brave Search API key for the
web_searchstarter tool (free tier is generous). - Optional: Valkey or Redis on
127.0.0.1:6379(currently unused; the working-memory backend ships separately and is not yet wired into the kernel tools).
Repository layout
cmd/
jarvisd/ daemon entrypoint
jarvis/ CLI + TUI client
jarvis-bridge-http/ HTTP transport bridge
internal/
agent/ per-agent supervisor + state machine + state persistence +
inflight session cap
config/ TOML loader with env-var expansion
crons/ per-agent cron scheduler with suspend/resume
events/ event bus + per-agent ring buffer
eventlog/ on-disk JSONL persistor
ipc/ Unix-socket framed JSON-RPC server
kernel/ lean tool catalog + LLM session runner + reflection
lifecycle/ spawn / destroy / graveyard
llm/ provider abstraction (Anthropic, openai-compat, Ollama, mock)
memory/ per-agent sandboxed markdown / JSON store
sandbox/ path-escape enforcement
starter/ embedded starter library installed into the global registry
tasks/ per-agent task board with cross-agent assignment
tools/ global tool Registry + Builder + Invoker + rolling Stats
tui/ Bubble Tea views (dashboard, attach, timeline, reasoning panel)
valkey/ client + key namespacing (ready, not yet wired)
watchers/ per-agent passive monitors with suspend/resume
docs/
tool-registry.md design + lifecycle for the global tool store
pkg/
toolio/ shared ABI for agent-written tools
watchio/ shared ABI for agent-written watchers
deploy/ systemd unit + env template
scripts/ install + update helpers
Tests
go test ./...
Integration smoke tests gated on ANTHROPIC_API_KEY (e.g.
TestAnthropicSmoke) skip when the env var is unset, so plain
go test ./... stays offline-safe.
# One-shot dashboard render to stderr — useful for eyeballing
# layout after a TUI change.
go test ./internal/tui/ -run TestSnapshot -v