Train a single ~9B orchestrator (Qwen3-8B) for local<->cloud routing via SFT
on synthetic traces derived from NeuLab ADP (agent-data-collection).
- sft_data/: full ADP trajectory loader, tier model + pricing, 4 paradigm
renderers, reward-ranked selection, conversations-JSONL serializer, CLI
- wire sft_trainer._generate_traces() to the pipeline (no GPU / no API keys)
- Qwen3-8B SFT config + scripts/orchestrator CLI wrapper
- offline fixture tests (12)
Heuristic competence labels for the cold-start; real paradigm execution +
GRPO are the documented v2.
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>
When `jarvis serve` runs with an API key configured, AuthMiddleware 401s
every /v1 and /api request that lacks a Bearer token. The frontend never
sent one, so telemetry, managed-agents, savings, etc. all failed (#266).
- Add getApiKey() (reads settings.apiKey, with optional
VITE_OPENJARVIS_API_KEY build-time override) and authHeaders() to
api.ts, plus an apiFetch() wrapper that prepends getBase() and injects
the Bearer header on every local-server call. Route all /v1 + /api
fetches through it so none can omit auth.
- Add `apiKey` to the Settings model (store.ts) and a password field in
Settings → Connection so users can enter it.
Keyless local servers are unaffected: with no key, no Authorization
header is sent (byte-for-byte unchanged). The Supabase savings path keeps
its own anon key — not conflated with the local key.
Bootstraps vitest (no prior frontend test runner) + a `test` script, and
adds api.auth.test.ts covering getApiKey/authHeaders. Verified: tsc
--noEmit clean, vitest 6/6, vite build succeeds.
Deferred (not in scope): WebSocket auth (browsers can't set WS headers)
and Tauri auto-injecting a generated key.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`jarvis serve` startup was slow on two counts addressed here:
1. discover_engines() probed each registered engine's health() serially,
and every probe is a blocking network check with its own ~2s timeout —
so N dead/slow localhost ports cost N*2s. Run the probes concurrently
in a ThreadPoolExecutor; the existing healthy.sort() normalizes order,
so the result is identical to the serial version. health() impls are
read-only on per-instance HTTP clients with no shared mutable state.
2. The PyPI update check ran a blocking urlopen (up to 3s on a cache
miss) inline before dispatch, delaying every command. Move it to a
daemon thread — it's best-effort and never raises.
Together these remove ~10-30s from cold startup. Adds a regression test
asserting discovery probes overlap (concurrency), not just that output
is unchanged (covered by existing tests).
Note: the larger ~30-40s win — the duplicate SystemBuilder.build() in
serve.py — is NOT addressed here; it's not redundant (the second build
wires a JarvisSystem the inline path never constructs) and needs a
design pass. Deferred.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Rust workspace fails to build on stable 1.86/1.87 with cryptic E0658
errors deep in dependencies: rig-core uses let-chains and
openjarvis-skills uses `is_multiple_of`, both stabilized in 1.88 (#252).
Add a rust-toolchain.toml pinning channel 1.88 (rustup then auto-selects
a working toolchain instead of erroring mid-build) and declare
rust-version = "1.88" in [workspace.package] to self-document it.
Because the toolchain pin makes CI's `cargo clippy -D warnings` run under
1.88 — whose clippy enables `uninlined_format_args` — also apply the
mechanical `format!("{}", x)` -> `format!("{x}")` rewrites across the
workspace (via `clippy --fix`; string output is identical, no logic
change). Verified clippy + fmt + `cargo test --workspace` clean on BOTH
1.88 and current stable.
Verified locally: cargo +1.86 and +1.87 fail (E0658), +1.88 builds and
tests cleanly. Supporting true 1.86 is infeasible without downgrading
rig-core below the versions exposing the token-usage symbols we use —
deferred.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(security): detect Rust at import time + SSRF Python fallback (#225)
RUST_AVAILABLE was hardcoded to True, so the exported flag never
reflected reality and the pure-Python fallbacks it was meant to gate
were unreachable.
- _rust_bridge: compute RUST_AVAILABLE dynamically by probing the
compiled extension once at import time.
- security.ssrf: check_ssrf now falls back to the existing
_check_ssrf_python implementation when the Rust extension is not
built, instead of raising ImportError. The SSRF guard is
security-critical and must never be silently skipped or crash just
because Rust was not compiled.
- tools.browser: drop the `except ImportError: pass` around the SSRF
check, which previously disabled SSRF protection entirely on installs
without the compiled backend (internal/metadata endpoints reachable).
- tests: cover the Python fallback path (metadata IP, private IP, and
public URL) with RUST_AVAILABLE patched False.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(security): make test_no_hostname backend-agnostic
The cherry-picked #451 fix adds a pure-Python SSRF fallback, but
test_no_hostname asserted the Rust-specific message "Invalid URL". The
Python fallback returns "No hostname in URL" for the same input, so the
SSRF suite failed on exactly the uncompiled-install path #451 targets
(in CI the Rust extension is built, masking it).
Assert the security behavior (URL blocked, non-None reason) and accept
either backend's wording, so the suite passes on both the Rust and
Python paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(tools): update browser SSRF test for non-bypassable check
The #225 fix removes the `except ImportError: pass` that silently
disabled the SSRF check in browser navigation. The existing
test_execute_ssrf_module_missing asserted that very anti-pattern ("skip
check and proceed"), so it failed once the swallow was removed.
Replace it with tests that assert the SECURE behavior: the SSRF check
runs unconditionally and is honored (a private-IP URL is blocked even
when navigation would otherwise succeed), and a public URL still
navigates. check_ssrf's pure-Python fallback means the import never
fails anymore, so the old skip path no longer exists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Rahul <therahulll56@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(server): load SOUL.md / USER.md context in streaming chat
* refactor+test: extract _build_managed_system_prompt + cover #431
The streaming persona fix was inline and untestable without a live
engine. Extract it into _build_managed_system_prompt (matching this
module's extract-and-unit-test pattern for the streaming helpers) and
add regression tests:
- SOUL.md persona is injected into the streaming system prompt (#431),
- the agent's own template is preserved,
- output matches a directly-constructed SystemPromptBuilder (parity with
the CLI/ask path — the whole point of the fix).
Behavior unchanged from the original PR; this only makes it testable and
locks in CLI parity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some OpenAI models (e.g. gpt-5, the default for a fresh cloud install)
reject a non-default temperature with HTTP 400 "Unsupported value:
'temperature' does not support 0.7 ... Only the default (1) value is
supported." — so the user's very first prompt fails (#426).
Detect that specific 400 (param=temperature + unsupported_value/"only the
default"/"does not support") and retry the create() once without
temperature, mirroring the tools-400 retry in the Ollama and
OpenAI-compat engines. Unrelated 400s are re-raised unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`jarvis agents list` crashed with a secondary Rich MarkupError when the
underlying exception message contained markup metacharacters like
`[...]`: the error handler did `console.print(f"[red]Error: {exc}[/red]")`,
and Rich re-parsed the interpolated message as markup (#297).
Escape the dynamic message with rich.markup.escape and keep only the
static "Error:" label styled, so the original error surfaces cleanly
instead of a traceback. Adds a regression test that reproduces the
MarkupError via an exception message with brackets.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apple FM's stream_response yields cumulative text snapshots, but
OpenAI-compatible clients concatenate delta.content — so streamed
responses were duplicated/stuttered (#378). Diff each snapshot against
the last and emit only the incremental suffix; fall back to the full
snapshot if the model revises earlier text, and skip empty deltas.
Rebased on #377 (apple_fm_sdk migration): uses the options= streaming
API. Adds a stubbed-SDK regression test asserting deltas are
incremental, not cumulative.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(engine): modernize apple_fm_shim for the public apple-fm-sdk
Apple released the official Foundation Models Python SDK as
`apple/python-apple-fm-sdk` (import name `apple_fm_sdk`, distribution
name `apple-fm-sdk`). The shim's `import apple_fm` predates the public
SDK and the per-call API has since moved as well; running the shim
against current `apple-fm-sdk` v0.1.1 fails on import, then on the
`/health` call shape, and again on every `respond` / `stream_response`
keyword.
This change brings the shim up to date with the real SDK without
altering its external OpenAI-compatible contract:
- Import `apple_fm_sdk` (the public name). Update the missing-package
error to point at the GitHub repo since the SDK isn't on PyPI;
installation is `uv pip install -e <clone>`.
- `SystemLanguageModel.is_available()` is an instance method and now
returns `(bool, SystemLanguageModelUnavailableReason | None)`. The
health endpoint instantiates the model and unpacks the tuple, and
surfaces the reason string when the model is unavailable.
- `LanguageModelSession.respond` and `.stream_response` no longer
accept `max_tokens` / `temperature` positionally; they take a
`GenerationOptions` instance via the `options` kwarg. The shim now
builds a `GenerationOptions(temperature=..., maximum_response_tokens=...)`
from each `ChatRequest` and threads it through both paths.
`temperature` is now actually honored (the previous code dropped it
silently).
- Add `response_model=None` to the `/v1/chat/completions` decorator so
FastAPI doesn't try to build a Pydantic field for the
`JSONResponse | StreamingResponse` union return type — that fails
with the FastAPI version pinned in the `server` extra.
- Update the module docstring to reference macOS 26 + Apple
Intelligence (the SDK's actual minimum), not macOS 15.
## How was this tested?
- `uv pip install -e ./python-apple-fm-sdk` against a local clone of
Apple's repo on macOS 26 / M5 Max with Apple Intelligence enabled.
- `uv sync --extra dev --extra server` then `uv run uvicorn
openjarvis.engine.apple_fm_shim:app --host 127.0.0.1 --port 8079`.
- `GET /health` → `{"status": "ok"}` (200).
- `GET /v1/models` → lists `apple-fm`.
- `POST /v1/chat/completions` with messages + temperature +
max_tokens → returns a real Apple Intelligence completion.
- End-to-end through OpenJarvis: add `[engine.apple_fm]
host = "http://localhost:8079"` to `~/.openjarvis/config.toml`,
then `jarvis ask --engine apple_fm --model apple-fm "..."` returns
the same Apple FM response routed through the OpenAI-compatible
engine wrapper.
- `uv run ruff check src/openjarvis/engine/apple_fm_shim.py` and
`uv run ruff format --check src/openjarvis/engine/apple_fm_shim.py`
both pass.
* test(engine): add stubbed-SDK tests for apple_fm_shim migration
Covers the apple_fm -> apple_fm_sdk migration: GenerationOptions carries
temperature + max_tokens and is passed via options= to respond() and
stream_response(), and /health unpacks the (available, reason) tuple
from SystemLanguageModel().is_available().
The real apple-fm-sdk is not installable in CI (not on PyPI; macOS 26 +
Apple Intelligence only), so the tests inject a stub SDK into
sys.modules. They verify the shim's OpenAI-compat wiring, not Apple's
real SDK behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: copy package includes in Docker build
* fix: copy package includes in GPU Docker builds too
PR #450 fixed the CPU Dockerfile but Dockerfile.gpu and
Dockerfile.gpu.rocm have the identical bug: they COPY src/ then
`uv pip install ".[server]"` without copying the non-src
force-include paths (scripts/install, deploy/windows), so hatchling's
wheel build fails the same way on GPU images (#447).
Adds the two COPY lines to both GPU Dockerfiles and generalizes the
regression test to guard every wheel-building Dockerfile (CPU + both
GPU variants) instead of only the CPU one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jon Saad-Falcon <41205309+jonsaadfalcon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a client streams (stream:true) with explicit `tools`, the server
routed to the agent stream bridge, which ignored request_body.tools, ran
the agent's own tool loop, and word-split filler content into fake token
deltas — dropping the caller's tool_calls. This is the streaming analog
of #414 (whose non-streaming fix was #454).
Now stream+tools bypasses the agent and streams the model's raw
function-calling decision via engine.stream_full(), emitting OpenAI-shape
tool_calls deltas and a tool_calls finish_reason. Adds tool_calls to
DeltaMessage and removes the now-dead _handle_agent_stream.
Verified end-to-end on Ollama (qwen3.5:4b) plus a unit regression test.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#414.
Root cause: routes.py:151 unconditionally routed non-streaming /v1/chat/completions through _handle_agent when an agent was registered. _handle_agent calls agent.run(input_text) which IGNORES request_body.tools entirely, runs the agent's own internal tool loop with its own (different) tool spec, and returns only result.content — never result.tool_calls. The "Understood. If you have another request..." filler is not hardcoded anywhere in OpenJarvis (the cloud_router.py:126 "Understood." is a different Gemini-only injection). It's the model's actual generic response when the agent re-prompts it without the user's intended tools.
Fix: one conditional. Skip _handle_agent when request_body.tools is present — the client is asking for raw OpenAI-compat function-calling, so route to _handle_direct which preserves tool_calls. Plus a forward-looking comment documenting this as an intentional trade-off so a future maintainer doesn't naively remove the guard.
Streaming path left intact (its asymmetry — "use agent_stream WHEN tools present" — is intentional per the existing comment at lines 143-145; reporter's repro is non-streaming).
Two regression tests:
- test_with_tools_bypasses_agent: mocks engine+agent, asserts tool_calls survives, agent.run is NOT called.
- test_without_tools_still_uses_agent: pins existing behavior for the no-tools path.
Reported by @gilbert-barajas — the side-by-side curl repro made the triage tractable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR B of the post-cluster install-hardening pair (PR A: install.sh #444 — merged). The audit found native Windows was the worst path at 2/10 zero-friction: the installer refused on every missing prereq (Python / git / Ollama), didn't pull a model, and gave the user no `jarvis` command after install. This closes those gaps.
Changes in deploy/windows/install.ps1:
1. Auto-install Python via winget when missing (was: hard refuse).
2. Auto-install git via the same Install-WithWinget helper.
3. Auto-install Ollama via the official OllamaSetup.exe (NSIS /S flag, $ProgressPreference SilentlyContinue for the 150 MB download).
4. Wait for Ollama daemon health before pulling the model (60s poll on `ollama list`).
5. Pull qwen3.5:2b foreground (~1.5 GB) — banner is honest if pull failed.
6. jarvis.cmd shim at %LOCALAPPDATA%\OpenJarvis\bin\ added to User PATH (deduped against expanded form so re-runs don't append). Shim uses %~dp0..\src to self-locate and uv from PATH so a future uv update can't break it.
7. Pre-check admin for the scheduled-task path; refuse fast in -Service branch if not elevated, default to skip-with-explanation in the interactive branch.
8. Final banner tells the truth: 'jarvis' if all good, 'jarvis doctor' if model missing, 'open a new PowerShell' if User PATH was just updated.
Adversarial review caught 5 real bugs before commit:
- CRITICAL: GetEnvironmentVariable returns REG_EXPAND_SZ raw → %LOCALAPPDATA% wasn't expanded → just-installed Python invisible. Wrap in ExpandEnvironmentVariables.
- HIGH: Ollama NSIS silent flag is /S not /silent (wrong flag opens GUI, hangs install).
- MEDIUM: PS 5.1 progress bar makes Invoke-WebRequest 30x slower.
- LOW: -Service branch missed isAdmin precheck.
- LOW: PATH dedup missed unexpanded %VAR% entries → duplicate every re-run.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR A of the post-cluster install-hardening pair. A multi-agent audit (4 install paths × 2 agents each) showed the README's "copy this one-liner and chat" claim broke on every fresh laptop because of missing git, missing Python 3.11, a racing Ollama daemon, and silently-swallowed model-pull failures. This PR closes the macOS / Linux / WSL2 gaps.
Changes in scripts/install/install.sh:
1. Auto-install missing tools (was: hard refuse):
- macOS: xcode-select --install + 10-min poll. Refuses fast under SSH (no display for the dialog).
- Linux: detects apt-get / dnf / yum / pacman / zypper / apk. Pre-checks `sudo -n true` and refuses fast with actionable guidance if sudo would prompt (stdin is the curl pipe — any prompt silently hangs under `set -euo pipefail`). Uses `;` not `&&` between apt-get update and install. Skips sudo entirely if already root.
2. Auto-install Python 3.11 via uv when missing. Captures real errors to $STATE_DIR/venv-create.err so disk-full / permission-denied isn't hidden behind "downloading managed Python".
3. Replace `sleep 1` after `ollama serve` with a 60-second poll on `ollama list`. Caller guards with `|| true` so timeout surfaces as a warning in the final banner instead of aborting under `set -e`.
4. Track MODEL_PULL_OK + PATH_MODIFIED. The completion banner now tells the truth: if the model pull failed, point at `jarvis doctor` (not bare `jarvis` which would crash). If PATH was just written to ~/.bashrc / ~/.zshrc, print the exact `source <rc> && jarvis` so it works in the same shell.
Adversarial review caught 5 real bugs before commit:
- SSH/headless macOS xcode-select hang → pre-detect SSH session
- sudo no-TTY silent abort → `sudo -n true` precheck
- wait_for_ollama timeout tripping ERR trap → `|| true` at call sites
- swallowed venv stderr hiding diagnostics → capture to log + surface on fallback failure
- contradictory "PATH new + model missing" banner → conditional NEXT_CMD
All 26 install bats tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Resize embed: width="100%" → width="75%".
- Re-encode WebP with a 4px #3a3a3a border baked in (GitHub's README
sanitizer doesn't reliably honor inline CSS borders on <img>, so the
frame is part of the asset).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub's README sanitizer strips <video> tags sourced from release
assets, so the previous embed never rendered. Swap for an animated
WebP (no audio anyway) at assets/openjarvis_demo_reel.webp — renders
inline on every Markdown viewer with auto-loop.
960px wide, 15fps, lossy q=60, 4.5MB.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tidies the README after the Windows cluster (#432-#438) landed. The Install section had grown three nested Windows sub-bullets, and Quick Start + Starter Configs both re-listed the same presets. -49 net lines, no content lost.
- `## Installation` is now a 3-row table (macOS·Linux·WSL2 / Native Windows / Desktop GUI), one one-liner each. Per-platform detail moves to the docs.
- `## Quick Start` absorbs Starter Configs into one preset table + one example + a row of per-preset deep-dive links.
- New "Platform-specific guides" hub at the top of `docs/getting-started/install.md` links to the existing `macos.md` / `linux.md` / `wsl2.md` / `windows-native.md` siblings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(readme): embed demo reel video in hero section
Adds a centered video block between the badges and the Documentation
links, framed by horizontal rules. URL is a placeholder pending the
user-attachments upload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(readme): point demo reel at readme-media release asset
Replaces the placeholder with a <video> tag sourced from the dedicated
readme-media prerelease.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes Phase-1 of #298 (Native Windows Support RFC).
Reverses the long-standing "native Windows is not supported" stance. PowerShell installer at `deploy/windows/install.ps1` (published to https://open-jarvis.github.io/OpenJarvis/install.ps1) plus a `jarvis-service.ps1` scheduled-task helper that mirrors `deploy/systemd/openjarvis.service` and `deploy/launchd/com.openjarvis.plist`. Loopback default (127.0.0.1, no API key) — same as launchd.
One-liner install:
irm https://open-jarvis.github.io/OpenJarvis/install.ps1 | iex
Adversarial review caught and fixed two real bugs pre-commit:
1. `irm | iex` drops `param()` flags — added env-var fallbacks (OPENJARVIS_SKIP_SERVICE / OPENJARVIS_SERVICE / OPENJARVIS_FORCE).
2. Scheduled tasks don't inherit the registering session's env — the LAN-exposed `OPENJARVIS_API_KEY` path now persists the key to User scope so the task's logon environment can read it.
Supersedes #434 (the guidance-only "use WSL2" install.ps1).
Massive thanks to @SeCuReDmE-main-dev for the careful RFC #298 — the three-phase decomposition (install / service / shared-memory bridge) is exactly the right framing. This PR ships Phase-1 and Phase-2 of the RFC fused into one release; Phase-3 (shared memory bridge) remains future work.
Thanks also to @KadenBordeaux for raising #334 ("'bash' is not recognized as the name of a cmdlet"). That report is what made this whole Windows-support cluster a priority — without your bug report the unsupported stance would still be in the README. The friction you hit motivated #432 (numpy/python cap), #433 (CLI startup resilience), #436 (python discovery helpers), #437 (desktop launcher fix), and this PR.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Closes#309.
Three root-cause fixes in `frontend/src-tauri/src/lib.rs`:
1. **Background stderr drainer.** `jarvis serve` was spawned with `stderr(Stdio::piped())` but its 4 KB Windows pipe buffer would fill from the startup log volume before the child could bind its HTTP port — hanging the process mid-startup. Now a tokio task drains stderr from the moment of spawn into a rolling 16 KB tail buffer; the pipe never fills.
2. **`try_wait()` early-exit detection.** The old `wait_for_url` was blind to child crashes — uv-not-on-PATH or extension-import failures would silently waste the full 10-minute timeout. The new `wait_for_jarvis_health` checks the child's exit status each iteration and surfaces the stderr tail with the exit code.
3. **HTTP 503 distinguished from connection-refused.** A 503 means the server bound but the inference engine failed to load (terminal); we now surface the body text immediately rather than polling for 10 minutes.
Adversarially reviewed before commit — caught and fixed a stderr-pipe back-pressure regression on the Ready path before push.
Huge thanks to @xoomarx for the careful #309 repro — without that exact symptom signature ("stuck on Starting api server" on Windows) the pipe-buffer deadlock would have been very hard to identify from the user-visible behavior alone.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reimplements the useful parts of #385 cleanly.
Adds two small cross-platform helpers under `openjarvis.core.utils`:
- `get_python_executable()` — prefers `python3`, falls back to `python` for Windows / minimal distros that only ship the unversioned name.
- `open_browser(url)` — `webbrowser.open` by default; on Windows uses `cmd /c start "" <url>` to avoid console-host edge cases.
Swapped at every hardcoded `python3` / `webbrowser.open` site: `connectors/oauth.py`, `evals/scorers/livecodebench.py`, `scripts/oauth_all.py`, `scripts/install/install.sh` (adds `PY_CMD` detection block), `scripts/quickstart.sh` (adds `MINGW*|MSYS*|CYGWIN*) cmd /c start` case), and the two affected test files. Test files wrap `get_python_executable()` in `shlex.quote()` before interpolating into `shell=True` strings — Windows interpreter paths often contain spaces.
Deliberately different from #385: `openjarvis.core.__init__` does NOT re-export `DEFAULT_CONFIG_DIR` (would have raised ImportError because it's in `openjarvis.core.config`, not the package `__init__`; re-exporting would also force eager import of the heavy config module at every `import openjarvis.core`). `oauth.py` keeps `from openjarvis.core.config import DEFAULT_CONFIG_DIR` alongside the new `from openjarvis.core import open_browser`.
Original API surface and call-site sweep by @sanjayravit in #385 — huge thanks for the careful Windows-compatibility audit. This PR preserves your design while fixing the ImportError edge cases caught during review.
Co-Authored-By: sanjayravit <sanjayravit@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses #404 (unable to launch / fully download) and contributes to #309 (stuck on starting api server) by removing the eager numpy import paths that fail hard when a Windows host has a partially-installed or cp314-incompatible numpy.
What changed:
- `src/openjarvis/connectors/embeddings.py` / `hybrid_search.py`: numpy imports are now lazy (inside the method that needs them) with a `TYPE_CHECKING` guard for annotations. Default-argument evaluation no longer touches numpy at module import.
- `src/openjarvis/cli/__init__.py`: the `deep_research_setup` command import is now guarded behind a `try/except Exception` so an OverflowError or ImportError during its module load doesn't crash the entire CLI.
- New regression test in `tests/cli/test_cli.py`: `test_importing_cli_does_not_import_numpy` spawns a subprocess and asserts `numpy` is not in `sys.modules` after `import openjarvis.cli`. Guards against future eager-numpy regressions on Windows.
Reported by @Tentacle39 in #404 and seen alongside @xoomarx's #309. Thanks to both — the Windows-only crash signature made this hard to diagnose without your repros.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fixes#350 (API server fails to start on Windows because numpy has no cp314 wheels).
Caps `requires-python` to `>=3.10,<3.14` in pyproject.toml so uv resolves a Python that has working numpy wheels on Windows. Extends the `test-windows` CI matrix to run on both Python 3.12 and 3.13 to keep the cap honest.
Reported by @RizaldyMongi in #350. Thanks for the careful repro — the Windows-only fallout was tricky to reproduce on Linux/macOS and the issue gave us the exact symptom signature to triage from.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-fixes 13 inherited ruff errors in agents/hybrid/skillorchestra/* and evals/scorers/swebench_harness.py (I001/F401/W291/W292/E703), strips trailing whitespace in evals/eval_orchestrator.py, wraps a long signature in speech/cartesia_tts.py, and extends per-file-ignores for `agents/hybrid/**` and `agents/research_loop.py` (research code with long prompt strings — same rationale as the existing evals relaxation).
Unblocks lint CI for the rest of the Windows-fix cluster (#432, #433, #436, #437, #438).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A 27B local model (Qwen3.5-27B via vLLM) passed a 7-task coding suite cleanly
(create/edit/bug-fix/implement-to-pass-tests/multi-file, verified by running
code + pytest); an 8B model was unreliable. Document so users pick a capable
model for real coding work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>