Commit Graph
816 Commits
Author SHA1 Message Date
176ed3029b fix(channels): unify send() destination/reply contract (Discord #515/#516) (#528)
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 #515
Fixes #516

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.0.3.dev816
2026-06-10 14:10:06 -07:00
590addae4c test(engine): expect EngineConnectionError for vllm 404 (fix main CI) (#530)
#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>
v1.0.3.dev815
2026-06-10 14:09:37 -07:00
bc9fa6b3c8 fix(memory): surface clear error when openjarvis_rust missing instead of silent no-op (#527)
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>
v1.0.3.dev814
2026-06-10 13:25:10 -07:00
18d9897317 fix(version): restore version to 1.0.2 (undo erroneous revert to 0.1.1) (#525)
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>
v1.0.3.dev813
2026-06-10 13:03:05 -07:00
a61d3c09e1 Improve local engine model resolution (#463)
Co-authored-by: Lakshmi narayana U <ln-mini@Lakshmis-Mac-mini.local>
2026-06-10 12:33:01 -07:00
c76c80d6b4 fix: forward tools through OpenRouter engine (#511)
* 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>
2026-06-10 12:30:22 -07:00
d908372eb5 fix(chat): honor model config in managed agent chat (#477) (#514)
* 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>
2026-06-10 12:15:37 -07:00
1b2b0a06e1 fix(learning): exclude padding tokens from SFT loss (#521)
* 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>
2026-06-10 11:48:57 -07:00
github-actions[bot] 9e4504cce4 chore: update clone traffic data [skip ci] 2026-06-10 07:32:27 +00:00
Jon Saad-FalconandGitHub d8d4985eb5 Update README.md 2026-06-09 13:54:06 -07:00
github-actions[bot] 3359629456 chore: update clone traffic data [skip ci] 2026-06-09 07:17:33 +00:00
Robby ManihaniandGitHub 726445433b fix: wire TraceCollector into server chat endpoints (#513) 2026-06-08 18:04:14 -07:00
github-actions[bot] b30bf43557 chore: update clone traffic data [skip ci] 2026-06-08 07:46:18 +00:00
github-actions[bot] a99ee7c473 chore: update clone traffic data [skip ci] 2026-06-07 07:26:47 +00:00
4523715bff fix(ci): gate live HuggingFace Hub download tests behind hub marker (#507)
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>
2026-06-06 16:31:50 -07:00
Jon Saad-FalconandGitHub f7c948fe12 Merge pull request #506 from open-jarvis/update-discord-link
Update Discord invite link
2026-06-06 12:09:19 -07:00
Jon Saad-FalconandClaude Opus 4.8 30a014faf4 Update Discord invite link to discord.gg/CMVBmDQ5Fj
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:07:07 -07:00
github-actions[bot] bb90480430 chore: update clone traffic data [skip ci] 2026-06-06 07:07:04 +00:00
github-actions[bot] 3ab7764e06 chore: update clone traffic data [skip ci] 2026-06-05 07:33:00 +00:00
68b27654a8 docs(showcase): add outcome-first gallery tier above Tutorials (#500)
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>
2026-06-04 17:42:23 -07:00
1827c1578b fix(leaderboard): filter quarantined methodology_version=0 rows (#501)
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>
2026-06-04 17:42:19 -07:00
28ef745384 fix(leaderboard): correct telemetry pipeline and outlier handling (#498)
* 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>
2026-06-04 16:59:24 -07:00
github-actions[bot] 50780a6a1d chore: update clone traffic data [skip ci] 2026-06-04 07:41:09 +00:00
f72abadf68 fix(desktop): AppImage env-strip + attach to existing healthy server (#496)
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>
2026-06-03 19:54:06 -07:00
945dabbce7 fix(channels): use conversation_id (not channel type) as Discord reply destination (#495)
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>
2026-06-03 19:54:03 -07:00
7506bcc0e4 fix(mcp): send Authorization: Bearer; auto-load MCP tools in ask/serve (#494)
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>
2026-06-03 18:27:00 -07:00
739bff417c feat(prompt): per-invocation persona scope (#380) (#493)
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>
2026-06-03 16:59:47 -07:00
de3e86c544 fix(install): use requires-python range; bootstrap shims are Python-free (#476, #484) (#492)
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>
2026-06-03 16:59:29 -07:00
github-actions[bot] 58e822ff91 chore: update clone traffic data [skip ci] 2026-06-03 07:45:26 +00:00
b3cfa398b4 fix(config): honor system_prompt.prefix from config.toml (#482)
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>
2026-06-02 20:14:42 -07:00
53fb42104e feat(rlm): expose real tool calls inside the REPL (#481)
* 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>
2026-06-02 19:31:08 -07:00
f922811cba fix(tools): return labeled page content from web_search (#480)
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>
2026-06-02 19:31:05 -07:00
seilkandGitHub 097795567b feat(prompt): expose SystemPromptBuilder.sections() inspection API (#457) 2026-06-02 19:31:02 -07:00
github-actions[bot] 98b79eca72 chore: update clone traffic data [skip ci] 2026-06-02 07:41:56 +00:00
58f7f717ae docs(test): use canonical github.io install URL in run-as-root case (#473)
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>
2026-06-01 16:51:16 -07:00
4b4fd587b2 fix(frontend): send local API key as Bearer on /v1 + /api requests (#471)
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>
2026-06-01 13:25:14 -07:00
690b95e050 perf(serve): parallelize engine discovery + async version check (#470)
`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>
2026-06-01 13:17:38 -07:00
d1ad3316b6 build(rust): pin MSRV to 1.88 + add rust-toolchain.toml (#469)
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>
2026-06-01 13:17:33 -07:00
2b14315f3c fix(security): detect Rust at import time + SSRF Python fallback (#467)
* 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>
2026-06-01 11:57:44 -07:00
09b19193fe fix(server): load SOUL.md / USER.md context in streaming chat (#449)
* 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>
2026-06-01 11:31:19 -07:00
b5926400e2 fix(engine): retry without temperature on unsupported_value 400 (#466)
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>
2026-06-01 10:26:17 -07:00
3398e0c10b fix(cli): escape exception text in agents-list error handler (#465)
`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>
2026-06-01 10:26:11 -07:00
bd1ab115ec fix(engine): convert apple_fm_shim cumulative snapshots to deltas (#464)
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>
2026-06-01 10:11:53 -07:00
5270149ea5 fix(engine): modernize apple_fm_shim for the public apple-fm-sdk (#377)
* 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>
2026-06-01 09:23:43 -07:00
a13d8a909d fix: copy package includes in Docker builds, incl. GPU (#450)
* 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>
2026-06-01 09:23:22 -07:00
github-actions[bot] 0fcee8236e chore: update clone traffic data [skip ci] 2026-06-01 07:50:11 +00:00
a08282c3e9 fix(server): stream tool_calls instead of agent filler on tool requests (#460)
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>
2026-05-31 16:09:36 -07:00
d13006263e fix(server): bypass agent on /v1/chat/completions when caller passes tools (#454)
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>
2026-05-31 14:12:19 -07:00
github-actions[bot] 6fc644e209 chore: update clone traffic data [skip ci] 2026-05-31 07:21:34 +00:00
Jon Saad-FalconandGitHub 21f1becb4d feat(desktop): single default model + custom OpenAI-compatible endpoints (#453) 2026-05-30 19:34:51 -07:00