The Cookbook Scan/Download (hwfit) table gave the Fit column key:'score', so
clicking the Fit header sorted by score instead of by fit. Give the Fit column
its own 'fit' sort key, add a matching option to the #hwfit-sort select, and
rank fit_level (perfect > good > marginal > too_tight > no_fit) in the
client-side sort. Default puts the best fit first; clicking again reverses it.
Score still sorts by score.
Closes#842
invalidate_search_cache(query) built its cache key as
generate_cache_key(f"{query}|10|None"), but the write path
(searxng_search_results) replaces the caller's default count of 10 with the
admin-configured _get_result_count() (default 5) before building the key.
So a default search for "X" is cached under "X|5|None", while invalidation
looked for "X|10|None" — they never match, and invalidate_search_cache
silently failed to remove anything in the default configuration, violating
its docstring ("invalidate ... just the given query").
Derive the count from _get_result_count() so invalidation matches the
default-search entry the write path actually stores. The same bug (and fix)
applies to both the src/search and services/search copies.
Note: time-filtered variants (e.g. "X|5|day") still aren't reachable from a
query-only signature, since cache keys are opaque SHA-256 hashes with no
stored query; clearing those would need a broader cache-index redesign and is
out of scope here.
Adds tests/test_search_cache_invalidation.py covering the default-count case.
When an agent turn uses native (OpenAI-style) function calling and the model
returns only tool calls with no prose, _append_tool_results built the follow-up
assistant message with content "" (empty string).
Google Gemini's OpenAI-compatible endpoint and Ollama both reject an assistant
message that carries tool_calls alongside an empty-string content with HTTP 400.
Because that message feeds the tool results back to the model, every tool-using
turn on these providers dies at the second round: the tool runs, but the agent
never produces a result.
Use None (JSON null) instead, which is the spec-correct form the OpenAI SDK
itself emits and which OpenAI and Anthropic accept too. Adds tests covering the
native tool-call content shaping.
The DuckDuckGo HTML fallback returns redirect URLs (//duckduckgo.com/l/?uddg=...)
instead of actual page URLs. This caused fetch_webpage_content() to reject them
instantly because _public_http_url() requires an http/https scheme, making search
results unfetchable in deep research mode.
Added _resolve_url() to:
- Convert protocol-relative URLs to absolute (https:)
- Convert path-relative URLs to absolute
- Extract the real URL from DuckDuckGo's /l/?uddg= redirect parameters
Every research endpoint interpolates session_id into filesystem paths
(Path('data/deep_research') / f'{session_id}.json') without checking
for traversal sequences. A crafted ID like '../../data/auth' reaches
arbitrary JSON files — readable via research_detail (which also leaks
file paths in error messages), writable via research_archive, and
deletable via research_delete.
Add _validate_session_id() which rejects anything outside
[a-zA-Z0-9-]{1,128}. Called before filesystem access in all 12
endpoints that accept a session_id path parameter.
The /api/chat/stream_status handler did a membership test against
_active_streams followed by an indexed read of the same key. Between
those two ops, a sibling stream's finally block (or a stop / cleanup
path) can pop the entry, turning the indexed read into a KeyError that
bubbles up as a 500. The race is the exact one _stream_set was already
written to avoid; the comment on the helper at the top of the module
spells out why a single .get() is the right pattern here too.
Collapse the two-step into a single .get() call so the lookup either
returns the live record or None, and report 'detached' / 404 based on
that single read. No behavior change on the happy path; the failure
mode under concurrent stream cleanup is now handled deterministically.
Closes#658.
A fresh `docker compose up -d` shows the searxng container failing its
healthcheck with permission errors at setup (reported in #721 — the
service comes up under names like `odysseus_searxng_1` and never goes
ready, which then blocks the main odysseus container because of the
`depends_on: searxng: condition: service_healthy` gate).
Root cause: the official `searxng/searxng:latest` image runs as the
non-root `searxng` user but its entrypoint still needs to
1. chown /etc/searxng on first boot so the persisted named volume is
owned by the searxng user inside the container,
2. su-exec to drop / re-assert privileges before launching uwsgi, and
3. let our wrapper entrypoint (which seeds settings.yml into the named
volume on first boot) write the file through the volume mount.
Without explicit `cap_add`, the container has neither CHOWN nor
DAC_OVERRIDE nor SETUID/SETGID, so the entrypoint aborts at the first
chown / su-exec / redirection with EACCES. The upstream searxng-docker
compose file solves this with the standard "drop everything, grant only
what's needed" capability pattern.
Fix: mirror the upstream cap_drop ALL / cap_add CHOWN, SETGID, SETUID,
DAC_OVERRIDE on the searxng service. This grants only the four caps the
entrypoint actually needs, matches what searxng-docker ships with, and
leaves ports, volumes, env, healthcheck, and the wrapper entrypoint
unchanged.
Closes#721.