Conversation
Running an agent that is ENABLED on the bench but not yet pushed to the
container produced a raw fleet 502 surfaced as a 500 to the SPA:
"invalid_spec: agent_id 'agent-<slug>' is not an installed delegate on
<container>". Enabling only flags the catalog dirty; the skill reaches the
container on APPLY ("Apply catalog changes", reviewer/admin-gated). So the
operator simply skipped (or has a pending) Apply — but the error read like a
crash.
_launch_audit now detects the fleet-agent's "not an installed delegate" failure
and translates it into an actionable, user-facing message ("This agent is not
loaded on your container yet. Open the Agents page, click 'Apply catalog
changes' to push it, then run it again."), stamped on the failed Run and
re-raised as a clean frappe error instead of the raw AdminUnreachableError. The
Run is still marked terminal failed (never left stuck "running"); only the
surfaced exception changes.
Regression test: an enabled-but-unapplied run_agent_now raises the apply hint
and leaves the Run failed with that message.
chore: correct Jarvis app metadata
chore: align Jarvis install and CI with repository move
… bubble New users now land on the same greeting-and-starter-cards welcome screen as everyone else. The one-time plain-text intro bubble (WelcomeAssistantMessage) and its entire home_intro backend (mark_home_intro_seen / record_home_intro_event endpoints, version fields on Jarvis Settings and Jarvis User Settings, backfill patches) are removed. Claude-Session: https://claude.ai/code/session_01WZJ3nhp1CqHuWzhGibypRa
jarvis/tools/_export/document/sanitizer.py imports nh3, but the package was never declared - it works on Frappe v16 only because frappe itself ships nh3 there. On a Frappe v15 bench the import fails and takes the whole tool registry (and the test suite) down with ModuleNotFoundError at collection time. Found by the first version-15 CI run (PR #927). Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
… before 0.2.5 Mirrors frappe v16's own nh3 pin so the ranges are identical on both majors. Review finding on PR #928. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
fix: declare nh3 as a direct dependency
Frappe 15's only_for() returns early when flags.in_test is set, so all 11 denial tests that route through it never checked roles on a v15 bench and failed their assertRaises. Frappe 16 removed the bypass, which is why they pass there. Production requests never set the flag, so the guards were always enforced on real sites on both majors - this is a test-view gap only, no security hole. Adds jarvis/tests/_role_guard.py with enforced_role_guards(), which probes only_for's source for the bypass (compat doctrine: capability, not version string) and clears flags.in_test around the denial call, then wraps the 11 affected assertions. Batch 1 of the v15 gap burn-down (group 5 of the 163 failures measured by the version-15 CI on PR #927). Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…mplate fix: show the welcome template for first-time users by default
Batch 2 of the v15 gap burn-down (groups 2, 5, 7 of the 163 failures the version-15 CI measured on PR #927), ~44 tests. - frappe.in_test (6 tests): v16-only module attribute; v15 uses frappe.flags.in_test. Add compat.in_test() (keeps v16's check as the first operand, so no v16 behavior change) and route run_import.py and _prepared_reports.py through it. Three tests mock.patch("frappe.in_test") directly, which cannot work on v15 (no attribute to patch); repoint them at the jarvis.compat.in_test seam. - DataImport.set_delimiters_flag (36 tests): v16-only method for the custom-CSV delimiter feature v15 lacks entirely. Guard it in compat.set_delimiters_flag() (skip on v15, where the default comma parse is already correct) and call that from _import_preview.py's two sites. - wiki_graph locking-read spy (2 tests): NOT an app bug. wiki.py is unchanged; the test's rigid spy(dt, name, field) signature could not absorb v15's internal get_value calling convention. Make both spies signature-flexible and pass args through verbatim. Verified: pre-commit green. v16 CI on this PR proves no regression; the cherry-pick onto version-15 is what proves the v15 fix. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
Addresses the three findings on PR #931: - in_test() now branches on whether frappe exposes the module attribute rather than ORing it with frappe.flags.in_test. On Frappe 16 the attribute is canonical, so a stray flags.in_test can no longer widen the result and let the scheduler-paused guard in run_import/_prepared_reports run work inline in production. On 15 the flag stays authoritative. (PLAUSIBLE finding.) - Add TestInTestAcrossMajors + TestSetDelimitersFlagAcrossMajors exercising the real helper bodies (including a simulated-v15 attribute-absent path and the no-widening assertion); existing callers only mocked them out. - Factor the duplicated signature-flexible get_value spy into one _manual_links_lock_spy() helper, used by both locking-read tests. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
test: enforce only_for denial assertions on Frappe 15
fix: three Frappe-15 compat gaps in import/report tools
Batch 3 of the v15 gap burn-down (groups 1 and 6 of the 163 failures the version-15 CI measured on PR #927), ~60 tests. Test-infra only; no app code. - pypika QueryBuilder.run (57 tests): Frappe 16 attaches .run to the base pypika.queries.QueryBuilder, but Frappe 15 attaches it to the dialect subclass (frappe.qb._BuilderClasss) and leaves the base without it, so the 19 patch("pypika.queries.QueryBuilder.run") sites in test_query.py raised AttributeError on 15. App code is unaffected (it calls .run on a real builder instance, which has it on both). Add _patch_qb_run(), which targets whichever class actually carries .run, and route all 19 sites through it. - orjson (3 tests): Frappe 16 serialises HTTP responses with orjson and depends on it; Frappe 15 uses stdlib json and does not ship orjson, so the tests' bare `import orjson` raised ModuleNotFoundError. Add dumps_like_response(), which serialises the way THIS Frappe serialises a response (orjson on 16, stdlib json on 15), and route the three round-trips through it. The dict-ness assertions still run on both majors. Verified: pre-commit green. v16 CI here proves no regression; the cherry-pick onto version-15 is the v15 proof. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…port Addresses the #933 finding: branching on `try: import orjson` (a) silently masked a broken orjson on v16, where production 500s on response.py's own top-level import while these tests stayed green, and (b) would route a v15 run through orjson if it were present transitively, when v15 production uses stdlib json. Discriminate on frappe.utils.orjson_dumps -- the v16-only symbol that IS the production response serializer -- mirroring _patch_qb_run's probe-real-state approach. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
test: fix pypika .run and orjson test-infra gaps on Frappe 15
Batch 4 of the v15 gap burn-down (group 3, ~28 tests). This is a real v15 PRODUCTION bug, not just a test artifact. Root cause: on Frappe 15, RedisWrapper.get_value() on a MISS writes frappe.local.cache[key] = None (the miss write-back) unless called with expires=True, while set_value(key, val, expires_in_sec=N) deliberately SKIPS frappe.local.cache for expiring keys (writes Redis only). So within one request the sequence read(cold miss) -> set(TTL) -> read returns the poisoned None forever, never consulting Redis again. Frappe 16 masks it because its set_value unconditionally overwrites frappe.local.cache. Jarvis was written against 16, so ~15 read sites omit the expires=True that 15 requires; on a real v15 site this silently double-computes schema/OAuth/telemetry caches and crashes where code subscripts a cached dict it expects to be non-None. Fix (Frappe 15's own documented get_value contract, inert on 16): - Add expires=True to the 38 app-code get_value() reads of TTL keys. No generator=/shared=/user= sites exist, so no fallback is disabled. - pump.py's two lease/gateway reads passed use_local_cache=False, a Frappe-16-only kwarg that TypeErrors on 15 (currently swallowed, so the cache was silently dead there). Route them through new compat.cache_get_fresh(), which probes for the kwarg and falls back to expires=True on 15 for the same always-fresh read. - Add TestCacheGetFreshAcrossMajors, incl. the cold-miss -> set -> read shape. Verified: pre-commit green. v16 CI here proves no regression (existing schema/ oauth/telemetry cache tests still pass); the cherry-pick onto version-15 is the v15 proof. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…en fresh read Addresses the three #939 findings: - wiki.py cooldown_key: the survey missed this TTL key because it is written via raw cache.set(nx, ex), not set_value, so my grep skipped it. It poisons on v15 exactly like its _NUDGE_OFF_KEY sibling two lines up, causing the wiki nudge nag-loop the code warns against. Add expires=True. - Memoization regression: expires=True skips get_value's hit write-back, so the three re-read-hot sites (triggers/engine _triggers_map on the every-save wildcard hook, account gate verdict, list_filters schema) lost their per-request local-cache memoization and now hit Redis every dispatch. Add compat.cache_get_memoized() -- read with expires=True (no poison) then store a real hit back into local.cache by hand -- and route those three through it. - cache_get_fresh v15 branch: stop relying on the "TTL keys are never in local.cache" assumption for the correctness-critical pump leases. Evict any local copy first, then read with expires=True, guaranteeing the live Redis read regardless of prior local-cache state. - Add TestCacheGetMemoizedAcrossMajors (memoized hit + no-poison miss). Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…I key) Google discontinued consumer login-with-Google for Gemini on 2026-06-18 (the gemini-cli OAuth client now returns UNSUPPORTED_CLIENT), so the Gemini chat *subscription* cannot work at runtime on any agent version. Remove the option to connect one; Gemini via API key is unaffected. Frontend: drop "Google Gemini" from the direct-connect fallback list, the onboarding upstream picker, and the upstream label maps (api-key Gemini rows and PROVIDER_DEFAULTS kept). Backend: remove the "Google Gemini" entry from the OAuth provider map, its hardcoded client id/secret in hooks.py, the whole oauth/gemini_cli_secret.py bundle-scan (and its bundled client-secret), the google-gemini-cli revoke endpoint, the turn-handler CliBackend mapping, and the Gemini subscription seed models. Drop the @google/gemini-cli npm dep + stale root lockfile. Catalog: regenerate jarvis/_model_catalog.py from the updated admin PROVIDER_SEED so the bundled fallback offers Gemini as api-key only (empty renderer_id / auth_profile_id, supports_subscription False, api_key-tier models intact). Tests updated to the new reality (Gemini is api-key-only, not an OAuth provider); api-key + vision coverage kept. Pairs with jarvis_admin_v2 PROVIDER_SEED change + patch v1_38_remove_gemini_subscription. Claude-Session: https://claude.ai/code/session_01MWxxyRgfRQV8wqsfgvtwGg
- Reset stranded sites: new patch v2_15_reset_removed_gemini_subscription flips any Jarvis Settings still on a Gemini direct-subscription (llm_auth_mode oauth/subscription + llm_provider "Google Gemini") to api_key mode and clears the stale OAuth account fields, so chat dispatch does not mis-route after the "Google Gemini" -> "google-gemini-cli" mapping is gone. (No connected tenants exist today; this is defensive for stale test-site rows.) - Fix user-facing copy that still invited a Gemini sign-in: the direct-connect card blurb and the pool editor "Chat subscription" description now name only ChatGPT. - Refresh stale comments/docstrings that still referenced google-gemini-cli / "two entries" (DirectSubscriptionCard fallback comment, agent_provider_for and turn_handler docstrings). Claude-Session: https://claude.ai/code/session_01MWxxyRgfRQV8wqsfgvtwGg
- test_oauth_api: the pool-signin-no-pool-scope case (switched to xAI Grok when Gemini was removed) asserted the raw scope "grok-cli:access", but the authorize URL encodes the colon -> assert "grok-cli%3Aaccess". - test_pending_oauth_capture: apply ruff-format (was reformatted by the pinned hook; missed in the earlier per-file lint pass). Claude-Session: https://claude.ai/code/session_01MWxxyRgfRQV8wqsfgvtwGg
fix: prevent Frappe 15 cache local-poisoning on TTL keys
…tion-ready design Stabilise the single composed-document template so any LLM-generated PDF (letter, memo, report, ...) looks polished and correctly aligned. Only the composed-content rich path changes; ERP record exports keep the Frappe Print Format. - theme.py: refined type scale + vertical rhythm; p/ul/ol/blockquote/code/hr rules; tool-built title masthead + geometry-aware full cover (fixes the collapsed cover + raw-markdown title + edge-bleed); .num right-aligns the header too; lighter repeating thead; callout as a distinct card; section-divider is a rule with a new .page-break; sharper bar charts. - furniture.py: resolve_brand (default Letter Head logo, else company name+address; fail-safe); tool-built masthead + running brand header; --header-spacing + reserved top margin; footer -> text zones so page numbers coexist with branding; page-size-aware watermark; sized logo. - export_document.py: title/subtitle/meta/cover params; md numeric-column alignment promoted to .num pre-sanitize; brand + masthead wired in. - graphics.py: css_bar title/caption + bounded per-row series color (additive). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the Gemini chat-subscription option (Gemini stays via API key)
Batch 5a of the Frappe 15 support burn-down. Test-only; no app code changes, so Frappe 16 behavior is identical. - test_realtime_handlers: _FakeConf only implemented .get(), but the v15 dispatch path reads real attributes off frappe.conf (_log_query -> conf.allow_tests, RedisWrapper.make_key -> conf.db_name) that v16 doesn't hit here. Delegate unknown attributes to the real conf (never None -- db_name re-keys the whole cache namespace). Fixes 3 ERRORs. - test_list_filters_smoke: frappe.whitelisted is a set on v16 but a list on v15; coerce to set before intersecting. Fixes 1 ERROR. - test_platform_agents_api_hardening: the singles value_cache sub-dict is created lazily by the first get_single_value, so index via setdefault instead of assuming the doctype key exists. Fixes 1 ERROR. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…ed set - test_realtime_handlers: document that the code under test reads conf only via .get() (mapping-backed, never through __getattr__), so the attribute delegation serves framework internals only and the handler's own reads stay isolated to each test's declared values. - test_list_filters_smoke: hoist set(frappe.whitelisted) out of the per-member loop; the collection is stable for a scan, so build it once. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
Two Criticals + the Important/Minor wave from the 11-lane review: - Cover height now accounts for the running-header top-margin reservation (shared HEADER_RESERVE_MM), so a branded cover fills the page instead of spilling onto page 2 (the bug reintroduced for tenants with a logo). - title/subtitle/meta/cover now activate the rich path, so a titled export always gets the masthead (title-only previously rendered no visible title). - Chart title/caption forwarded from _splice_charts to css_bar (dead feature); _decide_cover matches page-break as a class token (multi-class); title/ subtitle/meta length cap; page geometry validated up front so the HTML path rejects a bad page_size/margin like PDF; _series_class requires a real int. - Brand-resolution FAILURE (vs legitimate absence) surfaces a degrade note + detailed log; the company name is kept when only the address errors; a Letter Head logo in the footer field is used as a fallback. - Centered md columns promote to .text-center; h5 differentiated from body; lists share the prose measure cap; geometry str-coerced + margin-clamped. - Coverage: the company/address resolver (was untested), _decide_cover AND corners, _promote_table_alignment branches, cover-height header wiring, title activation, chart forwarding, _text_only, and more. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feat/rich pdf polish
test(v15): batch 5a - fix 5 test-infra assumptions about Frappe 16 internals
Batch 5b of the Frappe 15 support burn-down. One product/security fix
(applies to v16 too); the rest are test-only, so v16 behavior is unchanged
and its CI gate is the proof. Diagnosed against a real frappe@version-15
checkout, not from memory.
PRODUCT (v15 and v16):
- _export/safety.py escape_formula: the formula-injection guard only checked
the raw first char, so a payload with a leading space (e.g. " =HYPERLINK(...)",
as produced by unwrapping HTML to text) smuggled a live formula past it. Now
also checks the whitespace-stripped first char (OR, so genuine tab/CR-leading
cells stay escaped). Verified bypassable on v16 today; v15 just surfaced it
first via its older markdownify pin.
TEST-ONLY (v15 harness/fixture assumptions):
- 5 atomic-rollback tests: bad-Link fixture used assigned_by, which has a
fetch_from sibling on ToDo; Frappe 15's get_invalid_links silently skips the
invalid-link check for such fields. Point the bad Link at allocated_to (no
sibling) so it is rejected on both majors.
- test_account datetime canary: accept both None (v15, no cast) and
datetime(1,1,1) (v16 cast sentinel) for an empty Datetime single.
- test_admin_client, test_prewarm: route TTL-key reads through
compat.cache_get_fresh; on v15 a bare get_value returns a locally-memoized
value before it consults expires, so expires=True alone is not enough.
- test_pending_confirm mint: also patch cache.setex; v15 set_value routes TTL
writes through setex, not set.
- test_seq_watermark_migration: also bust the v15 table-columns redis hash
("table_columns" field "tab<doctype>"), not just the v16 string key.
- test_review_api_rework followup: add the enforced_role_guards() wrapper its
three sibling PLAIN tests already use (v15 only_for no-ops under in_test).
Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
…onale - test_export_seam: add TestFormulaEscape coverage for the widened escape_formula (leading-whitespace-then-trigger is neutralized, including the HTML-unwrap " =HYPERLINK(...) " shape; leading whitespace with no trigger stays untouched). The guard only runs on strings, so numeric cells are unaffected; this locks the intended string behavior against regression. - Dedupe the allocated_to/get_invalid_links rationale: keep the full explanation canonical at test_bulk_tools._BAD_USER and point the two other sites at it instead of repeating it. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
fix(v15): batch 5b - formula-injection hardening + 12 v15 test fixes
Uninstalling one installation could null the recurrence pointer (last_seen_run) of a DIFFERENT installation's surviving finding on Frappe 15, instead of detaching it to the finding's own first_seen_run. That corrupts the other customer's audit history: get_findings' run drill-down INNER JOINs last_seen_run, so the row vanishes from its owner, and Frappe link validation then rejects that owner's next save. Root cause: _detach_last_seen_run read first_seen_run as a get_all field. On Frappe 15 a get_all projection of Jarvis Agent Finding omits that Link field (confirmed via a CI probe: the raw DB row holds the value, but the get_all dict comes back without the key), so row.first_seen_run was None and the pointer was set to None. Frappe 16 returned the field, which is why this only surfaced on v15. Read first_seen_run with a direct db.get_value, which returns the true column value on both majors. Closes the last Frappe 15 test gap (test_cross_install_recurrence_bump_is_ detached_not_deleted). Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
Address review on the Frappe 15 detach fix: instead of plucking names then issuing one db.get_value per row, read name + first_seen_run in a single raw db.sql SELECT. Raw SQL (not get_all) is still required because Frappe 15's get_all omits the first_seen_run Link field from its projection; this keeps that correctness fix while making it one round trip regardless of finding count. Claude-Session: https://claude.ai/code/session_01Ui9kZy3ZCgzkwz2jRb5uci
fix: detach-last-seen-run nulled another install's finding on Frappe 15
…e one A gated ERP write parks a confirmation card server-side and the browser is told about it once, over a best-effort action:pending socket frame. If that frame is dropped there is no replay, so the card never renders: no Confirm button, and because a typed "go ahead" is resolved from the tokens of the cards currently on screen (approval_tokens), the typed path silently falls through to the model too. The only re-reads ran on the turn's terminal and bailed on a single strict-read blip, so a lost card could stay lost for the rest of a multi-step run - the "card/button not showing" and "go ahead does nothing" reports. Make the client pull server truth instead of trusting one push: - Poll the parked-card list on a short interval while a turn is live (started on run:start and on any action:pending), with a couple of trailing reads after it settles, then self-stop when the chat goes idle. - resyncPendingConfirmations now reconciles to the server's live set for the conversation (drops stale/consumed tokens, adds any the push missed) rather than only appending. - A transient ok:false (the strict owner-index read) keeps the on-screen cards and retries next tick instead of wiping the backstop. Confirming stays sequential server-side (one live card per conversation), so the poll never surfaces more than the flow intends; it just stops losing the one card the chain is waiting on. Holds up across a long chain of cards (1000-record runs park one card per batch). Frontend-only; SPA build green. Claude-Session: https://claude.ai/code/session_01MkE6urYua85gxhwqNSmgfN
Restore multilingual voice transcription with Gemini Flash
fix(agents): actionable error when running an un-applied agent
fix(chat): pull parked confirmation cards so a dropped push can't lose one
kavin-114
approved these changes
Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.