No description
  • Go 99.3%
  • Shell 0.7%
Find a file
Wesley 7e1cbc4d7d tui: capture panic detail inside Bubble Tea Update + View
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>
2026-05-15 14:05:19 +00:00
cmd agent: keep supervisor alive through Stop — fix dead-after-resume bug 2026-05-15 13:16:13 +00:00
deploy http-bridge: cmd/jarvis-bridge-http — HTTP front for the daemon 2026-05-14 14:28:36 +00:00
docs docs: horizontal-scale architecture — sharded actors + directory + NATS 2026-05-15 12:30:01 +00:00
internal tui: capture panic detail inside Bubble Tea Update + View 2026-05-15 14:05:19 +00:00
pkg abi: pkg/toolio and pkg/watchio shared ABIs 2026-05-14 09:06:22 +00:00
scripts http-bridge: cmd/jarvis-bridge-http — HTTP front for the daemon 2026-05-14 14:28:36 +00:00
.gitignore llm/anthropic: real provider with streaming + tool use 2026-05-14 09:24:55 +00:00
CLAUDE.md docs: refresh CLAUDE.md + README to match the post-refactor architecture 2026-05-15 09:50:28 +00:00
go.mod crons: per-agent scheduled actions with persistence 2026-05-14 12:37:06 +00:00
go.sum crons: per-agent scheduled actions with persistence 2026-05-14 12:37:06 +00:00
README.md docs: refresh CLAUDE.md + README to match the post-refactor architecture 2026-05-15 09:50:28 +00:00
SECURITY.md llm: generic OpenAI-compatible provider; LM Studio wired in 2026-05-14 18:16:41 +00:00

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 the reflect kernel 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 modifying jarvisd.
  • 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 either task_complete or 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 records author_agent, maintained_by, content_hash, keywords[], recent_edits[] — full design in docs/tool-registry.md.
  • task_query analytics with multi-preset bundling: pass presets:["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_calls streaming — incl. Huawei ModelArts MaaS via configurable chat_path for non-/v1/ deployments), mock (offline tests). Generic loop in the daemon picks up any [providers.<name>] block in config.toml so adding a new hosted endpoint is a config edit, no code change. Reasoning-model streams (reasoning_content deltas) 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 machinerunningpausedstoppedterminated. All transitions persist to config.json and survive daemon restarts. stopped agents fully suspend their cron scheduler + watcher goroutines so they stop polling external endpoints, not just stop firing LLM sessions. paused keeps 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 schedulerrobfig/cron/v3 with second-precision; entries persist to crons/crons.json and 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 via pkg/watchio. Trigger DSL evaluates inside the watcher (no LLM cost per poll); actions are wake / tool:<name> / message:<agent> / task:<lane> / silent. Suspended when the owning agent is stopped.
  • Self-reflectionreflect({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.search does 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 reflect so 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.jsonl for forensics + replayed to new attaches from an in-memory ring buffer so the TUI doesn't open blank. Events carry id + caused_by for 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 for tool_write.source (chroma / monokai). Tab toggles focus between input and transcript; ^c interrupts the in-flight session; ^g toggles transcript ↔ timeline. Mouse capture is OFF so native click-drag-to-copy works.
  • Agent lifecycleagent.spawn with ttl + terminate_on; agent.destroy archives to ~/.jarvis/graveyard/<id>-<ts>/ or removes outright; agent.self_destruct posts a child_terminated event back to the parent.
  • Starter tool libraryhttp_get, web_fetch, web_search (Brave Search API backend), and now live in the global tool registry from daemon start. They're NOT pre-loaded into any agent's prompt — agents find them via tool_search when needed. http_get + web_fetch send 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_KEY used by web_search), GOPROXY=off so 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).
  • Linux namespaces / seccomp sandboxing for compiled tool subprocesses (v1 is in-code path checks only).
  • Embedding-based memory search (current memory.search is 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-After parsing 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 to go build to 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/completions endpoint with tool_calls streaming works via the openai_compat provider.
  • Optional: Brave Search API key for the web_search starter 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