Skip to content

[pull] dev from menloresearch:dev - #129

Open
pull[bot] wants to merge 2359 commits into
phpmooc:devfrom
janhq:dev
Open

[pull] dev from menloresearch:dev#129
pull[bot] wants to merge 2359 commits into
phpmooc:devfrom
janhq:dev

Conversation

@pull

@pull pull Bot commented Sep 17, 2025

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

@pull pull Bot locked and limited conversation to collaborators Sep 17, 2025
@pull pull Bot added ⤵️ pull merge-conflict Resolve conflicts manually labels Sep 17, 2025
@jan-service-account
jan-service-account deleted the branch phpmooc:dev December 30, 2025 05:25
qnixsynapse and others added 25 commits July 13, 2026 11:33
…chunk sizes fit its context

getDefaultEmbeddingModelId/setDefaultEmbeddingModelId read/wrote raw
webview localStorage, but the Settings UI's default-embedding-model
store persists through the Rust settings backend on desktop. The
extension's embed() never saw the user's choice and silently fell back
to sentence-transformer-mini (256 ctx) every time.

Also add a real safety net: char-based chunking can't reliably predict
token count across tokenizers/content, so vector-db-extension now
verifies each chunk against the embedding model's actual /tokenize
output and recursively halves any chunk that still exceeds context,
instead of relying on a fixed chars-per-token estimate.
Three client-controlled invalidators were reshaping the prompt prefix
every turn, forcing llama-server to reprocess from token 0 instead of
reusing its cached prefix (reuse is pure longest-common-prefix matching):

- cache_prompt was never sent, relying on the server default; assert it
  explicitly so a preset/CLI override can't silently disable prefix reuse.
- Chat requests carried no id_slot, so with parallel>1 consecutive turns
  could land on an empty slot. Pin chat to slot 0; title generation keeps
  using the reserved background slot and can't evict it.
- Smart tool routing selected tools from the latest user message, changing
  the tool set (which serializes ahead of history) each turn. Freeze the
  routed set for the thread's lifetime, re-routing only when the connected
  servers or disabled-tool set changes.
Redesign the reasoning/CoT trace to reduce the vertical space it consumed
while streaming and to read as a clean timeline:

- While streaming, show only the last completed reasoning paragraph as a
  single bounded block that swaps (with a fade/slide transition) as each
  paragraph finishes, instead of the whole growing trace. The in-progress
  paragraph is not shown; the header conveys activity.
- Once done, the full trace renders as a single dotted-rail timeline, one
  dot per paragraph (splitReasoningParagraphs), fixing the doubled rail
  that came from ChainOfThoughtContent's border plus the step connectors.
- The CoT container is a bordered card only while expanded and collapses
  to the bare 'Thought for N seconds' summary.
On reload (notably mid-stream, when the llamacpp router keeps the
extension layer busy), DataProvider's one-shot fetchThreads could run
before the Conversational extension registered. fetchThreads returned []
for that not-ready state, so setThreads([]) wiped the sidebar list and it
stayed empty until the next clean reload (also surfacing as a transient
'Error in route match: __root__/').

fetchThreads now throws when the Conversational extension is unavailable,
distinguishing 'not ready' from 'no threads'. DataProvider retries with
backoff (up to 20 attempts) and only writes on success, so a failed
attempt never overwrites a populated list.
Map Jan's reasoning settings to each cloud SDK's native reasoning option
(no invented token budgets) and surface the control in the composer:

- New buildReasoningProviderOptions() → per-request providerOptions:
  Google thinkingConfig (dynamic -1 / off 0 + includeThoughts), Anthropic
  adaptive/disabled thinking, OpenAI reasoningEffort + reasoningSummary.
  Threaded into the streamText call in custom-chat-transport.
- ChatInput reasoning popover ungated to gemini/google and anthropic
  (Auto/On/Off; token-budget submenu stays local-only), plus a discrete
  effort picker (Default/Low/Medium/High/XHigh) for openai.
- model-factory: OpenAI uses the Responses API only when an effort level
  is set (so reasoning summaries stream) and stays on chat/completions
  otherwise, avoiding breakage of OpenAI-compatible proxies.

Note: Anthropic 'off' relies on the SDK omitting the thinking field
(= model default); the SDK drops {type:'disabled'}, so a model that
reasons by default can't be forced off via providerOptions.
First phase of turning the proxy into a translating gateway. Adds the
plumbing and the shared primitive without changing any behavior:

- ProviderConfig/RegisterProviderRequest gain an optional api_type
  ("openai" default = verbatim passthrough; "openai-responses"/"google"/
  "anthropic" will select a converter in later phases).
- New core/server/converters.rs with SseAccumulator, which reassembles
  complete Server-Sent Events across network chunk boundaries. The existing
  inline proxy parser splits each chunk on lines and drops partial lines;
  every native-response translator needs correct cross-chunk framing, so
  this is the load-bearing primitive to build converters on.

No provider is routed through a converter yet. cargo test --lib (8 new
SSE tests), clippy -D warnings, and cargo check all clean.
Add UpstreamConverter trait + OpenAIResponsesConverter: rewrites
chat/completions -> /v1/responses (messages->input, system->instructions,
reasoning_effort->reasoning, max_tokens->max_output_tokens, tools flatten)
and translates the native response/stream back to chat.completion, emitting
reasoning_content for reasoning summaries.

Wired into proxy.rs behind ProviderConfig.api_type='openai-responses';
absent/openai keeps verbatim passthrough. Streaming uses SseAccumulator for
cross-chunk event reassembly. 10 new converter tests.
Add GoogleGenerateContentConverter behind ProviderConfig.api_type='google'.
Request: messages->contents (assistant->model, tool->functionResponse keyed
by name via a tool_call_id->name map), system->systemInstruction,
reasoning_effort->thinkingConfig{thinkingBudget:-1,includeThoughts}, tools->
functionDeclarations, tool_choice->functionCallingConfig.mode. Response/stream:
candidates parts -> content/reasoning_content (thought parts)/tool_calls;
usageMetadata + finishReason mapped.

