* feat(engine): add DeepSeek as a first-class cloud provider
Adds DEEPSEEK_API_KEY support to the cloud engine, wiring DeepSeek's
OpenAI-compatible API (api.deepseek.com/v1) alongside the existing
MiniMax, OpenRouter, Anthropic, and Google providers.
- Add _DEEPSEEK_MODELS list (deepseek-v4-flash, deepseek-v4-pro)
- Add _is_deepseek_model() routing predicate
- Init self._deepseek_client from DEEPSEEK_API_KEY in _init_clients()
- Add _generate_deepseek() and _stream_deepseek() methods
- Wire DeepSeek into generate(), stream(), _stream_full_openai(),
list_models(), and health()
- Add approximate pricing entries for both models
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(engine): strict cloud model routing + deepseek can_serve branch
Builds on the DeepSeek provider (PR #504) with two routing-correctness
fixes to CloudEngine._client_for_model:
1. Add the missing DeepSeek branch so can_serve('deepseek-*') agrees with
list_models()/health() when only DEEPSEEK_API_KEY is set (mirrors the
minimax branch). Without it the engine advertised deepseek models via
list_models() but refused to serve them (the #532 can_serve contract).
2. Fix#335: _client_for_model previously fell through to the OpenAI client
for ANY unrecognized model name, so an OpenAI key (even a dummy
sk-dummy... one) made can_serve('qwen3.5:0.8b') return True. With the
local engine transiently down (classic post-Windows-restart Ollama not
yet up), model-aware get_engine then mis-selected the cloud engine for a
local model and died with "OpenAI client not available". Add a positive
_is_openai_model predicate (gpt-/chatgpt-/o1/o3/o4 + _OPENAI_MODELS) and
return None for unrecognized names, so can_serve declines them. generate()
and stream() keep their OpenAI fall-through, preserving loud failure for an
explicitly-requested unknown cloud model.
Tests: DeepSeek detection/pricing/health/list_models/generate-routing/
can_serve and a #335 regression (can_serve rejects local names with an
OpenAI key; unknown model not served even with all clients set; end-to-end
get_engine does not misroute a local model with a dummy OpenAI key).
Fixes#335
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Jen Huls <me@jenhuls.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
OpenJarvis can run vision-capable local models (gemma3, qwen2.5-vl), but the
CLI had no way to send them a picture -- the Ollama engine only serialized
text. This adds end-to-end image input.
What's new
- `jarvis ask -i/--image <file>` attaches one or more images to the query.
- `jarvis ask -S/--screen` captures the primary monitor (dependency-free on
Windows via .NET; mss/Pillow fallback elsewhere).
- Vision auto-routes to direct-to-engine mode; with an explicit --agent it
warns rather than silently dropping the image.
- Privacy guard: warns before sending an image to a non-local engine,
keeping OpenJarvis local-first by default.
- Context-window default raised 8k -> 16k (JARVIS_NUM_CTX) so an image plus
a conversation fit.
Implementation
- Message.images carries base64 data; messages_to_dicts() forwards it to
Ollama's /api/chat "images" field. Text-only messages are unchanged.
- GuardrailsEngine preserves images when it rewrites a flagged message.
Tests (tests/test_vision.py, 6/6 pass, ruff-clean)
- payload forwarding, text path untouched, num_ctx override, guardrail
image preservation.
Verified on AMD RX 9070 XT (Ollama/Vulkan, 100% GPU) with gemma3:4b:
solid-color image, file image, and live screen capture all described.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Ensures that the desktop GUI correctly sends the Authorization header
when an API key is configured. This resolves the 'Failed to get response'
bug on Windows systems with enabled authentication.
Ref: #266
Co-authored-by: sanjayravit <sanjay@example.com>
Two failure classes hit by a downstream team:
1) tmux/task-env death (TerminalBenchTaskEnv.__enter__, terminalbench_env.py):
create_session ran inside the spin_up_terminal generator-CM with no
exception safety — a tmux failure leaked the docker compose project
(down deferred to GC, never if the env was retained) and surfaced as an
opaque mid-run death. Now: exception-safe __enter__ with an idempotent
_teardown(), a tmux/asciinema preflight in the container BEFORE the
agent loop (TaskEnvironmentError naming the task image + remedy), and
fail-that-task-cleanly semantics — the failure is recorded as a harness
error (QueryTrace.error_kind="harness_error"), the container is downed,
and the run continues.
2) OpenHands in-container SETUP hang misattributed as a model result:
harness.run() had no bound (terminal-bench runs installed-agent setup
with max_timeout_sec=inf inside the per-trial agent budget), and the
summary conversion read the nonexistent results.trial_results attr and
hardcoded errors=0, folding zero-model-request trials into resolve-rate
as model misses. Now: global_agent_timeout_sec / global_timeout_multiplier
are threaded config -> backend -> Harness kwargs (default 1800 s bound on
SETUP+RUN; configurable per [run]/[[benchmarks]] TOML), and
summarize_benchmark_results() classifies harness/infra failures out of
the accuracy denominator keyed on token usage (zero/missing tokens +
unresolved = the agent never contacted the model), NOT failure_mode —
terminal-bench 0.2.18 leaves failure_mode UNSET on success AND on
genuine misses, so a failure_mode-based check would misflag every real
model miss. Genuine misses (tokens>0, is_resolved=false) stay in the
denominator.
QueryTrace gains error/error_kind (wired through to_dict/from_dict so the
fields actually reach traces.jsonl; backward-compatible loads), the
AgenticRunner flags zero-model-contact traces and TaskEnvironmentError as
harness errors, and export/summary/console output exclude harness errors
from resolve-rate while reporting them loudly.
terminal-bench stays an undeclared dep on purpose: it requires Python
>=3.12 while this project supports >=3.10,<3.14, so an unmarked extra
would break uv lock. pyproject/uv.lock untouched.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs/user-guide/evaluations.md documented a standalone "openjarvis-evals"
package, a "uv sync --extra eval" install, and an "openjarvis-eval" console
script — a layout from commit bd493832 that was never an ancestor of main.
Rewrite the page against the real surface (jarvis eval / python -m
openjarvis.evals), document all 40 registered benchmark keys and 4 backends,
fix the judge-model default, correct run-all semantics, and split the option
reference into the jarvis-eval subset and the module CLI's research-only
options.
Add the one-line [project.scripts] alias
openjarvis-eval = "openjarvis.evals.cli:main" (the click group the module
CLI already dispatches to) so the long-documented command name works again.
The page was a complete orphan: add it (and the equally orphaned
benchmarks.md) to the mkdocs nav and link it from docs/index.md.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`jarvis eval run --base-url ... --api-key ...` was silently dropped for
jarvis-direct/jarvis-agent (_build_backend only forwarded the flags to
hermes/openclaw) and ignored by terminalbench-native, which hardcoded
api_base="http://localhost:8000/v1". Worse, with --base-url set the
engine-discovery fallback silently substituted ANY healthy local engine
(observed: requested vllm + healthy endpoint at --base-url, got
OllamaEngine@localhost:11434 — the requested URL was never contacted).
Changes:
- _OpenAICompatibleEngine gains an api_key param (Bearer Authorization
header on the httpx client; {ENGINE_ID}_API_KEY env fallback with
hyphen-sanitized names; no header when unset).
- New non-registered OpenAICompatEngine + normalize_openai_base_url()
(strips a single literal trailing "/v1" so request paths don't double).
- SystemBuilder.engine_instance() injects a pre-built engine; build()
health-checks it and fails loudly naming the host instead of falling
back to discovery. Discovery substitution after an explicit -e key now
logs a warning.
- JarvisDirectBackend/JarvisAgentBackend accept base_url/api_key; on
base_url they pin an OpenAICompatEngine to that endpoint with a
fail-fast pre-flight (actionable error naming the URL and probe).
- _build_backend forwards base_url/api_key to first-party backends on
the CLI path; _run_terminalbench_native receives --base-url as
api_base (single /v1 suffix) and exports OPENAI_API_KEY around the
in-process harness run (terminus-2 routes via LiteLLM).
- Suite TOML [backend.external] stays scoped to hermes/openclaw
(suite_mode=True in the suite drivers) — first-party suite semantics
are explicitly deferred. The config-host path is untouched.
- Help text updated on both CLI surfaces; KNOWN_BACKENDS now lists
hermes/openclaw/terminalbench-native.
Fixes the eval-CLI endpoint gap reported by the downstream team.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(windows): desktop backend spawn (#531) + model-aware engine selection (#532)
Two runtime bugs found during end-to-end testing on a clean Windows 11
24H2 Azure VM.
#531 - Desktop "Failed to get response": run_jarvis_command spawned the
backend with .output(), which waits for the process to exit. `jarvis
serve` never exits, so the Tauri command hung forever (the Start button
never resolved); and it ran `uv run jarvis` with no cwd, so in a packaged
install -- where the cwd isn't the checkout -- `jarvis` wasn't found and
the server never started. Now: run from find_project_root(), and for
`serve` spawn detached (.spawn()), drain stderr, and poll /health for
readiness (mirrors start_backend); short commands keep .output().
The server layer itself was verified healthy on Windows (/health and
/v1/chat/completions both 200, localhost included) -- the fault was the
Tauri spawn path.
#532 - "OpenAI client not available" after reboot: when the local engine
is down, get_engine's fallback selected CloudEngine because health() is
True if ANY provider client exists -- without checking the resolved
model's provider has a client. A user with e.g. OPENROUTER_API_KEY and a
gpt-* model then hit the OpenAI path with no client. Add
CloudEngine.can_serve(model) (checks the specific provider client via the
same routing generate()/stream() use) + a default can_serve->True on the
base engine, and make get_engine model-aware so it skips an engine that
can't serve the model -- the user falls through to the helpful "no engine
available / start ollama" message instead.
Tests: engine discovery/cloud/model-matrix + cli serve/ask suites pass
(the one ask_e2e failure is a pre-existing version-banner flake, fails
identically on main). The Tauri crate couldn't be compiled locally (no
GTK/webkit sys-libs in this env); relies on CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(engine): cover model-aware engine selection + CloudEngine.can_serve (#532)
#533 added a `model` arg to get_engine and a can_serve() gate but shipped no
tests. Add them:
- get_engine skips a healthy engine that can't serve the requested model
(the cloud-fallback-for-unservable-model case behind #532),
- model=None preserves the legacy model-agnostic selection,
- CloudEngine.can_serve gates on the per-provider client (gpt->OpenAI,
claude->Anthropic, ...), verified empirically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Two bugs found during end-to-end testing on a clean Windows 11 24H2
Azure VM (closes#522). Both are dodged by the canonical `irm | iex`
one-liner but hit by the documented `-OutFile` fallback and any
non-interactive run.
1. Encoding. install.ps1 was UTF-8 without a BOM and contained em-dashes
plus a box-drawing banner. Windows PowerShell 5.1 decodes BOM-less
files with the legacy ANSI/OEM code page, mis-decoding the multi-byte
sequences and desyncing the parser into cascading here-string parse
errors. Converted the file to pure ASCII (em-dashes -> hyphens, banner
-> ASCII art) so it parses no matter how it's read.
2. Ollama readiness loop. With $ErrorActionPreference='Stop', the probe
`& $ollamaExe list 2>&1 | Out-Null` turned the daemon-not-up stderr
into a terminating NativeCommandError, aborting the install on the
first iteration and making the loop's own Start-Process serve retry +
Write-Warn2 fallback dead code. Wrapped the probe in try/catch so it
falls through to the self-start path as intended.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* security: harden network-exposed surface
Hardening for the network-reachable attack surface, prioritizing fixes
that are strong but do not change working local/loopback defaults.
- auth_middleware: constant-time API key comparison (secrets.compare_digest)
for the HTTP path, and gate /metrics behind auth so operational counters
are not readable unauthenticated. /health stays open.
- webhook_routes: fail closed when a channel's secret/token is unset. Twilio,
BlueBubbles, WhatsApp (verify + inbound), and SendBlue now reject (403)
instead of processing unsigned/unauthenticated input. Constant-time
comparisons for BlueBubbles/SendBlue/WhatsApp verify token.
- http_request: follow redirects manually and re-run the SSRF check on every
hop (capped at 5) so an allowed public URL cannot 30x-redirect to an
internal/metadata address.
- api_routes /v1/memory/index: restrict indexing to OPENJARVIS_WORKSPACE roots
when configured and refuse sensitive files (.env, keys, credentials).
- config.toml: default [server] host to 127.0.0.1 (loopback) with a comment
on how to safely expose to a LAN (0.0.0.0 + API key).
Tests: new fail-closed webhook tests, /metrics auth tests, and SSRF
redirect block/follow tests; updated SendBlue tests for the new
secret-required behavior. Affected suites pass (95 tests), ruff clean.
* fix(http): keep SSRF redirect-following patchable via httpx.request
The manual redirect-following loop used a private httpx.Client, which
bypassed the `http_request.httpx.request` mock seam that consumers' tests
rely on (e.g. the twitter-bot GitHub-issue tests escaped to the real
network and 401'd). Issue each hop via module-level httpx.request with
follow_redirects=False instead — same per-hop SSRF re-check, restored
testability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): wire SystemPromptBuilder so persona files load in jarvis chat (fixes#458)
* fix(chat): make `--persona none` actually disable persona files
This PR exposes `--persona none`, but SystemPromptBuilder._load_file read
empty paths as "." (Path("") -> ".") and raised IsADirectoryError, so the
documented opt-out crashed. Guard empty path_str so the "none" opt-out
(which _resolve_persona maps to empty file paths) cleanly injects no
persona. Adds an end-to-end regression test (building with persona
"none" must not raise). Also merges current main (branch was stale).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`jarvis serve` constructed every heavy component inline (engine discovery +
instrumentation, telemetry, memory, agent manager, per-agent tools) and then
called `SystemBuilder(config).build()` a second time inside the scheduler
block purely to feed `AgentExecutor.set_system()`. That second build
re-discovered and re-connected the engine, re-instrumented it, re-resolved
tools, re-opened the configured channel and re-created the agent manager —
~30-40s of fully redundant startup work (the headline remaining cost in #263
after engine probes were parallelised and the version check moved off the hot
path in #470).
Fix: assemble the executor's `JarvisSystem` from the components already built
inline instead of rebuilding from scratch. `AgentExecutor` only reads
`engine`, `model`, `config`, `memory_backend`, `tool_executor`,
`session_store` and `channel_backend` off the system; all are wired here. The
memory backend is now constructed just before the scheduler block (it was
built later) so the executor's system can reference it, and the primary
agent's resolved tool list is reused to build the scheduler's `ToolExecutor`
(preserving the MCP-discovered-tool pool the executor reads via
`tool_executor._tools`). `skill_manager` / the learning orchestrator are only
consumed by the orchestrator's `system.ask()` path, which the executor never
invokes, so they are intentionally omitted.
Tests: new `tests/cli/test_serve_single_build.py` patches
`SystemBuilder.build` and asserts it is never called during `jarvis serve`
startup, and that the executor still receives a system exposing
`tool_executor` / `session_store` / `memory_backend` (plus engine/model/config).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The channel `send()` contract was inconsistent across adapters. Almost
every adapter (Discord, Slack, email, WhatsApp, ...) treats the first
positional `channel` arg as the real DESTINATION id and `conversation_id`
as an optional reply/thread reference. Telegram alone treated
`conversation_id` as the destination (`chat_id = conversation_id or
channel`).
`JarvisSystem._on_channel_message` hard-coded the Telegram-shaped mapping
for ALL channels: `send(cm.channel, reply, conversation_id=cm.conversation_id)`.
Since inbound `ChannelMessage`s carry the channel TYPE label in `.channel`
("discord") and the real destination id in `.conversation_id`, this sent
the literal "discord" as the Discord channel id (HTTP 400
NUMBER_TYPE_COERCE, #515) and passed the channel id as a Discord
`message_reference` (MESSAGE_REFERENCE_UNKNOWN_MESSAGE, #516).
Fix: define and document ONE canonical contract on `BaseChannel.send` —
positional `channel` = destination id, `conversation_id` = inbound message
id used as a reply reference — and dispatch it from `_on_channel_message`
as `send(cm.conversation_id, reply, conversation_id=cm.message_id)`,
matching the already-fixed `ChannelAgent` path (#495/#459). Telegram's
`send()` is brought into line (destination = `channel`, with a
`reply_to_message_id` reply ref and a legacy `conversation_id`-only
fallback) so it keeps working unchanged.
The DiscordChannel `_gateway_loop` ChannelMessage shape locked by #495 is
untouched; `tests/agents/test_channel_agent.py` passes unchanged. The
stale `test_serve_channel_wiring.py` assertions (which encoded the old
buggy mapping from #94) are updated, and regression tests are added for
Discord (real channel id + correct message_reference), Telegram (chat id
+ reply ref + legacy fallback), and per-channel dispatch in
`_on_channel_message`.
Fixes#515Fixes#516
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#463 made the OpenAI-compatible engine wrap upstream HTTP errors (incl.
404) in EngineConnectionError with an actionable message, but
test_invalid_model_404 still asserted the raw httpx.HTTPStatusError, so it
broke on main once #463 landed (#463 was a stale fork PR with no CI, so it
wasn't caught pre-merge). Expect EngineConnectionError now, asserting the
httpx.HTTPStatusError is preserved as the chained cause. Whole tests/engine
suite is green again.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Memory tools degraded silently and misleadingly when the mandatory
`openjarvis_rust` extension was absent from the *serving* venv:
- `POST /v1/memory/store` returned HTTP 200 `{"status":"stored","note":
"no backend available"}` and stored nothing (silent data loss).
- `POST /v1/memory/index` returned a generic "No memory backend available",
and the desktop frontend discarded the server `detail` and threw a blanket
"Failed to index path", blaming the path instead of the real cause.
- `GET /v1/memory/config` reported `backend_type: sqlite` even though no
backend could be constructed.
Root cause: `SQLiteMemory.__init__` calls `get_rust_module()` (which raises
ImportError by design — the Rust ext is mandatory, no Python fallback), and
`_get_memory_backend` swallowed that ImportError and returned `None`,
conflating "native extension missing" (a hard install error) with "memory
intentionally disabled" (benign). A chunking floor also silently dropped whole
short documents, and the installer never verified the extension imported from
the serving venv before writing its success marker.
Fix (no fake Python fallback — the Rust ext stays mandatory by design):
- Add `MemoryBackendUnavailable` + `RUST_MISSING_HINT` in tools/storage/_stubs.
`SQLiteMemory.__init__` translates the bridge ImportError into this clear,
actionable error ("run `uv run maturin develop ...`").
- `_get_memory_backend` distinguishes the two cases: a missing native ext
raises HTTP 503 with the actionable hint; a benign unconfigured backend
still returns `None` (graceful path preserved for search/stats).
- `/store` now returns 503 instead of a 200 silent no-op.
- `/config` reports `available: false` + `detail` instead of falsely claiming
a healthy `backend_type`.
- `/index` adds a `note` when `chunks_indexed == 0` so "indexed" never
silently means "stored nothing".
- chunk_text no longer drops an entire short document below `min_chunk_size`
(the floor only discards tiny *trailing* fragments now).
- Frontend `storeMemory`/`indexMemoryPath` surface the server `detail` instead
of blanket strings; `MemoryConfig` gains optional `available`/`detail`.
(Left the pre-existing `backend` vs `backend_type` mismatch untouched.)
- build-extension.sh verifies `import openjarvis_rust` succeeds in the serving
venv before writing the `extension-built` marker.
Regression tests: tests/server/test_api_routes.py::TestMemoryRustMissing mocks
`get_rust_module` to raise ImportError and asserts /store (503, not 200 no-op),
/index (actionable detail, not "Failed to index path"), and /config
(available:false) all surface the clear error; tests/memory/test_chunking.py
asserts short-only docs are kept while tiny trailing fragments are still
filtered.
Fixes#502
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The package version was reverted from 1.0.2 to 0.1.1, which:
- makes `jarvis --version` / installed metadata report 0.1.1 (the root of
#478 "version unchanged after update" and the version half of #520), and
- drives autotag.yml to compute `0.1.2.dev<count>` — BELOW the real 1.0.x
line — so every push to main now auto-tags and publishes sub-1.0.2 dev
releases to PyPI (latest stable is 1.0.2; dev line was 1.0.3.dev*).
Restore version to 1.0.2 (the last released stable). autotag resumes
`1.0.3.dev<count>`, back on the 1.0.x line, and reported versions are
correct again. Pipeline-compatible (pypi-publish.yml still seds the
tag version). Full hatch-vcs dynamic versioning is a separate follow-up.
Fixes#478. Refs #520.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: forward tools through OpenRouter engine
The OpenRouter chat completion path built the request from only
`model`, `messages`, `max_tokens`, and `temperature`. `tools` and
`tool_choice` passed via `kwargs` were silently dropped, so
function definitions never reached the model. Symptom on a
managed deep_research agent: the model answered every query from
its own prior knowledge and never invoked `knowledge_search`,
`knowledge_sql`, etc.
The same path also discarded `choice.message.tool_calls` from the
response — when a model did return a tool call (verified directly
against OpenRouter with `google/gemma-4-31b-it:free` and
`nvidia/nemotron-3-ultra-550b-a55b:free`), the agent loop never
saw it.
This patch:
- forwards `tools` and `tool_choice` into the OpenAI-compatible
request in both `_generate_openrouter` (sync) and
`_stream_openrouter` (async stream),
- extracts `tool_calls` from the response in `_generate_openrouter`
in the same shape used by the OpenAI / Anthropic paths.
Verified end-to-end against a `deep_research` managed agent using
an OpenRouter preset with Gemma 4 31B + Nemotron 3 Ultra fallback:
before, the agent stated "I don't have access to your vault";
after, it calls `knowledge_search`, cites results, and produces a
structured answer.
* test(engine): regression test for OpenRouter tool forwarding
Asserts the OpenRouter path forwards tools/tool_choice to the
OpenAI-compatible API and parses tool_calls back into the result (#511).
Verified: passes on the fix, fails (KeyError 'tools') against pre-fix main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(chat): honor model config in managed agent chat (#477)
* test(server): regression test for managed-agent engine resolution
_make_lightweight_system must resolve the user's configured engine
(intelligence.preferred_engine, else engine.default) via get_engine,
not a hardcoded OllamaEngine (#477/#514). Asserts the key passed to
get_engine (captured before the system is built); runs under the server
extra (fastapi). Verified: passes on the fix, fails (KeyError) against the
pre-fix hardcoded-Ollama code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(learning): exclude padding tokens from SFT loss
* test(learning): add regression tests for SFT padding-loss masking
Cover the fix in both trainers (#521):
- OrchestratorSFTDataset.__getitem__: labels are -100 at padded positions,
equal to input_ids elsewhere, and input_ids is not mutated.
- LoRATrainer._train_step: the labels passed to the model are masked at
padded positions (captured via an injected model), with input_ids intact.
Both are torch-gated (pytest.importorskip / skipif HAS_TORCH), matching the
project's existing torch test gating, so they skip cleanly in the default CI
env. Verified locally with CPU torch: both PASS against the fix and FAIL
against the pre-fix code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <jonsaadfalcon@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two eval-dataset suites that download real corpora from the
HuggingFace Hub at runtime were running in the default CI lane. When the
Hub was unreachable or rate-limited they failed and reddened `main` even
though no code changed — confirmed by #506 (docs-only) failing on merge
while its own PR run passed an hour earlier. They also dominated CI
wall-time (~33 min of downloads + retry backoff on the failing run).
Add a `hub` pytest marker, apply it to both suites via module-level
`pytestmark`, and exclude it from the default CI lane
(`-m "not live and not cloud and not hub"`). The tests stay runnable on
demand with `pytest -m hub`.
The ADP provider swallows per-config download errors and returns 0
records on a network failure, so a Hub outage surfaced there as
`assert 1 <= 0` (not an exception) — it could not be made non-flaky by
exception handling alone, only by gating.
Coverage holds: removing these from CI drops total from 60.92% to
~60.73% (paranoid worst case 60.18%), still above the 60% gate.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses feedback from our Discord admin curating #config-showcase: the
existing docs land non-technical users straight into Tutorials, which
are script-first and TOML-heavy ("standalone script you can run
immediately, a TOML recipe, a detailed walkthrough"). For a curious-
but-non-technical reader trying to decide whether OpenJarvis is worth
their weekend, that's the wrong first contact — they bounce before they
ever see what the framework can do for them.
This PR inserts a new Showcase tier *above* Tutorials in the docs
information architecture. Each entry is outcome-first: hook sentence,
hero screenshot, 2-3 short paragraphs of personal context, then a
"How I set this up →" link that lands on the relevant Tutorial /
User Guide. The Showcase is the funnel; Tutorials are the build steps.
Five inaugural entries — drafted to be paste-ready for #config-showcase:
- showcase/morning-brief.md — Slack/email/GitHub overnight digest
- showcase/persistent-memory.md — SOUL.md/MEMORY.md/USER.md story
- showcase/cost-savings.md — the public leaderboard as motivation
- showcase/discord-companion.md — DM Jarvis from anywhere
- showcase/coding-assistant.md — code review on an airplane
Plus the contributor template and an assets directory:
- showcase/CONTRIBUTING.md — format skeleton + editorial conventions
(screenshot specs, what to redact, tone)
- assets/showcase/README.md — asset directory conventions
- assets/showcase/*.png — placeholder hero screenshots (1600x1000,
6 KB each, dark gradient) so the gallery
renders cleanly before community
submissions populate real screenshots
Information-architecture changes:
- mkdocs.yml — insert "Showcase" tier between Getting Started and
Tutorials. Funnel order is now: land → "what's possible?" → "build it."
- docs/index.md — new hero card directly under the tagline, pointing to
the Showcase. The research-framework framing stays, but no longer
occupies the first scroll-fold.
CSS:
- docs/stylesheets/extra.css — `.showcase-screenshot` class adds rounded
corners + subtle border so hero images (placeholder or real) read as
intentional rather than as broken-image artifacts.
Validation:
- `uv run mkdocs build` (CI mode) succeeds.
- `uv run mkdocs build --strict` produces zero showcase-specific
warnings. The 18 remaining strict-mode warnings are all pre-existing
on main (`desktop-auto-update.md`, `telemetry.md`, griffe parser
warnings on existing source, mkdocs_autorefs cross-reference issues).
Explicit non-goals (deferred to follow-up PRs in the showcase-tier
roadmap):
- `jarvis showcase` CLI for personal recaps (PR #2)
- Showcase-aligned recipes in `src/openjarvis/recipes/data/` so
"How I set this up →" links into 2-command installs (PR #2)
- Automated screenshot regeneration via Playwright on release tags (PR #3)
- Replacing placeholder PNGs with real screenshots — that happens
organically as community contributors and team members submit their
own setups (see CONTRIBUTING.md for the format)
Co-authored-by: krypticmouse <herumbshandilya123@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Pairs with the Supabase migration that added `methodology_version` to
`savings_entries` and demoted 43 corrupt rows (pre-fix telemetry from
March 2026) to version 0. Adds `methodology_version=gte.1` to the
public leaderboard fetch URL so the quarantined rows hide at the query
layer — fewer bytes over the wire than client-side outlier filtering,
and forward-compatible: new clients write `methodology_version >= 1`
and remain visible.
The leaderboard's existing client-side outlier thresholds stay in place
as a second line of defence in case any future v >= 1 row slips past
the bounds.
Smoke-tested live: anon-key fetch against
`mtbtgpwzrbostweaanpr.supabase.co` returns 291 rows (was 334), top
result is the largest-savings user with v=1. A separate `eq.0` probe
confirms quarantined rows are skipped only by the filter (no RLS
policy in place yet — flag for follow-up if you want hard isolation).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(leaderboard): correct telemetry pipeline and outlier handling
The public leaderboard at /leaderboard showed a clear bimodal Wh/token
distribution: most users at ~3-5 J/token, ~30% inflated by 1000-4000×.
A 5-agent investigation workflow + 4-agent verification pass traced the
inflation to a cluster of related bugs across telemetry, server, and
display layers. This PR fixes the in-repo half. Backfilling existing
Supabase data is a separate follow-up.
Bug 1 — Dual telemetry recording (`server/routes.py`)
=====================================================
`_handle_direct` wrapped the engine with `instrumented_generate`
unconditionally. When `app.state.engine` was already an
`InstrumentedEngine` (the common case when telemetry is wired in),
BOTH layers published `TELEMETRY_RECORD` — once from the inner
`InstrumentedEngine.generate`, once from the outer wrapper. Every
chat-completion request was counted twice in the leaderboard pipeline.
Fix: detect the InstrumentedEngine and unwrap to `._inner` before
passing to the wrapper so only one layer fires.
Bug 2 — KV-cache fallback over-counts multi-turn (`server/savings.py`)
=====================================================================
`compute_savings` falls back from `prompt_tokens_evaluated` to
`prompt_tokens` when the KV-cache-aware count is missing. But routes.py
aggregates by summing each turn's full prompt — which counts the system
prompt N times for an N-turn conversation. The fallback inflated FLOPs
and energy by N×.
Fix: use 0 (conservative under-count) when the evaluated count is
missing rather than falling back to the inflated `prompt_tokens` sum.
Bug 3 — TelemetryRecord lacked methodology versioning
=====================================================
There was no per-record version tag, so legacy (pre-fix) and current
records were silently aggregated together in the public leaderboard
even though they used different methodologies.
Fix: add `token_counting_version: Optional[int]` field to the
`TelemetryRecord` dataclass + a nullable column to the SQLite schema
(with idempotent migration); the constant moves from `server.savings`
to `core.types` to avoid the server→telemetry layering. New records
write the current version; pre-fix rows remain NULL. The aggregator
gains a `current_methodology_only=True` flag that filters NULL rows out
of leaderboard sums — local dashboards leave it off so historical
aggregates still render.
Bug 4 — Leaderboard JS displayed missing telemetry as legit zeros
=================================================================
Rows with significant token counts but `energy_wh_saved = 0` and
`flops_saved = 0` rendered as `0.00 Wh / 0 FLOPs` — visually identical
to a user who genuinely did almost nothing. The headline totals also
included these rows.
Fix: new `isMissingTelemetry()` detector renders missing-telemetry
energy/FLOPs cells as `—` (with a tooltip explaining why); a new
`.lb-missing` CSS class differentiates the placeholder visually.
Bug 5 — Outlier filter was too generous
=======================================
`MAX_ENERGY_WH_PER_TOKEN = 10` left a 10,000× margin that admitted
every Group B row even though those values are physically impossible
(would imply a space-heater per token). Same for the FLOPs cap at
`1e17`.
Fix: tighten to `0.5` Wh/token (still 500× over a typical consumer GPU)
and `1e15` FLOPs/token (still 10,000× over typical). This removes
existing pre-fix corrupt rows from the public view without touching
Supabase.
Tests
=====
New regression tests (all pass, ruff clean):
- `tests/server/test_routes.py::test_instrumented_engine_unwrapped_to_avoid_dual_telemetry`
— pins the bug 1 fix; asserts exactly ONE TELEMETRY_RECORD event per
request when the engine is already an InstrumentedEngine.
- `tests/server/test_savings.py` (NEW file, 3 tests) — pins the bug 2
fix (FLOPs not inflated via fallback) and the cost-side invariant
(dollar savings still use full prompt_tokens because cloud providers
bill per input token even when local KV cache hit).
- `tests/telemetry/test_aggregator.py::TestMethodologyFilter` (3 tests)
— default behaviour includes legacy rows (local dashboard parity);
`current_methodology_only=True` excludes them; the summary surface
honors the same filter.
Unrelated test note
===================
`tests/telemetry/test_energy_wiring.py::TestTelemetryStatsEnergy::test_export_includes_energy_fields`
and `::TestEndToEndPipeline::test_ask_to_export_with_energy` are flaky
on this branch but ALSO flaky on `main` (confirmed via stash + the
banner-only run). Root cause: `_version_check.py:132-135` prints the
"new version of OpenJarvis is available" banner to stdout outside the
throttle window, contaminating `json.loads(result.output)` in those two
tests. Reproducible with `OPENJARVIS_NO_UPDATE_CHECK=1` → all pass.
That's a separate bug (the banner shouldn't write to the same stream
as machine-readable output); not addressing it here to keep this PR
focused on the leaderboard pipeline.
What this PR explicitly does NOT do
====================================
- Backfill ~30% of Supabase rows that already shipped with inflated
values from pre-fix clients. That requires Supabase write access and
is the natural follow-up — see PR comments for proposed dry-run audit
query and the additive `methodology_version` column migration.
- Distinguish which past submissions came from buggy vs correct clients
retroactively (no `app_version` tag on existing submissions).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(evals): fix test_energy_scales_linearly under leaderboard fix
CI on #498 failed in the slow `test` job:
tests/evals/test_use_case_benchmarks.py::TestSavings::
test_energy_scales_linearly — ZeroDivisionError: float division by zero
Root cause: the test (and `test_energy_wh_matches_direct_formula`)
called `compute_savings(N, 0)` without `prompt_tokens_evaluated`. Under
the OLD buggy fallback, an unset evaluated count silently became N,
energy scaled linearly with N, and the test passed by accident. Under
this branch's conservative fix the fallback is 0, FLOPs collapse to 0,
and the `p10.energy_wh / p1.energy_wh` ratio divides 0 by 0.
The test's own docstring says "evaluated tokens (KV-cache model)" — it
was always meant to exercise the explicit-evaluated path, the API
call just didn't match. Pass `prompt_tokens_evaluated` explicitly so
the test now expresses the invariant it claims to.
Same one-line fix for `test_energy_wh_matches_direct_formula` plus an
explicit `flops > 0` sanity assertion — that test was passing trivially
with `0 == 0` after the conservative fallback fix, masking whether the
formula was actually being exercised.
No production code change in this commit. Verified locally: all 6
TestSavings tests pass; the 5 new regression tests added on this branch
still pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: krypticmouse <herumbshandilya123@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Closes#455. @wishwanto on Linux Mint hit two distinct bugs in the desktop launcher that ship together because they share the same boot_backend code path.
Bug A — AppImage hang on "starting server":
When the desktop is shipped as an AppImage on Linux, the AppImage runtime sets LD_LIBRARY_PATH to its temp-extracted lib dir. Children we spawn (uv, ollama) inherit that env, then Python's numpy/cryptography extensions dlopen the AppImage's mismatched libstdc++/libssl versions and python dies silently. Stderr drainer sees immediate EOF → GUI hangs forever.
Fix: new helper prepare_subprocess_for_appimage strips LD_LIBRARY_PATH, LD_PRELOAD, APPIMAGE, APPIMAGE_UUID, APPDIR, ARGV0 when $APPIMAGE is set. Linux-only via #[cfg(target_os = "linux")] — true no-op on macOS/Windows. Called at all 3 spawn sites: ollama sidecar, uv sync, uv run jarvis serve.
Bug B — Desktop force-kills the user's already-running `jarvis serve`:
Old code (post #437) ran fuser -k 8000/tcp / taskkill /PID /F on ANY HTTP response from :8000/health — including 200 OK from a healthy user-launched serve.
Fix: replace the indiscriminate kill with a health-aware decision tree:
* 2xx /health → confirm with a 500ms-apart second probe, then attach (set server/model/ollama ready, return without spawning)
* 503 → user-facing error "wait for engine or stop it"
* other 4xx/5xx → user-facing error with lsof -i :8000 (unix) / netstat (windows) hint
* Err → fall through to normal spawn (unchanged)
Adversarial review caught and fixed 5 real issues pre-commit:
- HIGH: parallel cargo-test env mutation race → APPIMAGE_ENV_LOCK Mutex
- HIGH: 2xx attach left model_ready/ollama_ready false → set both true before return
- MEDIUM: #[allow(unused_variables)] suppressed lint on Linux too → cfg_attr
- MEDIUM: single-probe attach trusted a 2s snapshot → confirmatory second probe
- MEDIUM: /bin/true would silently mislead on Windows → HARMLESS_BIN const per platform
2 new unit tests; CI green on all gates including rust.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes#459. Reported by @jasonftl with a precise smoking gun: incoming ChannelMessage has channel="discord" (TYPE label) and conversation_id=<numeric channel id>, but the reply code was using cm.channel as the destination — Discord saw /channels/discord/messages and 404'd silently, blackholing every reply.
The bug was two field-mappings in channel_agent.py:_process_message (the happy path + the exception path):
self._channel.send(
msg.channel, # WRONG — TYPE label
reply,
conversation_id=msg.conversation_id, # WRONG — channel id used as msg-ref-id
)
The existing DiscordChannel.send() contract — proved by the existing test_send_with_conversation_id test — is:
- first positional `channel` = native destination ID (Discord channel id)
- `conversation_id` kwarg = native message ID for reply threading
Fix: swap both fields to the correct ones from ChannelMessage:
self._channel.send(
msg.conversation_id, # Discord channel id
reply,
conversation_id=msg.message_id, # message id for threading
)
Plus a defensive guard in discord_channel.py: when channel is empty, refuse fast with a clear warning instead of POSTing to /channels//messages and silently 404'ing.
3 new tests, 38 affected pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes#461. Reported and empirically validated by @swilliams76360.
Two bugs prevented authenticated MCP servers (e.g. Home Assistant) from working with OpenJarvis:
1. StreamableHTTPTransport never sent Authorization: Bearer <token> — constructor didn't accept a token kwarg and _build_headers() never set the header. Authenticated MCP servers always returned 401.
2. jarvis ask and jarvis serve never iterated config.tools.mcp.servers — only loaded tools from ToolRegistry. MCP tools were silently dropped on every CLI invocation.
The reporter's 3-file fix was correct; the workflow investigation surfaced a 4th file (agent_manager_routes.py:695, identical broken code) and an adversarial-review catch (MCP clients in _build_tools would be GC'd on function return, closing transports mid-request — fixed by stashing on agent._mcp_clients).
Edits:
- transport.py: token kwarg + Authorization header (skips on empty/None — avoids malformed "Bearer " that triggers confusing 400s).
- mcp/loader.py (NEW): shared load_mcp_tools_from_config helper returning (tools, clients). Caller MUST hold the clients reference.
- builder.py + agent_manager_routes.py: extract cfg.get("token"), forward to transport.
- cli/ask.py: _run_agent calls the loader, dedupes by spec.name (registry wins), stashes clients on agent._mcp_clients.
- cli/serve.py: same pattern in main-agent AND channel-agent paths; mcp_clients initialised before the accepts_tools branch so the post-instantiation reference is always valid.
22 new tests (transport + loader + discovery updates), 179 total cli/server/mcp tests pass on this branch.
Adversarial review interrogated 10 angles — slotted-class attr safety, MCPConfig duck-typing, config.tools.mcp AttributeError risk, dedup precedence, token leak via str(exc), logger scope in serve.py, _mcp_clients shadowing, _channel_mcp_clients lifetime, empty-token future-compat, lazy-import cost shift. Nine non-issues; the tenth (theoretical token leak via httpx exception str()) assessed as low actual risk because the token is a header value, not URL-embedded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds --persona NAME / --persona none and a [memory_files].persona_name config field, resolving ~/.openjarvis/personas/<name>/{SOUL,MEMORY,USER}.md. Default (empty) preserves today's global-persona behavior exactly. Includes a path-traversal guard on persona names. Resolution lives in SystemPromptBuilder._resolve_persona so all callers benefit. Squad-derived: Ada (Qwen3.6-27B) authored the spec independently from the issue+code; Lucy (Qwen3-Coder-Next, 80B-A3B / ~3B active) implemented it; cross-function param threading completed in the test phase. 21 existing tests pass; +8 new persona-scope tests.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidates two reports that overlap in scope:
- #476 (@senki): install.sh hardcoded `--python 3.11` but
pyproject.toml declares `requires-python = ">=3.10,<3.14"`. The
installer should track the project's allowed range, not pin a
conservative-three-years-ago version.
- #484 (@sanjayravit): install.sh crashed on hosts with no
python3/python on PATH because `mark_done`, `beacon`, and
`get_anon_id` all called $PY_CMD via inline Python heredocs.
This PR closes both gaps with five surgical edits to install.sh
(all behavior-preserving for the existing happy path; the bats
tests prove it):
1. get_anon_id — replace `python3 -c uuid` with POSIX
`/dev/urandom + od` + bash substring expansion. Same UUID v4
shape; works on Python-less hosts.
2. beacon — replace the 40-line Python heredoc with curl + a
shell-built JSON payload. All inputs are from controlled
sources (event ∈ fixed vocabulary, stage from stage_label(),
numeric ids/codes from validated arithmetic, anon_id from a
fresh UUID) so no general-purpose JSON escaping is needed.
`|| true` is load-bearing — PostHog 5xx must never abort an
install via the ERR trap.
3. mark_done — replace `python3 -c json.load+update+dump` with
awk that regenerates the file from scratch. Idempotent: if
the key is already marked, return early. Robust against
prior format drift. wsl key is always rewritten last so a
later FORCE_WSL=1 re-run correctly updates it.
4. parse_requires_python — new helper. Greps the project's
requires-python field, handles inclusive (`<=3.13`) and
exclusive (`<3.14`) upper bounds correctly, falls back to
3.11 if pyproject can't be parsed (the previous hardcoded
value — safe under the existing 3.10-3.13 range).
5. create_venv — call parse_requires_python instead of
hardcoding 3.11. The existing uv-managed-Python fallback
(from #444) still kicks in if the host doesn't have the
target version installed.
Adversarial review caught two real bugs before commit:
- HIGH: parse_requires_python's exclusive-bound regex would
also match the digits after `<=` (inclusive bound) and then
incorrectly subtract 1 — producing 3.12 from `<=3.13`. Fixed
by checking the inclusive form first.
- MEDIUM: the new "no Python on PATH" bats test silently skips
symlinking `pgrep` on hosts where it isn't at /usr/bin or
/bin (some minimal BusyBox configurations). Added an explicit
"no matching process" fallback so start_ollama's check works.
New bats coverage:
- `install succeeds with no system Python on PATH (#484)` —
builds a PATH that excludes python3/python and exercises the
full install. Asserts state file is written and contains
greppable step keys.
- `mark_done is idempotent — second mark of same key doesn't
duplicate` — re-runs the install and verifies install_uv
appears exactly once in install-state.json.
- `create_venv picks newest in requires-python range, not
hardcoded 3.11 (#476)` — asserts uv was called with
`--python 3.13` (the upper minor of `>=3.10,<3.14`).
The git stub now includes `requires-python = ">=3.10,<3.14"`
in its fake pyproject.toml so create_venv has something
realistic to parse.
@sanjayravit — your PR #484 motivated this consolidation;
closing that one as superseded with credit.
Co-authored-by: krypticmouse <herumbshandilya123@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
A [system_prompt] prefix set in config.toml was silently ignored: (1)
load_config()'s section allowlist dropped the [system_prompt],
[memory_files], [compression], and [skills] blocks entirely; (2)
SystemPromptConfig had no prefix field; (3) the builder never prepended
one.
Add the four blocks to the allowlist, add prefix: str = "" to
SystemPromptConfig, and prepend a "prefix" PromptSection at the front of
SystemPromptBuilder's frozen sections so it leads build() output and is
exposed via sections() (#457). Empty prefix emits no section — existing
configs are byte-for-byte unchanged.
Rebuilt on top of #457 (which refactored the builder to PromptSection
objects); the original #452 by @SoulSniper-V2 patched the pre-#457
method. Credit to @SoulSniper-V2. Adds the regression tests the original
PR lacked: config parse + prefix-prepended + empty-prefix-unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rlm: expose real tool calls inside the repl
* rlm: wrap long TypeError message in repl
* style(rlm): collapse over-wrapped TypeError to satisfy ruff format
The cherry-picked RLM tool-call work left a `raise TypeError(\n message\n)`
that `ruff format --check` rejects (the PR's own lint-fix commit broke
format). Collapse to `raise TypeError(message)`. No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Eddie Richter <eddie.richter@amd.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
web_search returned thin, unlabeled results (Tavily default search_depth,
**title**/url/content blob), so small models in the native_react loop
tended to echo URLs instead of synthesizing content (#390). Query Tavily
with search_depth="advanced" and format each result as a labeled block:
### {title}
Source: {url}
Summary: {content or snippet}
joined by `---` separators. DuckDuckGo fallback uses the same labeled
shape. Falls back to a result's `snippet` when `content` is absent.
Extracted from #448 (the web_search portion only; that PR also bundled
an unrelated install.sh rewrite, left out of scope here). Credit to
@sanjayravit for the original fix. Adds tests asserting the labeled
format, the snippet fallback, and search_depth="advanced"; updates the
max_results test for the new call signature.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The run-as-root install case doc still showed the retry command using
the community-operated openjarvis.ai domain, whose TLS is broken and
which the project does not control (#337). Point it at the canonical,
project-controlled GitHub Pages URL, matching README and the install
docs. Documentation only — the bats test it describes asserts exit
code + "root" in stderr and has no URL dependency.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>