1bad85feb4caee1a350ec45a488f2b1ffe69d855
3752
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1bad85feb4 |
Release v0.51.258 — Release HZ (stage-r6) (#3611)
## Release v0.51.258 — Release HZ (stage-r6) Fresh-arrival low-risk pair (both @rodboev, landed in the last sweep window). ### Fixed | Issue | Fix | |-------|-----| | #3597 | The "update available" banner now shows from **any panel** (Settings → System "Check now", etc.), not just the Chat view — it was positioned inside the chat surface so it only rendered there. | | #3592 | Under Simplified Tool Calling, an assistant turn with **thinking but no tool calls** now renders that thinking inline on settlement instead of burying it in an empty collapsed activity group. | ### Review fix absorbed (Codex) #3592's inline-render `continue` skipped the activity-group creation that carried the turn's `data-turn-duration`, but the footer still suppressed the "Done in …" duration for any `assistantThinking` turn → thinking-only turns silently lost their duration display. Fixed: footer duration is now suppressed **only** for turns that actually build an activity group (`toolCallAssistantIdxs.has(mi)`), so thinking-only inline turns keep "Done in …". + regression test. ### Gate - Full pytest suite: **7631 passed, 0 failed** - ESLint: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (duration-drop) → fixed → **SAFE TO SHIP** ### Sweep note #3603 + #3604 (sidebar CLI-session classification, same author/area) were **not** included — they assert contradictory models for a sidecar-less `source='cli'` recovery row; flagged on both PRs for the author to reconcile. Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
6903891139 |
Release v0.51.257 — Release HY (stage-r5) (#3598)
## Release v0.51.257 — Release HY (stage-r5) Two rebased ★★★ fixes. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3546 | @rodboev | **"Refresh Models" on a provider card no longer returns "Error: Not found".** Sent `POST /api/models/refresh` but no route matched (404). Added the route, wired to the existing `invalidate_provider_models_cache(provider_id)`. | | #3548 | @franksong2702 | **Credential self-heal no longer writes to a dead `SessionDB` handle.** A credential-refresh evicted/closed the cached agent, but the retry rebuilt a new agent from kwargs still holding the old closed `SessionDB` → persistence silently targeted a dead handle. Per-request `SessionDB` construction centralized + refreshed on the retry. | ### Dropped from this batch - **#2660** (session-event SSE profile scoping) — the Codex regression gate found **two SILENT dropped-refresh bugs** the scoping introduced: (1) the `maxsize=1` subscriber-queue coalescing overwrites a pending profile-A event with a profile-B event → A-tabs filter B out and never refresh for the A change; (2) renamed-root/`default` alias mismatch (backend `_profiles_match` treats them equal, the client filter uses a literal `!==`). A dropped refresh (stale sidebar) is worse than the extra refreshes the PR removes. Held with `changes-requested` + repros + the fail-safe fix (coalesce to unscoped on a profile mismatch; normalize root aliases). ### Gate - Full pytest suite: **7622 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (#2660 dropped-refresh bugs) → #2660 dropped + held → **SAFE TO SHIP** Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
3ca1188f64 |
Release v0.51.256 — Release HX (stage-r4) (#3596)
## Release v0.51.256 — Release HX (stage-r4) Performance — bound WebUI memory growth & idle CPU on large installs. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3506 | @nesquena-hermes (reported w/ profiling by @djenttleman) | On a large install (~615 sessions / 40k messages / 454 MB state.db) the WebUI process climbed ~100 MB → ~1.5 GB RSS over days and held high idle CPU. Three root causes fixed: (1) `session_lifecycle._sessions` grew unbounded → new `discard_session()` drops the entry at agent-eviction boundaries, only when no in-flight commit / no uncommitted memory work (retry invariant preserved); (2) cache caps now operator-tunable (`HERMES_WEBUI_AGENT_CACHE_MAX` default 50→25, `HERMES_WEBUI_SESSIONS_MAX`); (3) GatewayWatcher computes a cheap fingerprint before the expensive per-session `MAX(messages.timestamp)` projection and only re-projects on change. | ### Rebase + review notes - Rebased onto current master; the code diff was verified **byte-identical to the nesquena-APPROVED head** at rebase time (only CHANGELOG re-resolved). - The Codex regression gate then surfaced **two correctness gaps** the approval didn't catch, both fixed here with regression tests: 1. **Watcher fingerprint missed same-count transcript rewrites.** `/retry`,`/undo`,`/compress` (`SessionDB.replace_messages`) rewrite messages with new timestamps but can leave `message_count` unchanged → stale sidebar `last_activity`. Fixed with a **per-session** grouped message aggregate (`id, count, user_count, MAX(timestamp)`) over the same non-excluded sessions (a global MAX would miss a rewrite of an older, non-newest session); cron/webui stay excluded so idle churn still doesn't re-project. 2. **LRU agent-cache eviction could close a live worker's agent** (`popitem(last=False)`, liveness-blind — pre-existing, but the lower 50→25 cap made it more likely). Eviction now snapshots `ACTIVE_RUNS` session_ids (before the cache lock — no nested lock) and skips live sessions, deferring (temporarily exceeding cap) rather than closing a live agent. ### Gate - Full pytest suite: **7612 passed, 0 failed** (one boot-cascade flake re-run; clean on re-run) - ruff: CLEAN · Codex (regression): 4 rounds → both gaps + a stale test fixed → **SAFE TO SHIP** Co-authored-by: nesquena <nesquena@users.noreply.github.com> |
||
|
|
8066d384ca |
Release v0.51.255 — Release HW (stage-r3) (#3594)
## Release v0.51.255 — Release HW (stage-r3) Backend hardening — single PR. ### Fixed | PR | Author | Fix | |----|--------|-----| | #3561 | @rodboev | Turn journal (crash-recovery backbone) writes **pid-scoped shards** (`{sid}~{pid}.jsonl`) instead of one shared `{sid}.jsonl`, so concurrent processes (e.g. a self-restart overlap) can't interleave-corrupt large JSON lines. `read_turn_journal` merges all shards + the legacy file and sorts by `created_at` — recovery unchanged, backward-compatible. | ### Gate - Full pytest suite: **7593 passed, 0 failed** - ruff: CLEAN · 18 turn-journal tests pass - Codex (regression): **SAFE TO SHIP** — verified legacy+shard merge (no data loss on upgrade), `~` separator can't collide with a session id, the cross-shard `created_at` sort doesn't break recovery (it derives state by timestamp; stream lookup keys by unique `stream_id`), and no reader/writer bypasses `_journal_path`. - *Non-blocking note:* old `{sid}~{oldpid}.jsonl` shards aren't pruned, so the journal dir can grow across restarts — storage hygiene, not a core-flow regression. Worth a follow-up cleanup (e.g. drop shards with no live pid on session delete). Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
11c0d1667f |
Release v0.51.254 — Release HV (stage-r2) (#3593)
## Release v0.51.254 — Release HV (stage-r2) Phase-2 medium wave 1 — 4 PRs (UI/mobile/cancel fixes + an un-held model dedup). ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3528 | @franksong2702 | Render partial tool calls after cancel — interrupted turns keep their `_partial_tool_calls` rows in the transcript + fallback tool-cards. (Codex confirmed it stays render-only, not forwarded to the provider API.) | | #3550 | @lurebat | Android offline recovery soft-reattaches the live stream instead of hard-reloading the page on a transient background/disconnect. | | #3479 | @mvanhorn | iOS Safari no longer snaps the conversation to the top when a handoff/compression card is inserted mid-stream or on `refreshSession()`. | | #3478 | @JayC-L | **Un-held:** named custom providers (`@custom:name:model`) dedup against bare model IDs without regressing Ollama multi-colon tags (`qwen2.5:7b-instruct-q4`). Only `@custom:` IDs strip the two-segment prefix. | ### Hold-sweep note #3478/#3489 was held yesterday for an Ollama multi-colon-tag regression risk (a blanket `lastIndexOf` would lose the model). The author pushed a scoped fix (only `@custom:` IDs use `lastIndexOf`); I verified `_normId` in node against the regression cases — Ollama bare tags are preserved. Un-held + shipped. ### Gate - Full pytest suite: **7588 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** — #3552 partial-tool-calls verified render-only (no `_API_SAFE_MSG_KEYS` leak / no 400-on-strict-provider, the v0.50.251 #1375 trap); #3551 no EventSource double-subscribe; #3541 no regression vs the #3525 scroll-follow shipped in v0.51.253; #3489 no over-dedup. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> Co-authored-by: lurebat <lurebat@users.noreply.github.com> Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com> Co-authored-by: JayC-L <JayC-L@users.noreply.github.com> |
||
|
|
ba70926e51 |
Release v0.51.253 — Release HU (stage-r1) (#3591)
## Release v0.51.253 — Release HU (stage-r1) Phase-1 low-risk batch — 7 PRs (no intervention beyond apply + one inline MUST-FIX). ### Fixed | Issue/PR | Author | Fix | |----------|--------|-----| | #3525 | @TomBanksAU | Streaming DOM-replace "follow" window tightened 1200px→120px — a reader who scrolled up mid-stream no longer gets snapped to the bottom on completion. | | #3556 | @ai-ag2026 | Topbar count distinguishes a partially-loaded transcript ("loaded of total" via server `message_count`); fully-loaded keeps the tool-row-filtered count. | | #3502 follow-up | @rodboev | Sidebar messaging source badges (Telegram/Discord/…) render as chips, not just CLI ones. | | — | @Karlineal | `.pre-header+pre` margin override scoped under `.msg-body` (removes a 10px gap above code blocks). | ### Tests - `test_ctl_script.py` kills orphan fake-python trees on Windows; conftest `_discover_python` checks the Windows venv layout (`Scripts/python.exe`). (#3537, #3577, @rodboev) ### Docs - Explicit WebUI–Agent compatibility policy + Docker pinning guidance. (#3232, @franksong2702) ### Dropped from this batch - **#3538** (self-update stash-pop recovery) — the Codex regression gate found a **BRICK-class data-loss**: the recovery path runs `git reset --merge` then `git stash drop`, permanently discarding the user's local modifications while returning `ok:true` + scheduling a restart. Held with `changes-requested` + a repro and the fix (keep the stash, return `ok:false`, no restart). Concept is good; the destructive `stash drop` must go. ### Gate - Full pytest suite: **7575 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (BRICK data-loss #3538 + tool-row count regression #3556) → #3538 dropped, #3556 fixed inline → **SAFE TO SHIP** Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com> Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com> Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: Karlineal <Karlineal@users.noreply.github.com> Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
f64b06ab4f |
Release v0.51.252 — Release HT (stage-q24) (#3530)
## Release v0.51.252 — Release HT (stage-q24) Two trivially-safe @rodboev changes (independent). ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #2481 | @rodboev | The floating "selected-text reply" button now has `user-select:none`, so its own label can't get caught in a text selection (no bleed-through). CSS one-liner. | ### Docs - README **Compatibility** section: upgrade WebUI + hermes-agent together until the stable agent API (#2491) lands. (@rodboev) ### Dropped from this batch - **#2977 `/use` skill command** was staged here but **dropped** — the Codex regression gate found an async stale-directive race (`cmdUse()` awaits `/api/skills` but `send()` doesn't await the handler → a fast next send can miss it, or a stale directive leaks to a later message) plus an over-eager `finally` clear that silently discards the directive on a local slash-command early-return. Held with `changes-requested` + a detailed rework note (tracked pending promise + clear-on-consume). Concept approved; needs lifecycle hardening. ### Gate - Full pytest suite: **7570 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** — `user-select:none` scoped to the button only, README docs-only, no `/use` code remains Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
15e654d468 |
Release v0.51.251 — Release HS (stage-q23) (#3527)
## Release v0.51.251 — Release HS (stage-q23) UX-verified live (path dropdown opens on `~/`). ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3433 | @puneetdixit200 | **Composer `~/` path autocomplete** (TUI parity). Typing a `~/` token in the composer opens a home-directory path-suggestion dropdown. Reuses the existing slash-command dropdown (positioning + keyboard nav) and the trusted `/api/workspaces/suggest` endpoint; replaces only the matched token on selection (surrounding text preserved). Slash-command autocomplete still takes precedence for `/`-prefixed input. | ### Gate - Full pytest suite: **7568 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · live-verified the dropdown opens on `~/` - Codex (regression): **SAFE TO SHIP** — slash-precedence preserved, `~/../../etc` → no suggestions (path-escape blocked via root-confined endpoint), bounds-clamped token replacement, esc-escaped, URLSearchParams-encoded Co-authored-by: puneetdixit200 <puneetdixit200@users.noreply.github.com> |
||
|
|
703aba3f3e |
Release v0.51.250 — Release HR (stage-q22) (#3526)
## Release v0.51.250 — Release HR (stage-q22) UX-approved (dark + light-fallback screenshots). ### Added | PR | Author | Feature | |----|--------|---------| | #3328 | @heagandev | **Zeus appearance skin** — OLED-near-black dark surfaces that keep the default **gold accent** (a high-contrast "gold on black" look no existing skin offered). Selectable from Settings → Appearance or `/theme skin zeus`. Dark-focused; falls back to the default light palette in light mode. | ### Notes - The PR was 2 days / ~19 releases stale and CONFLICTING; re-applied surgically onto current master (CSS palette + `zeus` registered at all 5 sites: config allowlist, boot.js swatch, index.html boot-map, i18n `cmd_theme` ×12 locales, picker) + THEMES.md doc row. The PR's own `test_zeus_skin.py` (6 tests) passes against the re-applied version. - Fully scoped + additive: Codex verified every new CSS rule is under `:root.dark[data-skin="zeus"]` — no bleed into the default appearance or other skins. ### Gate - Full pytest suite: **7563 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · vision-verified dark (OLED+gold) + light (clean fallback) - Codex (regression): **SAFE TO SHIP** Co-authored-by: heagandev <heagandev@users.noreply.github.com> |
||
|
|
ba27fee721 |
Add long-running live-to-final replies RFC (#3464)
Merging the RFC as the agreed product contract for long-running-session assistant replies. Thank you @franksong2702! 🙏
It's docs-only (no code), well-structured, and gives the project a shared vocabulary for the follow-up implementation slices — in particular the honest terminal-state set (completed / cancelled / interrupted / compression-exhausted / tool-limit-reached / no-response / error, specific-wins-over-generic) and the live → settled → recovery/replay lifecycle. Nathan blessed merging it as the north-star contract.
|
||
|
|
0501564098 |
Release v0.51.249 — Release HQ (stage-q21) (#3523)
## Release v0.51.249 — Release HQ (stage-q21) Small opt-in feature. UX-approved (screenshot of the toggle in Settings → Preferences). ### Added | Issue | Author | Feature | |-------|--------|---------| | #2974 | @rodboev | **"Auto-expand terminal on output"** preference (Settings → Preferences, **off by default**). When enabled, the collapsed embedded terminal panel expands automatically the first time a running command emits output. Fires once per stream (guarded on open && collapsed — not per chunk), and uses `expandComposerTerminal({focus:false})` so it doesn't steal focus from the composer. Backend-persisted boolean mirroring the `simplified_tool_calling` pattern; default-off = no behavior change on upgrade. | ### Gate - Full pytest suite: **7557 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · screenshot vision-verified (toggle renders cleanly under "Compact tool activity", unchecked by default) - Codex (regression): **SAFE TO SHIP** (clean first pass) — no-arg `expandComposerTerminal` callers unchanged, default-off incl. settings-load-failure path, all 5 plumb sites mirror the existing pattern Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
d828be6daa |
Release v0.51.248 — Release HP (stage-q20) (#3522)
## Release v0.51.248 — Release HP (stage-q20) Bug-fix. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #2782 | @rodboev | **A WebUI session whose sidecar was deleted server-side (e.g. `docker compose --force-recreate`) but whose messages remain in `state.db` no longer bricks the chat.** It used to look alive (`GET` 200 from a CLI stub) while every action 404'd (`POST /api/session/draft`, `/api/chat/start`). The GET handler now consults `_index.json`: a deleted **WebUI-origin** session (webui/fork/blank-non-CLI source) returns 404 so the client self-heals (clears saved id, strips the stale `/session/<id>` URL, falls through to the welcome screen). Genuine CLI/imported sessions keep their 200 read-only stub. Client self-heal now also covers mid-session sidecar deletion of the current session. | ### Review fix absorbed (Codex CORE catch) The first cut collapsed `source_tag or raw_source or session_source or ""`, defaulting a **blank-source** row to WebUI — which would wrongly 404 a **legacy CLI/imported** session that carries `is_cli_session:true` with blank source fields. Now classified **per-field**: any `webui`/`fork` → 404; any explicit non-WebUI source → keep the 200 CLI stub; all-blank → 404 only when NOT `is_cli_session` and NOT `read_only`. + 2 regression tests. ### Gate - Full pytest suite: **7555 passed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · 10 stale-session-restore tests (incl. 2 Codex-catch regressions) - Codex (regression): CORE legacy-CLI false-404 → per-field fix → **SAFE TO SHIP** Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
81e748b455 |
Release v0.51.247 — Release HO (stage-q19) (#3521)
## Release v0.51.247 — Release HO (stage-q19) Backend correctness fix. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3505 | @franksong2702 | **Reasoning effort is coerced to a level the active model/provider actually supports** before each request, instead of being sent verbatim and rejected. `openai-codex` `gpt-5` no longer gets `max` (→ `xhigh`); `o1`/`o3`/`o4` clamp to `low`/`medium`/`high`. Coercion only steps *down* (never escalates); `none`/unset preserved. The capability filter is applied across heuristic / models.dev / Copilot / LM Studio paths. | This is the narrow, correct fix for the detection gap that #3431 tried to address by removing the chip-visibility gate (which we shelved). The chip-visibility gate is **untouched** (Codex confirmed) — `get_reasoning_status`/`_applyReasoningChip` still hide the chip for unconfirmed models. ### Review fix absorbed (Codex + self-flagged) The first cut **dropped** a configured effort for *unrecognized* models, because capability detection returns `[]` for both "known-unsupported" and "simply-unknown" (custom providers, aggregator-rewritten ids, new releases) — that's a behavior change vs master (which sent it verbatim) and would silently disable reasoning. Fixed: an **empty** capability set now **preserves** the configured effort (provider stays the final authority; worst case = the same rejected request master already produces, i.e. no regression). Known-bad clamps return *non-empty* filtered sets, so they still degrade correctly. Nathan chose this "preserve-for-unknown" behavior. + regression test. ### Gate - Full pytest suite: **7548 passed, 0 failed** - ruff: CLEAN · 48 reasoning tests pass (incl. preserve-for-unknown + codex-clamp + never-escalate) - Codex (regression): SHIP-ONLY-WITH-FIXES (unknown-model drop) → fixed → **SAFE TO SHIP** - Verified empirically: gpt-5/codex max→xhigh, o3 max/xhigh→high, unknown high→high (preserved), none/unset preserved Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
772a5c17ed |
Release v0.51.246 — Release HN (stage-q18) (#3513)
## Release v0.51.246 — Release HN (stage-q18) Backend bug-fix. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3225 | @rodboev | **WebUI session rename now syncs the new title to the agent's `state.db`**, so the TUI/CLI stop showing the stale name. `/api/session/rename` now calls `_sync_session_title_to_insights(s)` after `s.save()` and before `publish_session_list_changed` — exactly mirroring the sibling `/api/session/title/regenerate` handler. Gated on the `sync_to_insights` setting and exception-contained (a sync failure can't break the rename). | ### Gate - Full pytest suite: **7544 passed, 0 failed** - ruff: CLEAN · 1 new regression test (call present + sync-before-publish ordering) + 82 rename/title-sync tests pass - Codex (regression): **SAFE TO SHIP** — mirrors the regenerate handler (sync after lock release, gated, exception-contained), no deadlock, no stale data Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
c243126ccb |
Release v0.51.245 — Release HM (stage-q17) (#3511)
## Release v0.51.245 — Release HM (stage-q17) Small UX bug-fix. ### Fixed | Issue | Author | Fix | |-------|--------|-----| | #3338 | @rodboev (#3502) | **Messaging sessions (Telegram, Discord, WeChat, …) now show their platform source badge in the chat-pane topbar**, not just the sidebar. The topbar badge was gated on `is_cli_session` (intentionally `false` for messaging sources), so it vanished once the session opened. Gate removed; a recovered native session stamped `source_label:"WebUI"` stays un-badged. Reuses the existing `.topbar-source-badge` styling — no new chrome. | Picked #3502 over the duplicate **#3499** (same issue/files) — #3502 adds the `WebUI` self-source suppression and a stronger regression test. #3499 closed as superseded with credit. ### Gate - Full pytest suite: **7543 passed, 0 failed** (first run hit the known boot-cascade flake — 376 connection-refused across 29 files; clean on re-run, as expected for a JS-only change) - ESLint: CLEAN · browser-smoke: CLEAN · 8 source-contract tests pin the fix - Codex (regression): **SAFE TO SHIP** — both renderers (panels.js + ui.js) fixed consistently, WebUI-suppression correct, read-only suffix intact, `textContent` XSS-safe Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
c8f8ca3428 |
Release v0.51.244 — Release HL (stage-q16) (#3509)
## Release v0.51.244 — Release HL (stage-q16) UX-approved direction via Telegram (workspace drag-drop polish you requested). All 4 drag-drop flows verified live in-browser. ### Added | PR | Author | Feature | |----|--------|---------| | #3402 / #3424 | @pamnard | **Drop OS files/folders onto a specific workspace folder row or breadcrumb** to upload into that directory (not just the current dir). OS folder drops are traversed (`webkitGetAsEntry`/`readEntries`) preserving nested structure. Uploads via the existing `/api/workspace/upload` (no new backend). | ### Fixed - **Composer drop-zone jank**: dragging a workspace file (or OS file) over the composer footer rendered a translucent overlay that let the textarea/chips/icons bleed through and collide with the hint text. Now a clean, fully-opaque box with a single centered **context-aware** label — *"Drop to insert workspace reference"* (workspace file → `@path` insert) vs *"Drop files to attach"* (OS file → message attach). - **Drag-drop handler coexistence (CORE, caught in review)**: #3424's OS-upload binding assigned `el.ondrop` on folder rows, which **overwrote** the drag-to-move handler from #3422 (also `el.ondrop`) — silently breaking move-to-folder (the ws-path drop fell through to the composer as an `@path` insert). Fixed by binding the OS-upload handlers via `addEventListener` so they compose; each handler gates on its own drag type. ### Drag-drop matrix — all verified LIVE in-browser (real drag→drop, asserted on disk) | Flow | Result | |------|--------| | OS image → composer footer | ✓ attaches | | workspace file → composer footer | ✓ inserts `@path` | | workspace file → workspace folder | ✓ moves on disk (report.md → docs/) | | OS file → workspace folder | ✓ uploads into target folder | ### Scope note #3424's PR branch carried the OLD pre-hardening `_handle_file_move`. Applied **frontend-only** — master's hardened move backend (v0.51.243, TOCTOU/symlink fixes) is untouched (Codex confirmed no `api/routes.py` diff). ### Gate - Full pytest suite: **7542 passed, 9 skipped, 3 xpassed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): CORE handler-clobber → fixed → **SAFE TO SHIP** Co-authored-by: pamnard <pamnard@users.noreply.github.com> |
||
|
|
a7f60689a8 |
Release v0.51.243 — Release HK (stage-q15) (#3507)
## Release v0.51.243 — Release HK (stage-q15) UX-approved direction via Telegram (workspace feature you named). Backend security hardened through 3 Codex rounds. ### Added | PR | Author | Feature | |----|--------|---------| | #3402 / #3422 | @pamnard | **Drag a file or folder in the workspace tree onto another folder row (or breadcrumb segment) to move it** within the workspace. New `POST /api/file/move`. Drop handlers use `stopPropagation` so the composer `@path` drag (#1097) and OS-file upload-drop (#3411) are unchanged. | ### Verified - Live end-to-end: `notes.txt` → `docs/` confirmed on disk; legit moves return the correct `new_path`. - 13 tests (incl. folder-into-self/descendant guard, existing-target collision, and 3 security regressions). ### Security hardening absorbed (3 Codex rounds — all fixed + regression-tested) 1. **TOCTOU symlink race on destination**: a path-based `source.rename(dest)` could be raced by swapping `dest_dir` to an external symlink between validation and rename. Now opens both parent dirs via the workspace-anchored `open_anchored_fd` (openat + `O_NOFOLLOW`, same helper as the upload hardening) and uses `os.rename(leaf, leaf, src_dir_fd=…, dst_dir_fd=…)` with an fd-based collision check; path-based fallback only where `dir_fd` is unsupported. 2. **Symlinked workspace root** returned a confusing 400 after a successful move — returned `new_path` now computed against `ws_root.resolve()`. 3. **Symlinked source entry**: `safe_resolve` follows the final symlink, so moving `link.txt` would move its *target* and dangle the link — now rejected via no-follow `lstat` on the lexical path. ### Gate - Full pytest suite: **7530 passed, 9 skipped, 3 xpassed, 0 failed** - ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES ×3 → all applied + tested → **SAFE TO SHIP** Co-authored-by: pamnard <pamnard@users.noreply.github.com> |
||
|
|
c0d86db5a8 |
Release v0.51.242 — Release HJ (stage-q14) (#3500)
## Release v0.51.242 — Release HJ (stage-q14) UX-approved via Telegram (Nathan — dark/light/mobile screenshots). ### Added | PR | Author | Feature | |----|--------|---------| | #3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. | ### Why it's safe - **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`). - Opt-in; a user has to select it. Default experience unchanged. ### Test-robustness fix (absorbed) The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened). ### Gate - Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed** - ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN - Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss) - Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com> |
||
|
|
e7930ad9a5 |
Release v0.51.241 — Release HI (stage-q13) (#3498)
## Release v0.51.241 — Release HI (stage-q13) UX-flow bug-fix — approved via Telegram. ### Fixed | PR | Author | Fix | |----|--------|-----| | #3471 (#3333) | @starGazerK | **New Chat keeps your unsent draft after peeking at history.** Start a New Chat draft → open a previous conversation → click New Chat: the draft is no longer lost. Empty New-Chat sessions are hidden from the sidebar, so there was no way back to the session holding the draft — New Chat just created another fresh empty session. The entrypoint now remembers the candidate empty draft session (one `localStorage` pointer) and, before creating a fresh session, re-validates it via `/api/session`, routing back only if it is still a safe empty draft (zero messages, no active stream, no pending message, not worktree-backed, matching profile, non-empty server-side `composer_draft`). | ### Why it's safe for existing installs - When there is no remembered draft, it's a **pure no-op fall-through** to the existing `newSession()` path — no behavior change. - Preserves the "zero-message sessions stay hidden from the sidebar" contract. - Conservative multi-guard validation; the pointer is cleared on draft-clear (after send) so an emptied draft never traps you on New Chat. ### Verified live (end-to-end on a test server) - **Positive**: typed a draft → visited a 2-message history session → clicked New Chat → landed back on the draft session with the text restored. - **Negative**: emptied the draft → New Chat created a fresh session (no accidental trap). ### Absorbed on review (Codex CORE MUST-FIX) The PR added `await _saveComposerDraftNow(...)` before the session-switch, which opened a rapid-switch race: clicking session B then quickly C could let B's stale continuation blank C's freshly-loaded state. Added `if (_loadingSessionId !== sid) return;` immediately after the awaited save and before the destructive state-clear (mirrors the existing #1060 stale-guard) + a regression test pinning the guard's position. ### Gate - Full pytest suite: **7510 passed, 9 skipped, 3 xpassed, 0 failed** - ESLint runtime gate: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (rapid-switch race) → fixed → re-reviewed **SAFE TO SHIP** - `tests/test_issue_new_chat_draft_restore.py` (7 assertions incl. the race-guard; live-verified behavior) UX-flow change, no visual/layout delta — approved via Telegram. Co-authored-by: starGazerK <starGazerK@users.noreply.github.com> |
||
|
|
1d7a139c26 |
Release v0.51.240 — Release HH (stage-q12) (#3496)
## Release v0.51.240 — Release HH (stage-q12) Mobile UX bug-fix — UX-approved via Telegram. ### Fixed | PR | Author | Fix | |----|--------|-----| | #3470 | @cnogrin | On mobile/touch, you can now **swipe up to stop streaming auto-scroll**. Previously the view snapped to the bottom on every token with no way to read earlier content while a response streamed — `_recordNonMessageScrollIntent()` only detected upward intent on the wheel path (`e.deltaY`), and touch events have no `deltaY`. Now tracks `_touchStartY` on `touchstart` and treats a `touchmove` that drags the finger down >8px (= scroll up into history, `scrollTop` decreases) as upward intent, setting the same `_messageUserUnpinned` flag the wheel path + scroll listener use. | ### Why this is safe for existing installs - **Only ADDS a touch-unpin path** — wheel + desktop behavior completely untouched, no existing branch modified. - New `touchstart`/`touchend`/`touchcancel` listeners are **passive + capture-only** (they only write `_touchStartY`), so they can't interfere with existing touch handling. - Net effect for users: mobile users *gain* the ability to scroll up during streaming (which was simply broken before). No one's working flow changes. ### Absorbed on review (Codex CORE MUST-FIX) The contributor's gesture sign was inverted (`dy<-8` = finger up = *follow* the stream), which would have unpinned in the wrong direction. Corrected to `dy>8` to match the existing scroll listener's `movedUp` semantics; fixed the comment; strengthened the regression test to pin the gesture direction. ### Gate - Full pytest suite: **7503 passed, 9 skipped, 3 xpassed, 0 failed** - ESLint runtime gate: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (inverted sign) → fixed → re-reviewed **SAFE TO SHIP** - Regression test `tests/test_issue3470_touch_unpin_streaming_scroll.py` (verified to fail against master) UX-approved via Telegram (mobile touch-gesture behavior, no visual/layout delta to screenshot). Co-authored-by: cnogrin <cnogrin@users.noreply.github.com> |
||
|
|
7e8d0ddbea |
Release v0.51.239 — Release HG (stage-q10) (#3494)
## Release v0.51.239 — Release HG (stage-q10) Phase 3 MEDIUM-ring **salvage** from #3407. The source PR bundled a universal reliability fix with debug scaffolding + Android-specific work; this release ships only the clean, universal nugget. ### Fixed | Salvaged from | Author | Fix | |---|---|---| | #3407 | @PatrickNoFilter | `server.py` ignores `SIGPIPE` (`SIG_IGN`) at import time so a client dropping the connection mid-response (tab close mid-stream, network drop, mobile backgrounding, dropped long-poll, `/api/updates/check` timeout) can't silently `Term` the whole process. The broken write now surfaces as a catchable `BrokenPipeError`; the server keeps serving. | ### Why salvage, not merge whole #3407 (585L, 11 commits) bundles three groups: (1) the SIGPIPE fix + a 271-line `diag_shim.py` debug module, (2) an Android-cgroup-specific `os.fork`/`setsid` restart rewrite in `updates.py`, (3) personal deploy scripts (`start-webui.sh`/`watchdog-loop.sh`, which the author notes are "user-side infra, not in the server tree"). Only the SIGPIPE fix is universal, low-risk, and ship-ready — the rest is investigation tooling for a now-solved mystery or platform-specific. The source PR is held with a detailed split explanation. ### Added safety over the source PR The original used a bare `signal.signal(signal.SIGPIPE, ...)` which would `AttributeError` on Windows (no `SIGPIPE`). The salvaged version is `getattr`-guarded so it's a no-op on Windows, preserving the native-Windows support shipped in #1952 (HD). ### Gate results - **Full pytest suite**: 7498 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff**: CLEAN · **browser-smoke**: CLEAN - **Codex (regression)**: SAFE TO SHIP — verified the getattr Windows-guard, that the ignore disposition lands correctly across the `os.execv` self-restart, and that subprocess children use `restore_signals=True` so the ignore doesn't leak to git/shell/editor children. Regression test `tests/test_issue3407_sigpipe_ignore.py` pins SIG_IGN on POSIX, no-raise import, and the getattr guard. Co-authored-by: PatrickNoFilter <PatrickNoFilter@users.noreply.github.com> |
||
|
|
1fe8950022 |
Release v0.51.238 — Release HF (stage-q9) (#3493)
## Release v0.51.238 — Release HF (stage-q9) Phase 3 MEDIUM-ring pick (3-factor: contributor×impact×mitigated-risk) — high-impact perf fix to the most-clicked affordance from a regular contributor (@franksong2702 ★★★), small code surface, CI-green. ### Fixed | PR | Author | Fix | |----|--------|-----| | #2518 follow-up | @franksong2702 | Clicking **New Conversation** on a cold start no longer hangs 3–4s on a catalog rebuild. `newSession()` fills `model_provider` from `window._activeProvider` (then prev-session) when the dropdown carries none, so `POST /api/session/new` takes the fast path on the first click too. | ### Pre-release dual gate caught a wrong-backend routing bug (fixed + regression-tested) The server fast path passes `(model, provider)` through **without validating the pair**, so naively attaching the active provider to *any* bare model could silently route to the wrong backend (e.g. bare `claude-opus-4.8` + active `openrouter`). **Codex** flagged this; **Opus** had judged it acceptable ("respect the selection over silent swap"). I took the stricter, empirically-grounded path and added a **family-mismatch guard** mirroring the server's own bare-prefix→provider map (`gpt`→openai, `claude`→anthropic, `gemini`→google): when the model's known family differs from the fallback provider, `model_provider` stays `null` so the server slow-path's family repair runs. This keeps the perf win for the common matching case while closing the mis-route. Backend behavioral tests confirm fast-path-on-match + slow-path-on-mismatch. (Also re-anchored the source-shape test assertions on the real `reqBody.model_provider=` assignment per Codex's 2nd note.) ### Gate results - **Full pytest suite**: 7495 passed, 9 skipped, 3 xpassed, **0 failed** - **ESLint runtime gate**: CLEAN · **ruff**: CLEAN · **browser-smoke**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES → guard + test-anchor applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: reviewed the original (judged acceptable); the shipped version is strictly safer (adds the family guard) Note: `docs/pr-media/2518/{PR_BODY.md,bench.py}` are the contributor's review aids, included per the tracked `docs/pr-media/` convention (157 files already tracked) — not app code. Closes #2518. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
74dcc4fe6f |
Release v0.51.237 — Release HE (stage-q8): reconcile early-cancel against live worker state (#3492)
## Release v0.51.237 — Release HE (stage-q8) Phase 3 MEDIUM-ring pick (3-factor framework): a real concurrency/state-consistency fix from a regular contributor (@franksong2702, ★★★ 38 merges). The hardest review of the sweep — the dual gate caught **two** silent data-loss bugs across two review rounds. ### Fixed | PR | Author | Fix | |----|--------|-----| | #3475 | @franksong2702 | Cancelling a live turn immediately after send now reliably stops the worker and settles the session to a cancelled state (was: spinner over a blank page). `cancel_stream()` falls back to the live active-run registry (`ACTIVE_RUNS`) + session agent cache when `STREAMS` has already detached, so the worker still receives `interrupt("Cancelled by user")`. `/api/session` reports run-journal active state from the live registry instead of trusting a persisted `active_stream_id`. | ### Pre-release dual gate caught TWO silent data-loss bugs (both fixed + regression-tested) The PR's refactor moved `agent.interrupt()` ahead of the partial-text/reasoning/tool-call snapshot, and that snapshot was no longer under `streams_lock`: 1. **Codex round 1** — the worker's `finally` (which pops `STREAM_PARTIAL_TEXT`/`STREAM_REASONING_TEXT`/`STREAM_LIVE_TOOL_CALLS` under `STREAMS_LOCK`) could clear those buffers the instant `interrupt()` wakes it, so a cancelled turn **silently lost its already-streamed text**. (Opus reviewed the original and said ship — it assumed the snapshot was still lock-protected; the stale comment claimed so but the code wasn't. Verified against the actual code → Codex was right.) 2. **Codex round 2** — my first fix only snapshotted on the `STREAMS`-present path; the detached `ACTIVE_RUNS`-only path (the case this PR adds) still lost text. Fixed by hoisting the snapshot above the `if stream_present` branch so it runs unconditionally under the lock. Both fixes have regression tests **verified to fail against the buggy versions** (`test_cancel_preserves_partial_text_when_interrupt_pops_buffers` + `test_cancel_preserves_partial_text_on_detached_active_run_path`). ### Gate results - **Full pytest suite**: 7481 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES (×2 rounds) → both MUST-FIXes applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: reviewed the original (SAFE); the shipped version is strictly safer (adds the under-lock snapshot Opus deemed unnecessary) - Deadlock concern cleared: no path takes `ACTIVE_RUNS_LOCK` then `STREAMS_LOCK`. Rebased onto current master (streaming.py/routes.py merged clean — no overlap with the #3468 dedup change shipped in HC). Closes #3475. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com> |
||
|
|
6f68fdb6ff |
Release v0.51.236 — Release HD (stage-q7) (#3491)
## Release v0.51.236 — Release HD (stage-q7) First Phase 3 (deep-review) release — picked by the 3-factor framework (contributor × impact × mitigated-risk): high-impact (#1952 native Windows support), backend-only (no screenshots), well-mitigated risk (POSIX path provably unchanged), from a contributor active this session (@rodboev, #3446/#3486 shipped earlier today). ### Added | PR | Author | Fix | |----|--------|-----| | #1952 | @rodboev | Native Windows support for `bootstrap.py` + the embedded terminal: POSIX-only `fcntl`/`termios`/`select` guarded behind `_TERMINAL_SUPPORTED`; terminal entry points raise `NotImplementedError`/no-op on Windows; bootstrap Windows block → warning; auto-install errors clearly on native Windows (WSL unaffected); foreground uses `Popen`+exit on Windows instead of `os.execv`. **POSIX behavior unchanged on every path.** | ### Absorbed on the way in (fix-it-ourselves, reviewed fresh) - `subprocess.CREATE_NEW_PROCESS_GROUP` → `getattr(subprocess, ..., 0)` — the constant is Windows-only, so a win32-simulating test `AttributeError`'d on Linux. Mirrors the `SO_EXCLUSIVEADDRUSE` getattr guard. - Fixed 2 over-reaching tests in `test_windows_native_support.py` — one was launching a **real installer subprocess** via an unstubbed `subprocess.run` (now stubbed; harness 2.8s vs 80s); removed unused imports. - Updated `test_onboarding_static.py` — it asserted the OLD "Native Windows is not supported" hard-block string this PR intentionally replaces; now asserts the new experimental-warning + auto-install guard. - Help-text accuracy: `--foreground` help now describes the Windows Popen path (Opus nit). ### Gate results - **Full pytest suite**: 7478 passed, 9 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN (gate hardened mid-release to auto-detect the cached chromium revision) - **Codex (regression)**: SAFE TO SHIP (simulated `sys.platform=win32`, verified POSIX modules not imported + all terminal guards complete + POSIX foreground still uses execv) - **Opus (correctness)**: SAFE TO SHIP (POSIX path provably unchanged, all fcntl/termios/select guarded, Popen+exit correct; noted inherent-Windows trade-offs that aren't PR bugs) Note: the Windows *runtime* path can't be executed on the Linux CI box; it was reviewed statically by both reviewers + the contributor's 209-line test (win32 simulated via monkeypatch). Linux/POSIX no-regression is fully verified. Closes #1952. Co-authored-by: rodboev <rodboev@users.noreply.github.com> |
||
|
|
aae584ba57 |
Release v0.51.235 — Release HC (stage-q5) (#3490)
## Release v0.51.235 — Release HC (stage-q5) Single fix in the data-sensitive transcript-merge path, with a Codex-found MUST-FIX applied. ### Fix | PR | Author | Fix | |----|--------|-----| | #3468 | @jasonjcwu | `_find_current_user_turn` returns the **last** matching user turn instead of the first, so post-compression `result_messages` (which carries the full history) no longer replays the entire conversation when the user repeats a similar question (137-msg session → 89 duplicate replays). | ### Pre-release review caught a CORE issue (fixed before ship) Codex's regression gate found that a naive last-match could be overridden by a **synthetic `role:"user"` continuation prompt** (the agent loop injects "Continue"/empty-recovery nudges — verified at `conversation_loop.py:1763/4183/4356`) that only *substring*-matches the user text — anchoring the merge **past** the real turn and dropping the assistant/tool output in between. Applied the fix: track **strong** (exact `_looks_like_current_user_turn`) and **weak** (substring) matches separately and return `last_strong → last_weak → fallback`. The real turn (strong) always wins over a later synthetic continuation (weak). Opus reviewed the original and said ship; the applied fix is strictly safer than what it reviewed. ### Gate results - **Full pytest suite**: 7465 passed, 8 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN - **Codex (regression)**: SHIP ONLY WITH FIXES → fix applied → re-reviewed **SAFE TO SHIP** - **Opus (correctness)**: SHIP IT (on the pre-fix code; applied fix is strictly safer) - **Regression test** (`tests/test_issue3468_duplicate_after_compression.py`, 7 cases): pins the last-match behavior, the strong-beats-later-weak invariant, and the end-to-end no-duplicate-replay invariant — each **verified to fail against the pre-fix logic**. Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com> |
||
|
|
ed46c65719 |
Release v0.51.234 — Release HB (stage-q4) (#3488)
## Release v0.51.234 — Release HB (stage-q4) Two medium-risk backend/infra fixes. All gates green. ### Fixes | PR | Author | Fix | |----|--------|-----| | #3289 | @rodboev | Refuse server startup when a live instance already serves the port (Windows/macOS silent port-sharing hazard). Live-listener probe (`GET /health`, 2s timeout) + Windows `SO_EXCLUSIVEADDRUSE` — **preserves fast restart** (POSIX keeps `allow_reuse_address=True`; a dying socket in the kernel backlog times out → startup proceeds). | | #3486 | @dso2ng | Allow remote/SSH terminal profiles to use target-side workspace paths under `terminal.cwd` without a server-local `stat()`. Local profiles unchanged — bypass only fires for remote backends and only for paths contained within `terminal.cwd`. | ### History note on #3289 This PR was **held earlier this sweep** — its original form globally disabled `SO_REUSEADDR`, which a Codex gate flagged as breaking fast restart (TIME_WAIT bricks rebind for ~60s). The contributor reworked it along the suggested lines (live-listener probe instead of the global disable). This release ships the reworked version. Unheld → full pickup → full gate. ### Gate results - **Full pytest suite**: 7458 passed, 8 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN (real server boots fine with the new startup probe) - **Codex (regression)**: SAFE TO SHIP (verified fast-rebind preserved + remote bypass gated on backend+containment, local validation unchanged) - **Opus (correctness + security)**: SAFE TO SHIP (probe false-positive, `_is_within` containment, local-profile bypass all hold up; applied its one minor double-call cleanup note) Closes #3289. Co-authored-by: rodboev <rodboev@users.noreply.github.com> Co-authored-by: dso2ng <dso2ng@users.noreply.github.com> |
||
|
|
7b02883dcf |
Release v0.51.233 — Release HA (stage-q3) (#3487)
## Release v0.51.233 — Release HA (stage-q3) Single high-impact data-integrity fix. ### Fix | PR | Author | Fix | |----|--------|-----| | #3472 | @Mubashirrrr | Guard `/api/session/truncate` `keep_count` against a **silent persisted transcript wipe** (negative value sliced as `messages[:-N]`, deleting the newest N messages and saving to disk) and an HTTP 500 (non-numeric). Now validates before the destructive slice — non-int → 400, negative → 400 — mirroring the existing `/api/session/branch` guard. `keep_count=0` "clear all" semantics preserved. | ### Gate results - **Full pytest suite**: 7450 passed, 7 skipped, 3 xpassed, **0 failed** - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors) - **Codex (regression)**: SAFE TO SHIP (probed the route directly — negative/non-numeric → 400 without mutation; `keep_count=2`/`0` still work) - **Opus (correctness)**: SAFE TO SHIP (guard placed before slice + save; both front-end callers compute from a non-negative DOM index) Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com> |
||
|
|
89b034b925 |
Release v0.51.232 — Release GZ (stage-q2) (#3485)
## Release v0.51.232 — Release GZ (stage-q2) Two low-risk fixes batched into one release. All gates green. ### Fixes | PR | Author | Fix | |----|--------|-----| | #3473 | @Mubashirrrr | Guard malformed/negative numeric query params on the cron endpoints (`/api/crons/output`, `/api/crons/recent`) — no more 500 on `?limit=abc`, no oldest-entry drop on a negative limit | | #3480 | @koshikai | Translate 80 previously-untranslated Japanese (`ja`) locale strings (MCP controls, tool summaries, toasts) — interpolation placeholders preserved, no keys added/removed | ### Gate results - **Full pytest suite**: 7446 passed, 7 skipped, 3 xpassed, **0 failed** - **ESLint runtime gate**: CLEAN - **ruff forward gate**: CLEAN - **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors) - **Codex (regression)**: SAFE TO SHIP (flagged a CHANGELOG endpoint-path typo `/api/cron/` → `/api/crons/`, fixed) - **Opus (correctness)**: SAFE TO SHIP (flagged an inline-comment imprecision about which entries a negative slice drops — fixed in both the comment and CHANGELOG) Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com> Co-authored-by: koshikai <koshikai@users.noreply.github.com> |
||
|
|
0b0a179e4c |
Release v0.51.231 — Release GY (stage-q1) (#3484)
## Release v0.51.231 — Release GY (stage-q1) Four independent, low-risk fixes batched into one release. All gates green. ### Fixes | PR | Author | Fix | |----|--------|-----| | #3368 | @nesquena-hermes | `/model` resolves models from the truncated `extra_models` tail of a large provider catalog (completes the half-done #3368 fix; @garyd9's `mimo-v2.5` case) | | #3457 | @pix0127 | Auto-hide the Plugins settings tab when no plugins are installed; deep-link fallback to Conversation | | #3474 | @Mubashirrrr | Guard malformed/negative `depth` on `/api/sessions/search` (no more 500 on `?depth=deep`, no silent newest-message drop on negative) | | #3433 | @sjh9714 | Resolve symlinked home before the trust comparison so `~/` workspace suggestions work when the process home is a symlink | ### Note on #3457 The original contributor patch reassigned a `const section` (a **runtime `TypeError` brick** on the settings panel) and placed the hidden-tab fallback *after* the panes were already toggled. Refactored to `let` + hoist the fallback above all consumers of `section`. ESLint runtime gate now clean. ### Gate results - **Full pytest suite**: 7441 passed, 7 skipped, 3 xpassed, **0 failed** - **ESLint runtime gate**: CLEAN (caught + fixed the #3457 const-reassign) - **ruff forward gate**: CLEAN (no new violations on changed lines) - **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors) - **Codex (regression)**: SAFE TO SHIP - **Opus (correctness)**: SAFE TO SHIP Closes #3368. Closes #3457. Closes #3433. Co-authored-by: pix0127 <pix0127@users.noreply.github.com> Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com> Co-authored-by: sjh9714 <sjh9714@users.noreply.github.com> |
||
|
|
2d20d336ea |
Release v0.51.230 — Release GX (stage-p14 — extract <think> to m.reasoning #3455 + LLM Wiki last-writer #1257) (#3466)
* Release v0.51.230 (stage-p14): extract <think> to m.reasoning #3455 + LLM Wiki last-writer (#1257) Salvage of #3455 (@gsurenull): dropped the stale api/config.py bits (MiniMax-M3 + SCHEMA_VERSION 3->4 — both already on master via #3374). Kept the two genuine fixes: (1) _splitThinkFromContent persist-path extraction of inline <think> blocks into m.reasoning (fixes 30-50% session bloat for reasoning-only providers like MiniMax-M3); (2) LLM Wiki status Last-writer 3-tier fallback (was always 'Not available' since #1257). Added 9 Node-driven think-split regression tests (data-loss guards: content-before/after preserved, unclosed blocks intact, lookalike tags not extracted). * fix(#3455): renderer-matching think extraction + wiki symlink/bounded-read guards (Codex review) Codex review of stage-p14 found 3 SILENT bugs, all fixed: (1) DATA-LOSS: _splitThinkFromContent's Pass-2 whole-body scan extracted a CLOSED literal <think>...</think> from visible prose/code (e.g. inside a fenced code block) into m.reasoning, emptying it — more aggressive than the renderer (which only strips LEADING blocks). Removed Pass 2; extraction now matches _streamDisplay semantics (leading-only, loop captures consecutive leading blocks). +fenced-code regression test. (2) PRIVACY: _llm_wiki_last_writer followed symlinked .md pages resolving OUTSIDE the wiki (is_file follows symlinks), leaking external frontmatter. Now requires resolved path under wiki_root. +symlink-containment regression test. (3) CONTRACT/PERF: replaced full read_text() with bounded line-by-line reads (frontmatter block only / capped log-heading scan), never page bodies. * fix(#3455): think-split is leading-single (renderer-matching) + fix 2 stale source-match tests Codex re-review finding #2: looping consecutive leading blocks diverged from the renderer (_streamDisplay/_parseStreamState strip ONE leading block). Now extracts exactly one leading block. Also updated 2 tests that asserted pre-split implementation strings: test_live_stream_tokens_persist (content:assistantText -> content:split.content, invariant preserved) and the consecutive-blocks test. NOTE: Codex finding #1 (client-only split doesn't persist server-side) is a separate architectural decision pending Nathan. * feat(#3455): split inline <think> server-side before s.save() so persisted file is compacted (Codex #1) Codex finding #1: the think-split was client-only, so the SAVED session file still carried inline <think> blocks (bloat) — the fix only compacted the browser copy. Added _split_thinking_from_content (api/streaming.py), a server-side twin of the JS helper with identical leading-only/single-block semantics, applied to the final assistant message before s.save() (extended the existing reasoning-persist block). Merges with on_reasoning-stream reasoning. +8 backend-parity regression tests covering the mid-body-code-block data-loss guard, unclosed-intact, single-leading, none-content. * test: update 3 save-path source-assertion tests for #3455 server-side think-split The backend think-split (api/streaming.py reasoning-persist block) changed the literal code shape + grew the pre-save block, breaking 8 source-assertion tests that anchor on it: - test_sprint42: assert _rm['reasoning']=_reasoning_text -> now _merged_reasoning/_existing_reasoning + _split_thinking_from_content present (intent preserved: reasoning persisted before save). - test_pr1318 (6) + test_pr1341: re-anchored the locator from the changed 'if _reasoning_text and s.messages:' line to the stable 'Persist reasoning trace in the session' comment marker; bumped the 1341 byte-distance limit 15000->16000 (the test self-documents bumping on legit pre-save growth). All behavioral invariants (reasoning persisted + context fields before save) unchanged. --------- Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
7c6fb33549 |
Release v0.51.229 — Release GW (stage-p13 — /model never silently snaps a versioned name to a -tier variant #3368) (#3465)
* Release v0.51.229 (stage-p13): /model never silently snaps versioned name to -tier variant (#3368, @nesquena-hermes) Agent-authored, nesquena-APPROVED. Rebased onto current master. Both _findModelInDropdown (ui.js) and _bestModelMatch (commands.js) reject a prefix-snap when the typed target ends in a version digit and the longer option's extra text is a variant/tier suffix (.pro) rather than a version continuation (.digit). Adds _nearestModelSuggestion + 'did you mean?' toast. 34 tests pass (14 new #3368 + 20 regression: #1188 fuzzy + #3360 collision). * fix(commands): /model did-you-mean toast renders suggestion + single quotes (#3368 review) Live-render review of the approved #3437 caught two toast-assembly bugs in cmdModel: (1) t('model_did_you_mean') was called WITHOUT the suggestion arg — model_did_you_mean is a (m)=>... template that t() invokes, so it rendered 'did you mean "undefined"?'; fixed to t('model_did_you_mean', suggestion). (2) no_model_match already ends with an opening quote, so '"${args}"' doubled it ('No model matching ""deepseek-v4""'); fixed to '${args}"'. +4 source-assertion regression tests. Verified live: toast now reads 'No model matching "deepseek-v4" — did you mean "deepseek/deepseek-v4-pro"?'. * fix(commands): slash-qualified versioned no-snap falls through to suggestion (#3368 Codex CORE) Codex review found a 2nd no-snap layer the version guard missed: for a slash-qualified versioned query (e.g. 'xiaomi/mimo-v2.5') whose only near catalog entry is a rejected tier variant ('xiaomi/mimo-v2.5-pro'), cmdModel's cross-provider /api/session/update fallback would silently persist the invalid model + 'Switched to...'. Now gated on !versionedNoSnap (_looksLikeVersionedModel(bare) && a near suggestion exists) so it falls through to the 'did you mean?' toast; genuinely off-catalog providers (no near variant) still direct-update. Verified live: '/model deepseek/deepseek-v4' no longer switches, shows suggestion toast. +1 regression test. --------- Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
9374e66855 |
Release v0.51.228 — Release GV (stage-p12 — workspace tree-drop #3411 + large-markdown preview #3378) (#3463)
* Release v0.51.228 (stage-p12): workspace tree-drop #3411 + large-markdown preview #3378 #3411 (@pamnard): stopPropagation on workspace file-tree OS-file dragenter/dragover/drop so a tree drop uploads to the workspace WITHOUT also attaching to the composer. #3378 (@starGazerK): raise md rich-render ceiling 64KB/1500L->256KB/5000L + backend file read 200KB->400KB, add 'Render as markdown anyway' force button (reuses cached raw content, no extra fetch). * fix(workspace): force-render uses fresh path-scoped cache, blocked while dirty (#3378 Codex follow-up) Codex review of #3378 found the markdown force-render path had two SILENT bugs: (1) saving a md file from the plain-text fallback didn't update _previewRawContent, so a later force-render showed stale pre-save content; (2) the cache-reuse check 'path===_previewCurrentPath' was tautological (var just assigned), so a force-render after a file switch could render the previous file's cached content. Fixes: track _previewRawContentPath (set on fetch AND save), reuse cache only when it matches the requested path, and block force-render while the editor is dirty/open. +3 regression tests. (#3411 was cleared clean by Codex.) --------- Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
fdfb935b5e |
Release v0.51.227 — Release GU (stage-p11 — keep active New Chat visible in sidebar #3408) (#3461)
* fix(sidebar): keep active New Chat visible before first message (#3408, @AJV20) Squashed net diff of #3408. Injects ONLY the active ephemeral session into the sidebar render rows (when the server list omits it) so a freshly-created New Chat stays visible/selected before its first turn; inactive empty sessions stay filtered as before. New Chat also resets a CLI source-filter back to webui so the active chat isn't immediately hidden. * fix(sidebar): gate active-row reinjection to 0-message ephemeral only (#3408 Codex follow-up) Codex review found _ensureActiveSessionRowPresent re-injected ANY active session after search-merge — so an active conversation WITH messages that was correctly filtered out by the search query would pollute unrelated search results. Gate the reinjection to Number(activeRow.message_count||0)<=0 so only the freshly-created 0-message ephemeral chat is re-added; an active chat with messages stays filtered by search as before. Added a regression test asserting the gate. --------- Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
0b5458f3da |
Release v0.51.226 — Release GT (stage-p9 — mobile composer context-usage ring #3062 + activity-feed default-expand setting #3080) (#3459)
* feat(composer): replace mobile config-button sliders icon with context-usage ring (#3062, @NottheGuy007) Squashed net diff of #3062 (the PR branch's tip commits were deletes of files not present in our tree). Replaces the composerMobileCtxBadge text badge with an SVG progress ring (ctx-arc + ctx-num) showing real-time context-window usage: ring fill via stroke-dashoffset, centered percentage, color-coded green<=50% orange<=85% red>85%. Ring resets to 0%/green on new session. * feat(activity): add 'expand activity feed by default' appearance setting (#3080, @AJV20) Squashed net diff of #3080. Adds a Settings -> Appearance checkbox (activity_feed_expanded_default, default off) to expand new Activity disclosures by default; preserves manual per-turn collapse/expand (explicit toggle still wins); live 'Waiting on model' rows explain what the agent is doing before/after tool calls. i18n keys for all locales. * docs(changelog): v0.51.226 — context-usage ring (#3062) + activity-feed default-expand setting (#3080) * test(mobile): update touch-target test for #3062 ring (badge removed) #3062 replaced the composerMobileCtxBadge text badge with the SVG context-usage ring (composerMobileCtxRing) but left 3 stale references in tests/test_mobile_layout.py. The full suite caught test_mobile_composer_primary_controls_keep_touch_friendly_sizing asserting the removed .composer-mobile-ctx-badge CSS rule + #composerMobileCtxBadge element. Updated the assertion to the new ring: confirm composerMobileCtxRing exists, the old badge is fully gone (not dangling), and the ring SVG is aria-hidden so it stays a decorative overlay that doesn't steal the config button's 44px touch target (which is still asserted via .composer-mobile-config-btn above). 56/56 mobile-layout tests pass. Codex: no production JS dereferences the removed badge. --------- Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
ec46f05c92 |
Release v0.51.225 — Release GS (stage-p7 — remote gateway health probe resolves gateway_state #3355) (#3458)
* fix(health): probe /health/detailed first and unify gateway env vars (#3355) * docs(changelog): v0.51.225 — remote gateway health probe gateway_state fix (#3355) * fix(health): normalize gateway URL health-suffix + cap remote body read (#3355 Codex follow-up) Codex review of #3355 found two issues: (1) SILENT — a gateway env var already pointing at a health path (e.g. GATEWAY_HEALTH_URL=http://host/health) produced doubled paths like /health/health/detailed once probe paths were appended. Now strip a trailing /health/detailed, /health, /v1/health, /status suffix before appending (mirrors api/updates.py). (2) CORE — the new resp.read() on a 2xx body was unbounded; a large/trickled remote response could hang /api/health/agent or balloon memory. Cap the read at _REMOTE_PROBE_BODY_LIMIT_BYTES (64KB)+1 and skip JSON parse when over cap (still report the gateway alive, just without parsed gateway_state). Adds regression tests for both (no doubled /health/health path; oversized body is capped + does not hang). Also updated _FakeResp.read to accept the size arg. --------- Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: nesquena-hermes <[email protected]> |
||
|
|
bb5aa02523 |
Release v0.51.224 — Release GR (stage-p6 — profile tool/skill config authoritative on streaming worker #3294) (#3456)
* fix: respect profile toolset/skill config on WebUI streaming worker (#3294) The streaming agent runs on a detached worker thread that does not inherit the per-request thread-local profile context (set from the hermes_profile cookie on the HTTP handler thread). On that worker, the ambient get_config() resolves via get_active_profile_name() which falls back to the process-global _active_profile (usually 'default'). A session under a non-default profile with an empty platform_toolsets.cli therefore loaded the DEFAULT profile's full toolset list, inflating a tools-disabled profile's prompt from ~400 to ~15K input tokens. Add api.config.get_config_for_profile_home() — a race-free direct disk read of an explicit profile home's config.yaml (no shared-cache mutation), which defers to get_config() when the requested home matches the ambient path so in-memory test overrides are preserved. The streaming worker now resolves _cfg from the session's own profile home, fixing toolsets, prefill context, and fallback chains in one place. Closes #3294 Co-authored-by: gottipx <gottipx@users.noreply.github.com> * docs(changelog): v0.51.224 — #3294 profile toolset config (dropped #3405 to hold per Codex stale-model-repair finding) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: gottipx <gottipx@users.noreply.github.com> |
||
|
|
b850a88e8b |
Release v0.51.223 — Release GQ (stage-p5 — openai-api first-class picker provider #3443 + MiniMax-M3 #3374) (#3453)
* feat: upgrade MiniMax default model to M3
Add MiniMax-M3 as the new default and prune deprecated older
versions (M2.5/M2.5-highspeed/M2.1/M2) from the model catalog.
M2.7 (and M2.7-highspeed) is retained as the legacy compatible
option for users who pin to it.
Updated:
- api/config.py: _FALLBACK_MODELS adds minimax/MiniMax-M3 (placed
before M2.7 so the dropdown surfaces it first)
- api/config.py: _PROVIDER_MODELS['minimax'] adds M3 first, removes
M2.5/M2.5-highspeed/M2.1
- api/config.py: _PROVIDER_MODELS['minimax-cn'] adds M3 first,
removes M2.5/M2.1/M2
- tests/test_minimax_provider.py: updated CN catalog assertions
to match the new {M3, M2.7} list
API URL and TTS configuration are unchanged.
Co-Authored-By: Octopus <liyuan851277048@icloud.com>
* fix(models): register openai-api as first-class picker provider
* fix(models): detect OPENAI_API_KEY as openai-api, not bare openai (#3443 Codex follow-up)
Codex review found #3444 added the openai-api picker entry but the env-detection
side still did detected_providers.add('openai') for OPENAI_API_KEY. The agent
registry has only openai-api and openai-codex (no bare openai), so a env-only
OPENAI_API_KEY setup emitted @openai: picker entries the agent can't resolve on
the send path. Detect openai-api to match the registry. Adds a regression test.
* docs(changelog): v0.51.223 — re-stamp keep-set (#3443 openai-api + #3374 MiniMax-M3); dropped #3289 + #3264 to hold per Codex
---------
Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: Octopus <liyuan851277048@icloud.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
|
||
|
|
cccb97d970 |
Release v0.51.222 — Release GP (stage-p4 — backend bugfix batch: title language drift #3293 + orphaned CLI sidecar prune #3238 + pin-quota lineage #3288) (#3452)
* fix: reject cross-script drifted auto-generated session titles (#3293) The title-language mismatch guard only knew two states: German (de) or empty, and _title_language_mismatch early-returned False whenever the user start wasn't German. So an English conversation whose LLM-generated title came back in Chinese / Spanish / Russian sailed through and persisted with llm_title_generated=true. The German case was the only one covered because that's the one prior report it was built for. Generalize from a German-specific binary to a language-agnostic cross-script check. Add _script_counts() + _dominant_script() (cheap, dependency-free Unicode-block classification: latin / cjk / cyrillic / arabic / hebrew / greek / devanagari). _title_language_mismatch now rejects a title that introduces a substantial amount (>=35% of alphabetic chars, min 2) of a script different from the conversation start's dominant script — so short titles that embed a borrowed Latin technical term still trip, while an English title with a single foreign place-name does not. The legacy German->English same-script heuristic is preserved verbatim. Kept api/streaming.py ASCII-only (the test_title_generation_source_has_no_cjk_ literals guard) — all CJK examples live in the test file, not the source. Closes #3293 Co-authored-by: andrewkangkr <andrewkangkr@users.noreply.github.com> * fix: prune orphaned imported-CLI sidecars from the WebUI sidebar (#3238) When a CLI/agent session is opened in WebUI it gets a WebUI-owned sidecar (webui/sessions/<id>.json + _index.json row) so it can render and reopen; all_sessions() then returns it independently of the agent state.db. If the user later deletes that session from the CLI / local Hermes storage, nothing pruned the sidecar — the merge loop only overlays CLI metadata when a matching state.db row exists and otherwise continues, so the stale row lingered in the sidebar indefinitely (there is no WebUI delete affordance for CLI rows). Add api.models.agent_session_row_exists(): an exact, uncapped existence probe against the state.db sessions table. The sidebar merge loop now drops a row that is_cli_session_row + not WebUI-native + absent from cli_by_id + whose state.db row is genuinely gone, and calls prune_session_from_index() so _index.json self-heals. The state.db probe is deliberate: get_cli_sessions() caps at CLI_VISIBLE_SESSION_LIMIT (20), so a still-existing session can fall out of that window and look deleted — pruning on cli_by_id absence alone would delete live sessions. WebUI-native rows with a CLI ancestor are never pruned, and any probe error degrades to keep-the-row so a transient failure can't lose data. Closes #3238 Co-authored-by: Luxciax <Luxciax@users.noreply.github.com> * fix: count pin quota by visible session lineage * docs(changelog): v0.51.222 — backend bugfix batch (#3293 title drift, #3238 sidecar prune, #3288 pin lineage) * fix(pins): forks count as own pin lineage, not collapsed to parent (#3288 Codex follow-up) Codex review of the batch found a pin-limit UNDERCOUNT: _session_row_lineage_root_id followed any parent_session_id to the root, but /api/session/branch creates independent visible fork sessions that also carry parent_session_id (session_source= 'fork'). Two pinned forks of the same parent collapsed to one quota lineage, letting a user exceed pinned_sessions_limit with no 400. Fix: a fork returns its own id as its lineage root (it's a separately-visible session); only compression/continuation rows still collapse to a shared root. Adds a regression test with two pinned forks + the parent counting as three distinct lineages, and confirms the existing pre-compression-snapshot collapse case still passes. * test(pins): update #2508/#2821 source-match tests for #3288 lineage rename #3288 replaced the raw-session-id pin counter (pinned_ids set) with a visible-lineage counter (pinned_lineage_ids via _visible_pinned_lineage_ids over persisted_rows/candidate_rows). Two pre-existing source-string-matching tests asserted the OLD implementation literals (pinned_ids = {, _session_field(existing, session_id...), len(pinned_ids) >=). Updated both to assert the new mechanism while preserving the invariants they actually guard: snapshot computed BEFORE LOCK (no all_sessions()-inside-LOCK deadlock), quota filtering via the shared _session_counts_toward_pin_quota helper, and the limit/400 guard. Behaviour unchanged; these were implementation-detail assertions, not behaviour tests. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: andrewkangkr <andrewkangkr@users.noreply.github.com> Co-authored-by: Luxciax <Luxciax@users.noreply.github.com> Co-authored-by: Andy Kang <andrewkang.kr@gmail.com> |
||
|
|
7c48c37629 |
Release v0.51.221 — Release GO (stage-p3e — block all workspace symlink escapes + portable TOCTOU hardening [security]) (#3398) (#3451)
* [security] fix(workspace): block all symlink escapes from the selected workspace (#3398, @Hinotoi-agent) Previously safe_resolve_ws allowed a symlink placed inside a workspace to resolve to an external host path as long as it wasn't a system dir (/etc, /proc, etc). But the workspace file API is reachable by LLM agent tool calls (read_file_content), so an in-workspace symlink to ~/.ssh, ~/.hermes/auth.json (credentials), etc. was a real read path. Now ANY symlink escape is blocked: safe_resolve_ws resolves and requires the result stay under the workspace root; list_dir hides escaping symlinks (they could never be opened anyway); internal symlinks resolving back under the workspace still work. Updated the upload symlink-target test to accept the new 400 'Path traversal blocked' rejection (was 403) — the invariant (nothing lands outside the workspace) is unchanged. Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com> * docs(changelog): v0.51.221 release header for #3398 symlink-escape security fix * [security] harden workspace file API against symlink-swap TOCTOU via portable anchored openat-walk (#3398 follow-up) Codex review of #3398 flagged that safe_resolve_ws() validates a path but list_dir/read_file_content/upload/extraction then re-open by pathname, leaving a TOCTOU window: a symlink swapped in AFTER the check could still escape. (This race pre-existed #3398; closing it here so the containment is complete.) A first attempt used /proc/self/fd for the post-open containment check, but that BRICKS workspace browsing on macOS/Windows (no /proc → every read/list rejected). This version is portable: - open_anchored_fd(): opens the (already symlink-resolved) target component-by-component from the workspace root via openat (dir_fd) + O_NOFOLLOW. Every component must be a real non-symlink entry, so a component swapped to a symlink mid-flight is refused. No /proc dependency. Used by read_file_content (read from the fd) and list_dir (enumerate via os.scandir(fd), per-entry fstatat/readlinkat). - open_anchored_create_fd(): same anchored walk for writes, creating missing intermediate dirs with mkdir(dir_fd=) and the leaf with O_CREAT|O_EXCL| O_NOFOLLOW. Used by the workspace upload write AND archive (zip+tar) member writes, anchored against the TRUE workspace root (not the mutable extraction dest_dir, closing Codex's root-swap finding). fd-leak-safe on rejection. - Portability: gated on os.supports_dir_fd; platforms without it (Windows, where symlink creation needs admin) fall back to a plain O_NOFOLLOW open/exclusive create — no new race protection but no regression vs the prior path-based code. Legit in-workspace symlinks still resolve and read/list fine (safe_resolve_ws collapses them to a real in-workspace path, which the anchored walk then opens). Verified: the swap-race leaks external content against the old path-based read and is blocked here; macOS-class symlinked-root workspaces work; no fd leak over 300 rejected creates. Adds TOCTOU + anchored-create regression tests. * [security] close 3 more #3398 TOCTOU gaps from Codex r3: root-swap, pre-create mkdir, Windows list_dir fallback Codex round-3 review found three residual issues in the anchored openat-walk: 1. (CORE) The workspace ROOT itself could be swapped to a symlink after resolve() but before the root os.open() — add _O_NOFOLLOW to the root open in open_anchored_fd() and open_anchored_create_fd() (and make_anchored_dir()), so a raced root symlink is refused. Verified: root-swap race now blocked. 2. (SILENT) Upload/extraction still did pathname Path.mkdir() AFTER the containment check, so a raced symlink component could make the server create dirs outside the workspace before the anchored file create rejected. Removed the redundant member_path.parent.mkdir() calls (open_anchored_create_fd already creates intermediates via anchored mkdirat) and replaced the two base-dir mkdirs (upload target dir + archive extraction root) with a new make_anchored_dir() that walks from the true workspace root via openat+O_NOFOLLOW + mkdir(dir_fd=). 3. (CORE) list_dir() unconditionally used os.scandir(fd)/os.stat(dir_fd=)/ os.readlink(dir_fd=), which would brick workspace browsing on platforms without os.supports_dir_fd (Windows). Split list_dir() into a _DIR_FD_OK anchored branch and a path-based fallback branch (prior behaviour) sharing one _process() entry builder. open_anchored_create_fd()'s Windows fallback now also creates parent dirs. Adds regression tests: no-dir_fd fallback (list+read+create+symlink filtering) and the root-swap race. All prior TOCTOU + anchored-create tests still green. * fix(workspace): portable symlink-loop filtering in list_dir via follow-stat ELOOP CI on Python 3.13 caught test_mutual_symlink_loop_filtered failing: a mutual symlink loop (a->b->a) was NOT filtered from the listing. Root cause: the new readlink-based cycle detection relied on (target_resolved / raw_link).resolve() RAISING on a loop, but Path.resolve() loop handling differs by Python version (3.11 raises RuntimeError, 3.13 can return a path), so the loop slipped through on 3.13. Fix: compute a version-independent 'reachable' flag per symlink via os.stat(..., follow_symlinks=True) — the syscall reliably returns ELOOP for mutual/self loops and ENOENT for broken targets on every platform/version. A symlink whose follow-stat raises can never be opened, so list_dir filters it. Applied in both the dir_fd-anchored branch (fd-relative stat) and the Windows path-based fallback branch. Mutual loop now filtered on all versions. --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com> |
||
|
|
42aef61b31 |
Merge pull request #3442 from nesquena/docs/contributor-attribution-backfill
docs: backfill contributor attribution for absorbed PRs (graph credit + CONTRIBUTORS.md) |
||
|
|
f25ff515a0 |
docs: backfill contributor attribution for absorbed PRs (#2622, #2931, #3104, #3220, #3223, #3337)
Six contributor PRs were shipped via the cherry-pick/absorb path but their
absorb commits never carried a `Co-authored-by:` trailer, so the contributors
received zero commit credit on their GitHub contribution graphs. Three of them
(@antoniocarlos97ss, @liuqiangweb-svg, @pix0127) were also missing from
CONTRIBUTORS.md entirely; the other three (@AJV20, @mysoul12138) were already
listed via CHANGELOG attribution but still lacked the graph credit.
This commit:
- Adds the three missing contributors to CONTRIBUTORS.md (single-PR section).
- Carries Co-authored-by trailers for all six so each gets a real commit on
their contribution graph (the non-history-rewrite way to repair this).
- Bumps the tracked totals (194 -> 197 contributors, 843 -> 846 credits).
The shipped work, by PR:
#2622 (@pix0127) WebUI dashboard plugin system w/ iframe isolation
#2931 (@liuqiangweb-svg) Edge TTS as an alternative speech engine
#3104 (@antoniocarlos97ss) workspace file upload + drag-drop w/ archive extract
#3220 (@AJV20) generated media artifact cards
#3223 (@AJV20) manual session title regeneration
#3337 (@mysoul12138) syntax highlighting in workspace file preview
Co-authored-by: pix0127 <8500500+pix0127@users.noreply.github.com>
Co-authored-by: Andy <281253538+liuqiangweb-svg@users.noreply.github.com>
Co-authored-by: antoniocarlos97ss <101895404+antoniocarlos97ss@users.noreply.github.com>
Co-authored-by: AJV20 <24819659+AJV20@users.noreply.github.com>
Co-authored-by: mysoul12138 <203929894+mysoul12138@users.noreply.github.com>
|
||
|
|
b774b03900 |
Merge pull request #3441 from nesquena/release/stage-p3c
Release GN — v0.51.220 (fix aux title generation 422 with @provider: model ids, #3430) |
||
|
|
70068522fb | docs(changelog): v0.51.220 release header for #3430 aux-title @provider fix | ||
|
|
c556d649b7 |
fix: strip @provider: prefix from auxiliary title_generation model id (#3430, @pamnard)
Manual title regeneration (POST /api/session/title/regenerate) and background aux title generation failed with 422 / llm_error_aux when auxiliary.title_generation.model in config.yaml used the WebUI-internal @provider:model picker format (e.g. @gemini:gemini-3.1-flash-lite) — the raw @-qualified id was forwarded to the provider API verbatim. Normalize it through the canonical _split_webui_provider_model_value() helper before the aux call. Co-authored-by: pamnard <pamnard@users.noreply.github.com> |
||
|
|
b9a0b77baa |
Merge pull request #3439 from nesquena/release/stage-p3b
Release GM — v0.51.219 (extend URI-scheme model-ID fix to backend normalization, #3436) |
||
|
|
3091ac9512 | docs(changelog): v0.51.219 release header for #3436 backend URI-scheme fix | ||
|
|
264863cc37 |
fix(config): extend #3429 URI-scheme fix to backend normalization + model-key matching (#3436, @b3nw)
v0.51.218 fixed getModelLabel() (the visible chip) for URI-scheme model IDs but left the same first-segment-slash-strip bug in the matching/dedup paths: api/config.py _norm_model_id + _get_label_for_model, and static/ui.js _normalizeConfiguredModelKey. For gpt://folder/model/latest those treat the path slashes as provider delimiters, mis-normalizing the model-identity key (the #3360 collision class, for URI ids). Adds a URI-scheme guard to all three so the scheme is not stripped. Kept v0.51.218's getModelLabel (more thorough than #3436's variant — it extracts the model name vs returning the whole id); took #3436's backend + _normalizeConfiguredModelKey halves with backend/frontend parity tests. Co-authored-by: b3nw <b3nw@duck.com> |
||
|
|
f7087a3d1d |
Merge pull request #3438 from nesquena/release/stage-p3a
Release GL — v0.51.218 (fix getModelLabel mangling URI-scheme model IDs, #3429 regression) |
||
|
|
25e95a7366 |
fix(ui): #3429 round-2 — never fall back to authority or placeholder (Codex MUST-FIX)
Codex re-check: degenerate URIs still leaked — gpt://folder123 returned the
authority, gpt://folder123/${MODEL} returned the placeholder. Removed the _all[0]
authority fallback and guarded the literal-last-path fallback against placeholders;
a URI with no usable model segment now falls back to the raw id. Added regression
cases for gpt://folder123 and gpt://folder123/${MODEL}.
|
||
|
|
c223b418a5 |
fix(ui): #3429 follow-up — drop authority, keep digit-leading model names (Codex MUST-FIX)
Codex gate found two edges in the first cut: (1) the candidate segment list included the URI authority, so gpt://folder123/v4 and .../latest returned the folder; (2) _isVersionTail matched any digit-leading segment, dropping a real model named 2026-model. Fix: build path segments from AFTER the authority only; tighten the version-tail regex to pure version tokens (latest/stable/v4/1.2), not mixed names; fall back to last-usable (non-placeholder) path segment so the authority is never returned. Added edge-case regression tests. |