Widen UpstreamConverter: upstream_path(body) (model+action in URL, ?alt=sse
for streaming) and auth_header (Google uses x-goog-api-key, not Bearer);
proxy wiring + OpenAI impl updated. Fix chunk_str_with_usage to set already
chat-shaped usage verbatim. 10 new Google tests.
Add AnthropicMessagesConverter behind ProviderConfig.api_type='anthropic'
(OpenAI-in -> native /v1/messages-out; orthogonal to the existing inbound
/messages Anthropic->OpenAI path, which is untouched).

Request: system messages -> top-level system, assistant tool_calls ->
tool_use blocks, tool messages -> tool_result blocks, consecutive same-role
messages merged (Anthropic rejects non-alternating roles), tools ->
input_schema, tool_choice mapped, max_tokens defaulted (Anthropic requires it).
Response/stream: text/thinking/tool_use blocks -> content/reasoning_content/
tool_calls; stop_reason + usage mapped; input_tokens carried from message_start.

Trait: add extra_headers hook (Anthropic anthropic-version) + auth_header
x-api-key; StreamState gains input_tokens. Proxy applies extra_headers in the
key loop. Reasoning intentionally not auto-enabled (Anthropic thinking needs an
explicit budget_tokens we can't infer). 11 new tests.
createOpenAIModel now returns openai.responses(modelId) unconditionally
instead of only when a reasoning effort level was set. Responses is a
superset of Chat Completions and the only surface returning reasoning
summaries; genuine OpenAI always supports it. Custom OpenAI-compatible
providers route through createOpenAICompatibleModel, not here, so this
cannot hit a proxy that only implements /chat/completions.

Drops the now-unused effortLevel lookup + isThinkingBudgetLevelKey import.
The Responses-API-only OpenAI path (56c8724) broke three test mocks
that only stubbed openai.chat, not openai.responses.
…ptions

buildReasoningProviderOptions returned Record<string, Record<string, unknown>>,
which the AI SDK's streamText providerOptions (SharedV3ProviderOptions =
Record<string, JSONObject>) rejects, breaking the tsc build.
feat: OpenAI-compatible translating gateway + Responses API + unified reasoning
Add a regression test that drives CustomChatTransport.sendMessages twice on
one thread and asserts the system prompt, tool set, and prior model messages
are re-serialized byte-for-byte before being sent to the provider, guarding the
KV-cache prefix reuse the transport relies on.

Bump app version to 0.8.4.
…he UI

listThreads() swallowed errors into [], and a not-yet-ready extension
race could overwrite an already-populated list with an empty result.
Let failures propagate so the retry loop kicks in, skip empty-result
overwrites when threads are already loaded, and re-arm the fetch on
late extension registration.
…r and thinking options

Three regressions from the v0.8.4 reasoning/SSE work:

- filterDefaultSseEvents was applied unconditionally in createCustomFetch,
  deleting every named SSE frame. Anthropic (message_start/content_block_delta)
  and the OpenAI Responses API (response.output_text.delta) stream exclusively
  named events, so Anthropic errored and OpenAI rendered empty assistant
  messages; Gemini's data-only frames passed through. The filter is now opt-in
  (filterNamedSseEvents) and enabled only for the llamacpp, MLX, and
  openai-compatible factories it was written for.

- Anthropic thinking is now gated by model generation: adaptive exists on
  4.6+ only and `display` shipped with 4.7, so pre-4.6 models get
  {type: enabled, budgetTokens} mapped from the thinking-budget level
  (the SDK adds the budget on top of max_tokens) and the 4.6 family gets
  adaptive without display.

- An empty api_key fell back to apiKey: '' which puts an empty auth header
  on the wire; Anthropic answers that with a misleading "x-api-key header is
  required" 401. requireRemoteApiKey() now throws an actionable
  "No API key configured" error instead.
swift-jinja 2.4.0 changed Value.object keys from String to ObjectKey,
which fails to compile against swift-transformers 1.3.3's jinjaValue().
The transitive dep was unpinned (from: 2.0.0), so CI drifted onto 2.4.0.
Constrain it directly in the root manifest to the last-good 2.3.x line.
…rontend logs to app.log

Introduce a single logging path across the web-app, core, and extensions.

- core: add shared `logger` (levels -> console.*) + `formatLogArg`, no Tauri dep
- web-app: install console interceptor in main.tsx boot that forwards
  console.* to app.log via tauri-plugin-log (release: warn/error/info;
  debug builds: also debug/log); reuses core's formatLogArg
- llamacpp/mlx: drop direct @tauri-apps/plugin-log usage (fixes double
  logging now that the interceptor forwards console.*) and use shared logger
- assistant/rag/download/core: migrate raw console.* to shared logger
- demote provider-registration logs to debug (DataProvider.tsx +
  remote_provider_commands.rs)
- tests: add logger to @janhq/core mocks, rewire util.test assertions
…d setting

- Render the finished reasoning trace as one continuous dotted rail so a tool
  call between two reasoning paragraphs stays threaded instead of restarting
  the rail. StepRow now hosts arbitrary content; dead ReasoningTimeline removed.
- Remove the "Fold interim text into reasoning" setting and its fold-mode code
  path; interim answer text between reasoning steps always renders as a normal
  message.
…off localStorage

- Add "Auto-generate chat title" interface setting (default on). When off, the
  thread-title summarizer is skipped and the llama-server router reserves no
  extra background parallel slot (RESERVED_BACKGROUND_SLOTS drops to 0).
- Fix llamacpp-extension reading web-app stores from localStorage: proxy config
  and the model-provider blob live in settings.json (backendStorage) on desktop,
  so the reads returned null — ignored proxies and a no-op per-model YAML
  backfill. Route all persistence through a new backend-settings helper
  (settings_get/set/remove); migrate the backend-type preference and every
  migration/backfill done-marker off localStorage too.
