* fix(chat): warn before "regenerate" permanently deletes later messages
Both "regenerate" entry points — the per-message footer's ↻ "Regenerate
from here" button, and the vision editor's "Regenerate message" button —
call POST /api/session/{id}/truncate, which does a real, permanent SQL
delete of every message after the point clicked, then generates a new
reply. Clicking either on anything but the very last exchange did this with
zero warning: the button's own label doesn't say it also erases the rest
of the conversation, and neither button visually signals it's destructive
differently from any other message-footer action.
Caught live: a mis-click destroyed a real conversation with no way to
recover it (no backup covered the affected window). This adds a
confirmation prompt — via the existing uiModule.styledConfirm — whenever
the action would delete more than just the message(s) being regenerated,
stating the exact count of messages that would be lost. Regenerating the
most recent exchange (the everyday case, nothing exists after it) is
unaffected and still proceeds with zero friction.
New test: tests/test_regenerate_truncate_warning_js.py — drives the real
regenerateFrom/resendUserMessage functions under Node with a fake message
list DOM, confirming: the last-exchange case never prompts and truncates
directly; an earlier case prompts with the correct count; cancelling
aborts before any network call; confirming proceeds with the correct
keep_count. Also fixed a pre-existing, unrelated gap this surfaced while
verifying the full suite: test_pr6020_browser_review_regressions.py's
Node harness never stubbed `navigator` at all (sessions.js reads
navigator.platform at module load) — a gap on origin/dev itself, not
caused by this branch's diff, using the same Object.defineProperty fix
already applied on #6019/#6148 for the sibling issue there (Node 21+'s own
getter-only navigator global, not relevant to this specific failure but
the same defensive pattern).
Verified: full pytest suite in-container, 5798 passed / 11 pre-existing
unrelated failures (README/docs/agent-loop baseline) / 0 new.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* test(chat): cover plain resend after dev's replace-by-default change
dev made plain resend (no options) replace from the clicked turn by
default, with append-only resend opt-in via { append: true } (see
test_resend_message_nondestructive.py). The footer "Resend message"
button calls resendUserMessage(el) with no options, so it now truncates
everything after the clicked turn and goes through this PR's warning.
Replace the old "plain resend never prompts" test, which assumed the
previous append-by-default behavior, with one asserting that plain
resend on an earlier pair prompts with the right count, and one
asserting that explicit append-only resend never truncates or prompts.
Also resolve ui.js/sessions.js through the same cache-busting query
string chat.js imports them with. ES modules cache by full URL, so the
harness's unversioned import of ui.js was a second module instance and
its uiMod.styledConfirm stub never reached the instance chat.js calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): warn before an edit deletes later messages; offer to fork instead
Sending an edited user message truncates the chat at that message, the
same unrecoverable server-side delete as regenerate and resend, but did
it with no prompt. It now goes through the same confirmation, worded for
the action ("Sending this edit will also permanently delete the N
messages after it"). Cancelling leaves the editor open with the draft.
The confirmation for all three actions (regenerate, resend, edit) also
offers "Fork from here": it forks the chat just before the user turn,
switches to the new chat, and replays the action there (the same message,
or the edited text), leaving the original chat untouched. forkFrom now
shares the same fork helper.
Tests drive the real editUserMessage Send handler under Node and cover
the edit prompt (count, cancel, last-message no-prompt) and the fork
choice for each action (fork keep_count, no truncate, replayed text).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): cut and fork a chat by message id, not loaded-page position
Regenerate and edit sent keep_count, the clicked message's position among
the messages loaded in the page. A long chat loads only its newest messages
until the user scrolls up, so that position is smaller than the real one and
the truncate deleted real messages above the clicked one (regenerating the
latest reply in a ~50-message chat removed about a dozen earlier messages).
Resend already sent before_msg_id; regenerate and edit now do the same.
The fork options this PR adds had the same flaw. Fork now accepts
before_msg_id (fork before that message) and through_msg_id (include it),
resolved against the stored messages; keep_count stays as the fallback for a
message that has no id yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): keep a regenerated turn's message id current; stop on a refused cut
Regenerate keeps the original user bubble on screen and resubmits its text
without drawing a new bubble. The resubmitted message is a new DB row, but
the bubble kept the deleted row's id, so the next regenerate's cut was
refused (404) and the page sent the turn anyway, stacking a second reply.
The stream now reports the saved user message's id (user_message_saved),
which the page attaches to the user bubble, or to the bubble a regenerate
kept. Regenerate and edit stop with an error when the server refuses the
cut instead of sending.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(chat): name the buttons in words instead of symbols
CONTRIBUTING asks for no Unicode emoji in UI or code; the test comments
used the regenerate and pencil glyphs to name buttons.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(chat): share the fork-and-send step across edit, resend and regenerate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): pass the regenerate fork its attachment ids explicitly
Resend already hands its collected file ids to _forkAndSend; regenerate
relied on the earlier _pendingRegenAttachments assignment. Pass _regenIds
too so both fork paths read the same, and check the hand-off in the
regenerate tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(chat): assert fork choices keep the user message's attachments
Run regenerate and resend's "Fork from here" against a user message with
an uploaded file and check that the send fired in the fork carries its
file id, replacing the source-text check. chat.js exposes a read-only
_pendingRegenAttachmentIds() for this, like cookbook-hwfit.js's test hooks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(chat): preserve attachments across edit and regeneration
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
The three shipped compose files forwarded the variable bare, so every
`docker compose` command on a default .env printed "variable is not set".
Default it to the 500 MiB that .env.example documents, like the other
limits, and add a guard test that rejects any bare ${VAR} in compose files.
Fixes#6619
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
The searxng host binding was hardcoded to 127.0.0.1:8080 in all three
compose files, which collides with other stacks that legitimately use
8080 (e.g. a local llama.cpp server). Parameterize it the same way the
odysseus UI port already works:
ports:
- "127.0.0.1:${SEARXNG_HOST_PORT:-8080}:8080"
Default unchanged (8080). The UI->searxng connection uses the container
port over the docker network (SEARXNG_INSTANCE=http://searxng:8080), so
no other change is needed.
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
* tests: skip native bash behavior test on Windows in test_production_external_bridge
On Windows the only containment mechanism (windows_tree) enforces wall-clock
only; ENFORCING mode refuses bash (filesystem/process_tree unavailable), so
exit_code is 1. The test asserts bubblewrap/process_group: POSIX-only by design.
Part of #4287
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* tests: skip native bash exit-status test on Windows in test_preview_execution_evidence
BashTool needs enforcing containment (filesystem/process_tree); Windows
windows_tree gives wall-clock only, so the command is not executed and
exit_code is 1 instead of 7. POSIX-only by design.
Part of #4287
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
Root cause: _is_sensitive_path splits on os.sep, so POSIX literals missed
on Windows; /tmp is not a root on Windows (only $TMPDIR); inline-JSON test
compared against "/tmp/wf.txt" while Windows abspath is C:\tmp\wf.txt.
Change: native-separator helper, TMPDIR monkeypatch, abspath alternative.
Before: 9 failed; after: 0 failed (74 passed, 1 symlink skip).
Part of #4287
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
On native Windows the default text encoding is the ANSI code page
(cp1252 on en/pt systems), so the module-level read_text() calls in
eight static tests fail at collection with UnicodeDecodeError on bytes
0x81/0x90 that appear in static/js/chat.js, chatRenderer.js, app.js,
index.html and src/visual_report.py. Pass encoding="utf-8" like the
other 700+ tests in the suite already do.
Found by running the suite natively on Windows 11 (Python 3.11).
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Alexandre Teixeira <general@alexandreteixeira.dev>
- Add a diagnosis for "NVIDIA driver on your system is too old" and
"CUDA driver version is insufficient for CUDA runtime version", the
errors PyPI's CUDA 13 torch/vLLM builds hit on drivers older than 580.
- Remove the nested _diagnose_serve_output copy in cookbook_routes.py.
It shadowed the helper and had drifted, so /api/cookbook/tasks/status
never used the helper's newer diagnoses.
- Capture the tmux pane with -J in the status poll so lines wrapped at
the pane width do not split error text mid-word.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Gallery search built a single `%{search}%` LIKE, so multi-word queries only matched the exact adjacent phrase ("red car" missed "a car that is red") and literal % / _ acted as SQL wildcards. Split on whitespace and require each token to match (prompt OR tags OR ai_tags), escaping LIKE wildcards per token; same escaping for the tag filter. Promote _escape_like to public escape_like_pattern() shared helper.
Part of #2123.
Tests: tests/test_gallery_search_multiword.py (5 tests); 4 fail pre-fix, 5 pass post-fix. Neighbors (session_search, gallery null-user, shims): 25 passed.
update_skill only checked whether the target directory existed. A skill
with that name in another category lives elsewhere on disk, so the
rename went through and left two skills with one name. Update and
delete look skills up by name, so the next delete removed whichever
file os.walk reached first.
Check the owner's skills in every category before renaming, and answer
409 from PUT /api/skills/{id} instead of the misleading 404.
Replace urllib-based PyPI metadata retrieval with curl --retry 8 to
handle transient TLS/SSL EOF errors that abort the Docker image build.
The sdist download also gets retry logic. No functional change to the
patching or wheel-building steps.
Co-authored-by: isharak7m <192635824+isharak7m@users.noreply.github.com>
budget_context_for_model() returns 0 when the endpoint does not prove its
context window, and compute_input_token_budget() answers 0 with the
conservative default budget (6000). The agent loop's small-local-server
clamp (`candidate_context <= 8192`) also matched 0 and cut the request to
max(1200, 0) tokens, so on any endpoint with an unreported window an open
document or a few thousand tokens of earlier context were trimmed away
before the model saw them ("5519 tokens > 1200 budget (ctx=1800)").
Apply the clamp only to a known window.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
When the chat model cannot see images, the separate vision call's description
is appended to the user's message. Nothing marked it as machine-derived, so the
chat model read it as the user's own prose and answered the description instead
of the question.
The label goes below the `[Image: <name>]` marker line because the client
strips the block by matching that exact prefix; renaming the marker would leave
the raw description rendered in the user's bubble.
Fixes#5573
Co-authored-by: Claude <noreply@anthropic.com>
`_gather_inputs()` fails soft: a source that raises leaves its inventory as
`[]`. Every probe reads an empty inventory as "not configured" and answers
`disabled`, so a broken integrations file, email store, or endpoint database is
indistinguishable from a subsystem the operator deliberately turned off. With
the remaining subsystems disabled, `GET /api/diagnostics/services` then reports
`overall: "ok"` while three of its inputs failed to load — the exceptions appear
only in DEBUG logs.
Record each failure alongside the empty value (`failed`: source -> controlled
category) and report the affected subsystem through `_source_unavailable()`
rather than probing it with an inventory that is empty for the wrong reason.
- `degraded`, not `disabled`: the configured state is unknown, not known-off.
Not `down` either — nothing reached the subsystem and failed; the inventory
could not be read. Severity is non-zero either way, so a report containing a
failed source can no longer roll up to `ok`.
- A source that loads and is genuinely empty still reports `disabled`.
- One broken source degrades one entry; the others are still probed.
- `meta` carries the source name and the controlled `_classify_error()`
category only, never exception text, credentials, or database detail.
- `inputs.get("failed")` so an inputs payload without the key still probes
normally.
Verified with the reporter's harness: settings loading with
`search_provider: "disabled"` while the other three sources raise now yields
`overall: "degraded"` with `searxng` still `disabled`; before, all five read
`disabled` and `overall` was `ok`.
In routes/cookbook_routes.py, the exception handler for GET /api/cookbook/hf-gguf-files referenced repo instead of the parameter repo_id, causing an unhandled NameError and 500 status when upstream requests to Hugging Face failed.
Fixes#6341.
Six regexes over tool output backtracked polynomially on long whitespace
runs or repeated openers: the browser snapshot generic-line filter and
post-interaction split, the task-row and skill-bullet parsers, the email
"no results" check, and the Google /sorry/ interstitial check.
Rewrite each without overlapping quantifiers, keeping the same matches for
the text these tools emit. Move the /sorry/ check into
is_google_sorry_url() so it can be tested.
Preserve complete scientific notation and signed floating-point literals across
arbitrary streaming chunk boundaries in _DegenerateStreamGuard. Buffer pending
tokens until delimiter characters arrive to avoid splitting numbers into
incomplete or arbitrary alphanumeric tokens, while maintaining the 2048-number
runaway backstop and detection of repeated phrases and word loops.