- localStorage is now used only for the one-time pre-backend settings migration,
  enforced by a guard test (no-localstorage.test.ts).
Pressing stop escalated to force_stop_model -> POST /models/unload,
freeing the model from VRAM and discarding its KV cache, so resending
forced a full model reload and prompt re-ingestion.

The escalation existed because aborting the HTTP request alone did not
stop generation: tauri-plugin-http 2.5.2's fetch_read_body drained the
response body with no cancellation, keeping the llama-server connection
open (with max_tokens=-1 an infinite thinking loop never ended), so the
router never saw a client disconnect.

Bump tauri-plugin-http to 2.5.9 (JS x3 + Rust crate), which reads the
body chunk-by-chunk and calls fetch_cancel_body on abort, dropping the
connection. The router then observes the disconnect and stops generation
with the model still loaded and KV cache intact, so resending reuses the
prefix cache. This pulls the Tauri stack to 2.10.3 (no new cargo-audit
advisories; resolves RUSTSEC-2026-0009).

Remove the now-dead force_stop_model command and the escalation in
ChatInput (which also violated the guest-js invoke rule).
… reconcile vector-db ACL

- setup: gate extension install on manifest integrity (files referenced by
  extensions.json must exist), so a missing/empty/partially-deleted extensions
  dir self-heals on launch instead of booting with broken extensions
- system: drop stale jan.exe before rename on Windows so a version change
  reliably overwrites the CLI binary (Unix already overwrites via fs::copy)
- vector-db: remove orphan memory_* permission artifacts so the regenerated
  ACL manifest matches build.rs (11 real commands) under Tauri 2.10's stricter
  resolver
fix: Stabilize chat prefix caching, restore cloud providers, unify logging (v0.8.4)
…ers (#8435)

* fix(mcp): guard against duplicate serve() on streamable-http servers

Remote streamable-http MCP servers (type "http") intermittently failed to
connect with `400 Bad Request ... when send initialize request`, or connected
then dropped a few seconds later.

Root cause: `start_mcp_server` had no idempotency guard, so a server could be
serve()'d twice (e.g. a rename fires activate + syncServersAndRestart, and
`restart_active_mcp_servers` restarts active servers without clearing them
first). Each serve() spins up an independent rmcp client that sends its own
`initialize` (both with request id 0); the second is rejected by the server as
a duplicate on an already-initialized session, tearing down the connection.

Fix: make `start_mcp_server` idempotent — skip if the server is already running
or a start is already in flight (tracked via a new `mcp_starting` set on
AppState), clearing the in-flight marker when the attempt completes. The
reconnect path (`schedule_mcp_start_task`) is intentionally left unguarded so
auto-reconnect can still re-serve.

Fixes #8411

* fix(mcp): log duplicate-start skips at debug level

Address review: the idempotency-guard skip messages are routine, not
noteworthy, so log them at debug instead of info.
* fix(providers): send anthropic-version on model-list fetch

Custom Anthropic-compatible providers (e.g. an anthropic proxy) failed to
fetch their model list: the GET /v1/models request omitted the
anthropic-version header that the Anthropic API requires, producing a 400
surfaced as the generic 'check your API key and base URL' error. The
built-in anthropic provider worked only because it ships the header as a
baked-in custom_header, which user-created providers cannot obtain (no
custom-header UI).

Add ensureAnthropicVersion() which sets anthropic-version: 2023-06-01 unless
already present, applied on both model-fetch header paths (tauri
fetchModelsFromProvider and remoteModelCatalog buildHeaders). Harmless to
OpenAI/Gemini and other providers, which ignore the unknown header.

Fixes #8410

* fix(providers): gate anthropic-version header to anthropic providers

Only inject anthropic-version when the provider fronts Anthropic (built-in
anthropic or an Anthropic-compatible proxy, detected by provider name or
host) rather than on every provider's model-list fetch.

* fix(providers): make custom Anthropic providers work end-to-end

Custom Anthropic providers (api_type=anthropic, custom name/host) failed
because header injection and catalog support keyed off a name/host
substring instead of the api_type discriminant the inference dispatch
already uses, and because custom providers ship no custom_header so the
browser-access opt-in was missing.

- Gate anthropic headers on api_type first (name/host as fallback).
- Inject anthropic-dangerous-direct-browser-access alongside
  anthropic-version; the webview is a browser context and Anthropic 401s
  Origin-bearing requests without it. Applied to catalog fetch, model-list
  refresh, and chat inference (createAnthropicModel).
- Honor api_type in supportsRemoteCatalog/fetchTopRemoteModels so
  custom-named Anthropic gateways populate the hub catalog.
qnixsynapse and others added 30 commits August 7, 2026 16:02
…8611)

Add a show_reasoning toggle to the agent TUI that folds the model's
thinking blocks behind a [thinking] indicator while streaming, and lets
the user expand them inline. On the live view a reasoning block shows
[thinking] while open and [thought for Ns] for a short TTL after it
closes (falling back to the plain [working]).

Also fix a stale ToolPermissions import in the max-parallel test that
moved into tauri_plugin_agent_tools and clear several cli-build warnings:
unused Mutex import, unused r1 binding, and the now dead
set_skills_enabled_in_agent_toml under the cli feature.
…on gate (#8614)

The OS jail (bubblewrap/seatbelt/AppContainer) confines every shell command
regardless of the permission gate, so prompting on each write and exec is no
longer what contains the CLI agent. Flip the default: tool calls are
auto-approved, and --safe turns the interactive gate back on.

- Remove --yolo from the bare TUI, `cli agent run`, and `cli agent step`;
  add --safe, inverted once at the arg boundary.
- Rename OrchestrationArgs.yolo to auto_approve. Desktop entry points keep it
  false, so desktop behavior is unchanged.
- Drop the --yolo stderr banner, which would now fire on every default run.
  The TUI notes the active mode on startup instead.
- HardDeny is untouched: .jan/agent/ internals, [tools] deny, and plan mode
  still refuse regardless of auto-approval.

Docs restructured around Default/Safe/Plan. The CI example drops the flag and
warns that --safe with no TTY auto-denies and stalls on the first write.

Also correct two claims this invalidated, plus one that was already wrong:
allow_network's rationale no longer cites the prompt, the generated agent.toml
template says "go through the permission gate" rather than "still prompt", and
permissions.mdx no longer says the sandboxed shell has no network by default
(true on desktop, false on the CLI, where DEFAULT_ALLOW_NETWORK is true).
* fix(agent): count marginal token spend in session budget (#8613)

The session budget summed each completion's absolute total_tokens. Since
every turn replays the full accumulated conversation, total_tokens per turn
equals the whole growing context, so summing it grows quadratically with
context length and cut off legitimate long tasks with
"session token budget exhausted" before they finished — while a true
runaway could burn the same cumulative totals undetected.

Track the marginal increase between requests instead, so the ceiling guards
real new spend without penalizing the unavoidable context replay. Compaction
can shrink the replay below the prior total; saturate at zero so it never
refunds spend.

Fixes #8613

* fix(agent): charge completion tokens after compaction
Tool group and standalone rows previously only checked the last call's
status and lagged until the group closed, so an aborted run (cancel,
error, dispatch_subagent racing its result) could leave a "->" running
marker or a false "check" success on calls that never resolved.

- ToolGroup::is_running now scans all calls, not just the last, since
  results within a batch can land out of dispatch order.
- Add ToolGroup::outcome_tag/row and a GroupRow state (Open/Closed/
  Aborted) so a failed call outranks an unresolved one, and an aborted
  group renders unresolved calls with a distinct "o" marker instead of
  a false checkmark.
- refresh_group_row updates the open group's row as each result lands
  instead of only at group close.
- Track standalone (diff-producing) call rows via pending_rows so
  abort_tool_rows (replacing finalize_tool_group at cancel/error sites)
  can mark any still-open row interrupted rather than leaving a stale
  "->".
- "Preparing X: path" now names the actual tool instead of hardcoding
  "write".
…ound (#8617)

* feat(agent): drop turn caps, make the session token budget the only bound

max_turns and budget.max_steps were near-duplicate ways to bound a run, and
only one of them was ever wired: [budget] was absent from AgentToml, so
max_steps (and budget.max_tokens) were written by /settings and the scaffold
template but never read. Meanwhile SessionBudget had a live consumer with
nothing feeding it -- the CLI never sent max_session_tokens, so every run was
effectively unbounded.

Remove the turn caps entirely and wire the budget instead. The agent takes as
many turns as the task needs; cumulative token spend (or cancellation) is what
stops a runaway loop.

Removed:
- [agent].max_turns, and max_turns/max_steps from the scaffold template
- --max-turns on both `jan` and `jan cli agent run`
- DEFAULT_MAX_TURNS and the max_turns_override plumbing
- the max_turns and budget.max_steps rows in /settings

Wired:
- [budget].max_tokens -> max_session_tokens on the request body ->
  SessionBudget, defaulting to 128000 when unset; an explicit 0 disables the
  ceiling. Counted marginally (see #8615), so it bounds real new spend rather
  than the context replayed each turn.
- loop.rs treats an absent max_turns as unbounded (was 8), which also lifts the
  silent 8-turn cap the desktop chat path was hitting.

Two paths keep a turn cap deliberately, since neither can be cancelled
mid-run: `jan cli agent step` sets max_turns=1 on the body directly, and the
API-server proxy injects PROXY_DEFAULT_MAX_TURNS=8 when a client omits it,
preserving the documented openapi.json default.

Also groups the per-run numeric knobs into SessionLimits rather than adding a
tenth positional arg to App::new beside three existing bare numbers.

Unrelated fixes in code this touches: dotted_keys_write_and_remove_under_their_section
failed to compile under --features test-tauri (it calls the cli-gated
set_agent_key from an ungated test), and the unique_root test helper keyed temp
dirs on tag + per-process counter alone, colliding across the two test binaries.

* docs(agent): drop max_turns/max_steps docs, make token budget the only bound

The head commit removed max_turns/--max-turns and budget.max_steps, wiring
[budget].max_tokens as the only cap on run length (default 128000, 0 disables).
Update project-config, cli, context, and slash-commands pages that still
described the removed surface.
…th (#8619)

Committed transcript entries were rendered once, at the width in effect
when they were appended, so a resize left markdown tables, boxed diff
panels and truncated tool labels sized for the old terminal.

`transcript` becomes a `Vec<Row>`: each row keeps its source
(`RowKind::{Line, Markdown, Tool, Result}`) and renders to lines at the
width of the frame it is drawn into. Diffs re-box and re-highlight,
prose re-wraps, and tool labels -- now stored untruncated -- re-clamp,
growing back when the terminal widens.

A row may render to several lines, so transcript indices (group,
reasoning, subagent, pending-row, `expanded`, `reveal`) address rows and
survive a resize; `draw` repeats each row's index in `row_index` so click
mapping still resolves. `view_width` moves to the top of `draw`, before
the todo HUD reads it to size the layout.

`Row::lines` memoizes per width: the whole transcript is laid out every
frame, so re-parsing markdown and re-highlighting diffs 20x/sec costs
16.9ms/frame at ~500 rows vs 14.1ms with the memo (14.05ms before this
change). Rows are replaced wholesale, never mutated, so it cannot go
stale.
…8620)

A group row marked itself failed if any folded call errored, so a call
that failed and then succeeded on a retry kept reading as failed. Track
the most recently arrived result on the group (results can land out of
dispatch order, so it can't be read back off `calls`) and tag the row
from that. The failing call keeps its own marker in the expanded detail.
…e input (#8625)

The plan and the running subagents describe now, not what was said, so they
no longer live in the transcript. `status_panel` renders them into a slot
between the body and the separator rule:

- Two columns joined by `join_columns` at width/2 (`todo_column` left,
  `agents_column` right), stacking below `PANEL_SPLIT_MIN_WIDTH`.
- `panel_budget` bounds the slot by what is left after the header, rule,
  input and dock plus `MIN_TRANSCRIPT_ROWS`, capped at `PANEL_MAX_ROWS`,
  so a short terminal loses panel rows and never conversation. Each column
  elides its tail with a `+N more` count.
- The plan column expands only the phase in flight and keeps compact stats;
  the agents column shows the newest call per agent, with detail rows as
  budget allows (`AGENT_MAX_ROWS`).
- An `await_subagent` whose run has a live panel prints no waiting row.
- `[thinking]` shimmers (`shimmer_spans`, `SHIMMER_PAUSE`) since folded
  reasoning is otherwise silent on screen; other badges stay flat and the
  label width is fixed.
- `gap` compares activity bands, so folded reasoning next to tool rows
  spends no blank; the borderless input reserves `content + 1` rows; and
  `dock_line` merges the path with the footer hints onto one row.

Both halves also give their rows back once they stop being live state.
A finished plan hides after `TODO_HIDE_AFTER` (3s): `refresh_todo_deadline`
stamps `todos_closed_at` from `draw`, once per frame rather than at each of
the four `todos` write sites, and clears it the moment work reopens. It has
to be wall-clock -- `turns_since_todos_closed` only advances on model
roundtrips, so a run that finishes its plan and stops would pin a fully
checked-off list forever. Hiding is display-only: `/todo` still opens the
list and `age_closed_todos` still owns clearing it.

The fan-out needs no timeout, since each child's `SubagentEnd` already
moves it into the chat as a summary row -- but that was the only thing
closing a panel. A run ending without one (upstream error mid-fan-out, a
`Done` beating the child's event) stranded the block, spinning in the dock
on an idle session until the next `/clear`; `cancel_run` cleared it, but
silently. `close_live_subagents` now runs from `on_done`, `on_error` and
`cancel_run`: a child cannot outlive the run whose stream carries its
events, so any panel still open is closed and summarized as `interrupted`,
sharing `push_subagent_summary` with the clean path so the two cannot drift.
Up/Down now recalls previously submitted lines into the composer when the
buffer is empty; otherwise they still scroll the transcript, and PageUp/
PageDown always scroll so scrollback stays reachable. Recalling steps back
through the session's history, past the newest entry returns to a fresh
message, and a resend of the same line dedupes to a single history entry.
#8621)

Bracketed paste while an ask prompt is open went to the chat composer,
so a pasted custom answer never reached the ask responder and the model
saw ERROR [ask_cancelled]. Route paste into the active custom answer
field; paste during option selection is dropped so it cannot leak into
the next chat message.

Repeated key events (keyboard auto-repeat) were previously rejected by
the ask key handler, so holding Enter did nothing. Ignore only
key-release events now.

Tests drive the real handlers end to end: repeated Enter selection,
paste into the custom editor, and a custom answer received at the
agent invoker boundary.
…agent (#8637)

* fix(agent-tui): show the full command on a single bash tool row

A lone in-flight call is specific enough to name, so the live group row
now shows its own label instead of the counted "Running 1 command"
breakdown; two or more calls still summarize. The bash labels also drop
their fixed 80-char cap on both the running and finished rows -- the row
already clamps to the draw width, so the command fills the terminal and
grows back on a resize.

* feat(agent-tui): track the mouse by default and own drag-to-select

Mouse tracking is on at startup with no hotkey; `mouse = false` in
~/.jan/config.toml turns it off and hands selection back to the terminal.
Ctrl-T, App::mouse_capture and the per-tick enable/disable diffing in
chat_loop are gone.

Alternate scroll (1007) is saved, forced off and restored on exit: it
made the terminal translate the wheel into arrow keys, which the composer
read as message-history recall. Tracking itself is requested by hand
rather than via crossterm's EnableMouseCapture, which also asks for
any-motion reporting (1003) -- an event per idle pointer move. We take
buttons and wheel (1000), motion while held (1002) and SGR coords (1006).

Tracking takes selection away from the terminal, so the TUI implements
it: drag selects with no modifier and copies on release, Alt+drag takes a
rectangle, and a drag held past an edge scrolls a row per frame while the
anchor follows the content. Press and release are split so a drag that
starts on a folded row selects instead of expanding it. The highlight is
a post-pass over the finished frame, so it covers every surface; the
text is lifted from that same buffer one frame later (only draw holds
one) and copied via arboard plus OSC 52, so it survives jan exiting.

Recall is rebuilt from the conversation (rebuild_recall) rather than kept
as an independent log, fixing two ways the two could drift: a resumed
thread had no recall at all, so Up scrolled instead, and a rewind left
entries for messages it had just dropped.

* fix(agent): hand back partial progress when the session budget runs out

Exhausting the session token budget mid-turn returned an Err, which failed
the whole run. A subagent inherits the parent's remaining budget, so it was
the likeliest caller to hit the ceiling -- and its partial work was lost
with the error instead of reaching the parent that asked for it.

Treat exhaustion as a soft stop: append an assistant message describing the
progress so far, publish MessagesUpdated, and return a synthetic completion
with finish_reason "stop". Tool calls still do not run, so nothing further
is spent against the ceiling.

* fix(agent-tui): run compaction off the render loop with a throbber

/compact awaited the summarizing model call inline in chat_loop, so no frame
was drawn for the whole round trip and the TUI looked hung. Auto-compaction
blocked the same way, and papered over it with a note it later scrubbed back
out of the transcript by matching row text.

Both paths now hand a CompactKind to the loop, which spawns the call and
joins it in select! (the login/update pattern), so the 50ms tick keeps
painting: the header badge reads compacting/auto-compacting and the input row
carries the spinner plus elapsed seconds. Row::plain_text goes with the
scrubbing hack.

Off-loop means history can change under the call, so: a run will not start
while a compaction is in flight (its result replaces history wholesale), and
finish_compaction measures against the length the summary was computed from
and re-appends anything queued since, instead of dropping it.

* fix(agent): force todo plan only on active /goal runs

Narrow the eager todo-plan addendum to /goal runs: an unattended loop
that keeps firing turns needs a phased list staged up front, while an
ordinary turn leaves the model free to decide. Replace the first-message
word-count heuristic (should_suggest_eager_todo_plan) with
should_force_goal_todo_plan, gated on a goal_mode body flag and an
empty registry. The upkeep addendum still applies on any turn with a
list. Drop the now-unused user_message test helper.

* feat(agent-tui): forward goal_mode in the run body

Body() sets goal_mode true only while an active /goal runs, so an
achieved or cleared goal reverts to an unchanged body and ordinary turns
never advertise it. Single concern split out on its own for review.

* fix(tui): resolve standalone tool rows to past tense once result lands

* fix(tui): recover pasted images from the clipboard file list

macOS Finder copies publish a file URL rather than raster data, so
`get_image` fails and the paste was only recovered when `get_text`
happened to return an absolute path. Fall back to arboard's
`file_list`, which also covers Explorer and Linux file managers.

The list is whatever the user copied, not necessarily an image, so
gate entries on a known image extension (`image_mime_of`, with
`image_mime` keeping its PNG default for explicit `/image` paths) and
cap `load_image_file` at MAX_IMAGE_BYTES, checked via metadata before
the read. Entries that fail either check are skipped rather than
ending the search.

* fix(agent-tui): drop the duplicate summary above a standalone diff

A resolved standalone tool row already names the tool and file in past
tense ("Edited tui.rs"), so the result's own "Applied N edit(s) to X"
line only repeated it directly above the diff panel.

`RowKind::Result` now takes an optional `content`, and the summary line
is skipped when the call row was rewritten, the call succeeded and a
diff panel follows. Errors keep their text: the row says nothing about
why the call failed.
* fix(agent): preserve compaction checkpoints

* test(agent): preserve file tool context in compaction
* fix(agent-tui): size boxed diff and code panels to the draw width

`boxed_panel` spends `gutter + 4` columns on its frame, but the transcript
call sites reserved a flat `width - 8` while their gutters are 6 and 8 chars
wide. Every panel row came out 2-4 columns too wide, so the closing border
wrapped onto a line of its own: no right edge and a blank row after every
content line.

`diff_lines`/`boxed_panel` now take the total draw width and derive the
content width through a shared `panel_inner`, removing the duplicated `- 4`
from the result row, the expanded group detail, the permission preview and
the markdown code block.

* feat(agent-tui): resume the whole session, not just its text

/resume restored only user/assistant text: reasoning, tool calls, their
results and diff panels were gone, and the model came back unaware of the
work it had done. Two files now back a resume.

display.jsonl (core::cli::journal) is the transcript as rendered, in
emission order, dumped by a single background writer thread from persist --
so every turn boundary and every cancel leaves a resumable journal without
a frame paying for the write, and concurrent renames cannot land a stale
one. replay_display_log feeds it back through the same functions that
rendered it live, so there is no second renderer to drift. It cannot be
folded into messages.jsonl: reasoning is deliberately never resent to the
model, and /compact rewrites the wire history without touching what the
user can see.

messages.jsonl now carries tool_calls/tool_call_id through as extra keys on
the thread.message record, and rebuild_wire_history reconstructs the
conversation for both /resume and `jan cli agent run --resume`, enforcing
tool pairing: an orphaned result is dropped, and a call whose result never
reached disk gets an explicit placeholder rather than an invented outcome.

Also: on_done/cancel_run strip <think> from the history they push, which
the streaming layer always intended (reasoning is display-only) but the TUI
had been resending and persisting; rewind replays the journal so kept turns
keep their tool rows; and apply(ToolCall) flushes buffered prose once at the
top of the arm instead of per branch, which both removes a duplicated call
and keeps the journal in emission order.

* feat(agent-tui): open on a splash instead of two dim notes

The first frame was an empty screen with two dim status lines at the
bottom, which said nothing about what jan is or how to drive it. Open
with the same wordmark `jan --help` prints, followed by the facts a
first-time reader needs -- model, project and branch, how tool calls are
approved -- and the commands to go on with.

The art now lives in one place (`core::cli::brand::LOGO`), shared by the
help header and the TUI, so the two wordmarks cannot drift.

`RowKind::Banner` keeps the splash as data, like every other
width-dependent row: a resize re-lays it out rather than leaving it sized
for the width it was committed at. A terminal too narrow for the wordmark
gets the name as text instead of a clipped one, `hint_rows` wraps between
hints (dropping a description only when a hint does not fit alone), and
the invite shortens rather than wrapping.

No information is lost: the sandbox/`--safe` line and the "type a message
to start" invite moved into the splash, and the invite is dropped when
`--task` already seeded the first message.

* refactor(agent-tui): drop the header name chip, lead with the model

The splash now names the app, so ` jan agent ` in the header said it a
second time -- and spent 11 of the header's columns doing it, which is
what pushed `ctx` and the status badge off a 60-column frame. The model
leads instead, bold so the row keeps an anchor on the left; an unset
model reads `no model` in red rather than opening the row with blanks.

With the model permanently in the header, the splash's own `model` row was
the duplicate, so it goes too. The `project` row stays despite the dock
also carrying it: that copy is dim and easy to miss, and the tools act on
that directory.

The splash also resets `last_kind`, so a startup note (or the first
message) gets a blank line off it instead of butting against its last row.

* feat(agent-tui): give system lines their own gutter and severity

Notes were dim text flush against the left margin, which is exactly how
model prose renders -- "not signed in" read like something the model said.
Every other transcript class already owns a two-column gutter (`› ` user,
`│ ` tool, `┊ ` reasoning), so system lines get one too: `• `, coloured by
severity, with the body keeping its emphasis.

`Level` (Info/Warn/Error/Good) replaces a colour picked by hand at each
site, and `system_marked` lets a category with an established glyph keep
it, so the column never carries two markers -- the goal loop's lines now
sit under `◎`, matching the header badge. Prose stays gutterless, which is
what makes the rest readable as "not the model".

Folded into one path: the turn's error line, the early-finish warning, the
cancel line, the permission denial (which had hand-written its own `•`),
the todo reminder (its `◈` was a second marker), and the `/help`,
`/threads`, `/login` and `/goal` block headers.

* feat(agent-tui): run the system gutter down the whole block

A multi-line system block (`/help`, `/threads`, a goal status) marked only
its first line, so its body floated unattributed under the header. The body
rows now carry `┆`, giving the block one unbroken left edge -- distinct from
the tool rows' `│` and the reasoning rows' `┊`, so three stacked blocks stay
tellable apart.

That needed system lines to become width-aware: as plain `Line`s, a row too
long for the terminal was soft-wrapped by the paragraph, and the
continuation landed in column 0 with no gutter at all -- exactly the
unattributed text this is meant to fix, and it broke the left edge as well.
`RowKind::System` keeps the source and wraps at the draw width like the
other width-dependent rows, so a resize re-lays it out. `cont` says what a
continuation puts in the column: an edge glyph repeats, a marker glyph gives
way to blanks, since a second `•` would read as a second note.

Wrapping goes through `wrap_spans_at_words`, a word-aware sibling of the
code-block wrapper (which stays hard-splitting -- code has no spaces to
break on). A token longer than the width is still split hard.

Also drops a stale doc comment that had drifted onto `clear_selection`.
…8643)

Hiding: the carve-out covered `.jan/agent` and only hard-denied it. `ls`
still listed the directory before refusing to open it, and the thread
store under it left the conversation's own transcripts greppable. Widen
the prefix to all of `.jan` (`JAN_DIR`) and make hiding mean absent:
`ls` omits the entry, `find`/`grep` keep filtering it during traversal,
and path args and bash tokens hard-deny. Anything added under `.jan`
later is covered without a new rule.

Back it with the OS sandbox, so the spellings a token scan cannot see
(`cd .jan`, `$(echo ...)`, a generated script) are unreachable rather
than merely unscanned: `Policy::hide_root` mounts an empty tmpfs over
`<root>/.jan` after the workspace bind on bubblewrap, and denies the
subpath last (later rules win) on Seatbelt. AppContainer grants the
workspace by an ACE and cannot carve a subpath back out without writing
a deny ACE onto the user's directory, so there the scan stands alone.

`HardDeny` now carries a reason: a hidden hit used to be reported as
"see [tools] deny in agent.toml", sending the model to read a file that
is itself hidden.

/init: it submitted its canned onboarding prompt as an ordinary user
turn, so the whole prompt body was rendered as a `>` row and journaled
for replay. `submit_user_hidden` starts the turn without displaying or
journaling it; the command's own note is what the user asked for. Hidden
turns leave staged images alone rather than attaching them to a prompt
the user did not write.

Promote the command as well: it is the first thing a new project wants
and was invisible. It joins the splash hints, and a project with no
instructions gets a one-line invitation. Both the invitation and the
command's own note reuse the rule the system prompt uses
(`context::has_context_file`), so an ancestor's JAN.md counts as
onboarded and stays quiet.
…un on headless hosts (#8646)

* fix(agent-cli): recover from context overflow, and stop trusting a disproven context window

Four defects left a CLI session stuck once it overflowed the model's
context window, and made the numbers on screen describe something other
than what was happening.

Compaction never reached the session. `turn_cycle` compacted into a
local `conversation_messages` but only published `MessagesUpdated` on
its two return paths, so a run that never recovered (attempts
exhausted, or a compaction that failed to shrink) threw the compacted
conversation away with the task. The TUI kept the oversized history and
every later turn re-overflowed by construction. It is now published as
soon as it lands.

The error path never compacted. `should_auto_compact` was consulted only
in the `Done` arm, so an overflow went to `on_error`, which went idle
and dequeued the next queued message straight back into the same
oversized history. `on_error` now recognizes the overflow marker and
queues a compaction plus a retry of the turn that failed, bounded to
`MAX_OVERFLOW_RETRIES` per user turn so a model that overflows at any
size still hands control back. Abandoned recoveries share `halt_turn`
with the ordinary error path, so neither drifts.

The summarizer request was larger than the one that overflowed.
`summarize` replayed the entire conversation plus a prompt, which after
an overflow cannot succeed: it errored and silently substituted
FALLBACK_NOTE, deleting the dropped span instead of condensing it --
including for `/compact`. It now takes only the span being dropped,
rendered to a text transcript (so a trailing tool call whose result is
in the kept tail cannot break tool pairing) and clamped head-and-tail to
a character budget.

Nothing checked the context window against reality. `context_window` is
a config guess with nothing tying it to the model in use, yet the header
gauge and the subagent share divided by it unclamped, reading past 100%
on any model with a larger window -- exactly when `should_auto_compact`
also fired every turn. A prompt the provider accepted now disproves it:
the session stops using it as a denominator, drops it from the gauge,
hides the subagent share, stands down proactive compaction in favour of
the reactive path, and says once how to set it. The share is clamped
regardless. `estimate_token_count` also read arguments off the tool call
instead of `function.arguments`, scoring every tool-heavy history as
empty; it now counts arguments, names, tool-result ids, multimodal text
parts and a per-message envelope.

* feat(agent-cli): add --output-format json for machine-readable run results

Add a --output-format flag to 'jan cli agent run' that replaces the
streamed prose with a single JSON result object on stdout when the run
finishes. Progress and diagnostics stay on stderr, so stdout pipes
cleanly into jq. The envelope is folded from the same event stream the
printer reads (no second source of truth), reports the partial answer on
failure, classifies context_overflow vs upstream_error, and sums usage
across turns and subagents.

* fix(agent-cli): drop the libdbus link so the CLI starts on headless hosts

keyring's `sync-secret-service` backend pulls dbus-secret-service -> dbus ->
libdbus-sys, which links the system libdbus. That put a DT_NEEDED on
libdbus-1.so.3 into every `jan` binary, and the loader resolves it before
`main`, so a headless host without the D-Bus runtime failed at exec:

    jan: error while loading shared libraries: libdbus-1.so.3:
    cannot open shared object file: No such file or directory

provider_secrets already degrades gracefully when the Secret Service is
missing (KEYRING_DOWN latches and it falls back to the encrypted file), but
none of that code ever got to run.

Switch to `async-secret-service`, which reaches D-Bus through zbus and is
pure Rust, so nothing is linked and an absent daemon is just an error the
existing fallback handles.

Pair it with `async-io` rather than `tokio`: zbus's `tokio` block_on drives a
runtime of its own and panics ("Cannot start a runtime from within a
runtime") when called from inside one, and load_provider_keys is sync but
reached from async CLI paths via cli::providers. `async-io`'s block_on
merely blocks the calling thread, as the old sync backend did. Covered by a
regression test that fails with the `tokio` feature and passes with
`async-io`.

Verified: libdbus-1.so.3 is gone from the binary's NEEDED entries, and
libdbus-sys is absent from both the `cli` and `test-tauri` graphs.
* fix(agent): avoid forcing unavailable tools

* fix(agent): expose custom Ask responses

* test(agent): align Ask result contract
The workflow landed on main (#8649), but `pull_request` resolves workflow
files from the merge ref -- head merged into base. A dev-based branch
merged into dev contains no rust-check.yml, so nothing was selected and
the gate never fired. Putting it on dev is what makes it apply.

Ports rust-check.yml (with the CLI config linting --all-targets) and
scripts/stub-tauri-resources.sh, and drops rust-coverage.sh's inline copy
of the stub logic in favour of the shared script.
…8650)

Silence clippy lints that surfaced under --all-targets -D warnings and
the newer (1.9x) toolchain, in code exercised only in test builds.

- utils/math.rs: inline let_and_return, manual_clamp, use RangeInclusive
  contains() in tests, const-assert constant-invariant test
- utils/string.rs: for-loop over char_indices, Option::is_some_and,
  !is_empty trail
- utils/network.rs: strip_prefix for *. wildcard host check; drop
  needless borrows on Command .args()
- utils/system.rs: strip_prefix for the Windows \\?\ device-path prefix
- utils/crypto.rs: keep % == 0 (is_multiple_of would raise MSRV above
  the declared 1.82 floor), via targeted #[allow]
- agent/loop.rs: collapse collapsible_match test arm into a match guard
- agent/global_config.rs: contains_key instead of get().is_none() in test
- agent-tools/tools/gate.rs: #[allow] join_absolute_paths to keep the
  absolute-path-escape semantics of the test (clippy's suggested fix
  would have gutted its intent)
* fix(llamacpp): trim shifted chat context

* test(llamacpp): cover context-shift request trimming
)

* feat(cli): send an anonymous usage ping from the headless jan CLI

Reuses the desktop's HMAC-signed update-check endpoint purely as a usage
counter: once per 24h, fires a signed request with the CLI version,
OS/arch, and a persisted anonymous install id. Shares the existing
update check's JAN_CLI_NO_UPDATE_CHECK opt-out and its silent-failure
behavior.

* fix(cli): send Jan-Agent in the usage ping's User-Agent, not Jan

The CLI ping reused the desktop's exact User-Agent format and shares its
version number, so the two were server-side indistinguishable. Naming
the CLI client "Jan-Agent" lets the analytics backend split them apart.

* fix(cli): satisfy clippy let-unit-value in telemetry opt-out test
* feat(agent): add built-in Jan self-knowledge skill

* fix(agent): refine Jan onboarding skill

* docs(agent): map Jan Agent storage

* docs(agent): use ASCII storage navigation

* docs(agent): document Jan file locations

* fix(agent): correct Jan instruction guide
Introduce a progressive-disclosure memory catalog: curated notes are
advertised by name plus a one-line summary in the system prompt, and the
model loads each full note on demand with memory_read.

- agent-tools/memory: add catalog() and describe() helpers (auto-tested)
- context: inject an '# Available Memories' block via load_memory_catalog
- default_skill: point at memory_read instead of automatic recall
- remove the vector-db backed retrieve/index flow from loop.rs and the
  old core/agent/memory.rs module
…t.rs (#8664)

Remove the per-turn "Today's date is" line prepended in the orchestration
loop; the runtime environment block already emits the date, so it appeared
twice on every project run. Prompts now live only in context.rs: move the
eager-todo and todo-upkeep system-prompt addenda out of loop.rs into
context.rs and reference them from there.
* feat(agent): reword default identity to Jan agent harness

* feat(agent-cli): recolor thinking badge yellow and bold text orange

* perf(agent-cli): lay out only the visible transcript rows

draw cloned every row's lines and let the body Paragraph word-wrap the
whole transcript twice per frame (once for line_count, once to render),
so per-frame cost tracked session history rather than the viewport: at
width 100, 3.2ms/frame at 100 rows, 58ms at 2000, 236ms at 8000 -- past
the 50ms tick, which starved input and made long sessions crawl.

Memoize each row's wrapped height alongside its lines, sum those to get
the scroll total, then materialize and wrap only the segments overlapping
the visible window. Flat at ~0.7-0.9ms regardless of length.

row_index is now one entry per visible body row in wrapped coordinates,
which also fixes click-to-expand drifting whenever a row wrapped.

* fix(agent-cli): drop seconds from the TUI clock
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull merge-conflict Resolve conflicts manually

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants