Skip to content

sync: upstream v0.51.184 (full catch-up from v0.51.131)#42

Merged
Du7chManiac merged 554 commits into
masterfrom
sync/upstream-v0.51.132
May 31, 2026
Merged

sync: upstream v0.51.184 (full catch-up from v0.51.131)#42
Du7chManiac merged 554 commits into
masterfrom
sync/upstream-v0.51.132

Conversation

@Du7chManiac
Copy link
Copy Markdown

@Du7chManiac Du7chManiac commented May 31, 2026

Full catch-up sync of upstream nesquena/hermes-webui into the fork: from our prior tip v0.51.131 all the way to the current upstream tip v0.51.184. After this lands, the fork is fully in sync with upstream (0 un-merged v* tags).

Opened/maintained manually because the scheduled sync-upstream workflow cannot push branches that touch .github/workflows/ — its GITHUB_TOKEN lacks the workflows scope (failed run #26694022064). Almost every upstream tag in this range touches a workflow file, which is why the backlog had grown to 50+ tags.

Merge structure

Two --no-ff merge commits preserved for bisectability:

  1. Merge tag 'v0.51.132' — the next stage-by-stage tag (3 conflicts).
  2. Merge tag 'v0.51.184' — catch-up to tip (8 conflicts).

Conflicts resolved

v0.51.132 (Tier 2): api/auth.py (union PUBLIC_PATHS; folded multi-user has_users() into upstream's new is_password_auth_enabled()), api/routes.py (/api/auth/status kept our first_boot/users_configured gate + added passkey fields), static/login.js (kept multi-mode onSubmit, added passkey machinery, fixed invalidPwinvalidCreds).

v0.51.184 (Tier 2 / 2b / 3):

Note on the split webui/agent update feature

The fork goal of split WebUI/Agent updates has converged with upstream (PR nesquena#2843 "Ignore Agent updates", PR nesquena#2893 zero-target guard). This PR keeps our per-target implementation rather than swapping to upstream's single-banner internals, because the user-facing split is preserved either way and our structure is what the rest of the fork's code/tests depend on. The fork-only origin-published-tags fix (_origin_release_tags) is retained — it's structural to being a fork that sync-merges upstream tags origin never publishes.

Validation

Follow-ups (not in this PR)

  • The sync workflow still can't push workflow-file changes — durable fix is a PAT / GitHub App token with workflow scope. Until then, syncs touching .github/workflows/ must be pushed manually.
  • docker-smoke.yml still has some ghcr.io/nesquena/... image references alongside our thecouchcoder-com retag; pre-existing, worth reconciling separately.

🤖 Generated with Claude Code

AJV20 and others added 30 commits May 28, 2026 10:37
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
11-PR low-risk cleanup:
- nesquena#3043 openai-codex models.dev reasoning passes xhigh
- nesquena#3044 reset _messagesTruncated on new session
- nesquena#3047 discoverability: api lineage representative for stale CLI flag
- nesquena#3049 title-language detection threshold + English false-positive fix
- nesquena#3051 docker docs: sudo compose + Linux host-gateway
- nesquena#3054 SSE reconnect: visible-but-unfocused current pane
- nesquena#3055 fallback title: drop German-only Session Bilder case
- nesquena#3056 title prompt: language-neutral instruction
- nesquena#3070 /api/upload reports actual stored filename
- nesquena#3071 clarify SSE fallback preserves owner session id
- nesquena#3072 gateway-chat forwards image attachments as image_url parts
stage-batch35: v0.51.153 / Release DY — 11-PR low-risk cleanup
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
#	tests/test_webui_gateway_chat_backend.py
9-PR medium-risk cleanup:
- nesquena#3037 routes.py: argv-style prefill hook + env-var override for notes drawer
- nesquena#3046 models.py: compression parent not repaired as stale interrupted turn
- nesquena#3048 session_discoverability.py: --repair-safe CLI with default dry-run
- nesquena#3053 ui.js: streaming KaTeX guard for parser-owned equations
- nesquena#3059 models.py: empty partial activity rows excluded from sidebar recency
- nesquena#3060 profiles.py: API key writes to .env (chmod 600), not config.yaml
- nesquena#3064 routes.py: MEDIA: image tokens allow exact session-referenced paths
- nesquena#3069 models.py: cron sessions with project_id surface via Cron Jobs chip
- nesquena#3077 gateway_chat.py: HTTP 401 maps to gateway_auth_error event
nesquena-hermes and others added 24 commits May 31, 2026 02:45
…y-list (Codex review #2)

Under a named profile, process HERMES_HOME is ~/.hermes/profiles/<name> but the
allowlist still grants base ~/.hermes — so the prior deny (anchored only on the
active-profile root + STATE_DIR) left ~/.hermes/state.db and sibling-profile
secrets (~/.hermes/profiles/other/auth.json) reachable. Build deny roots from
every Hermes state root the allowlist accepts: active HERMES_HOME, base ~/.hermes,
api.profiles._DEFAULT_HERMES_HOME, and STATE_DIR; apply the state-subdir dir-denies
under each. Widen the CSP-slice structural test window to match.
…kspace carve-out (Codex review #3)

Codex round-3 found the prior multi-profile hardening OVER-blocked: denying
STATE_DIR + base/profiles wholesale 403'd legitimate active-workspace media.
Redesign around a single principle: the ACTIVE WORKSPACE is the user's own
content (never deny), Hermes INTERNAL STATE lives outside any workspace (deny).
If target is inside the active workspace -> allow; else deny known secret/config
basenames + internal state subdirs across all Hermes roots. Also folds in Opus
defense-in-depth: adds cron/logs/checkpoints/backups subdirs +
gateway_state.json/channel_directory.json/jobs.json basenames. Adds an
over-block regression test (a /tmp artifact named settings_* still serves 200).
…broad/internal roots (Codex review #4)

Codex round-4: the carve-out could re-open the hole if the active workspace is
pathologically set to a broad/internal root ($HOME, ~/.hermes, a profile root)
— get_last_workspace only checks is_dir(), so workspace=~/.hermes would serve
state.db. Gate the carve-out: disable it when the active workspace IS, CONTAINS,
or is CONTAINED BY any Hermes root, or is $HOME / a */profiles dir / a named
profile root / an internal state subdir. Adds a unit test proving state.db stays
403 when the active workspace is the Hermes home. Widen CSP-slice test window.
…make bare file:// rewrite code-fence-aware (Codex review #5)

1. api/routes.py: case-fold /api/media deny filename + dir containment checks
   (os.path.normcase + casefold) so STATE.DB / Sessions/ cannot bypass the
   state/secret deny on case-insensitive filesystems (macOS/Windows).
2. static/ui.js: move the bare file:// media-stash pass to run AFTER fenced-block
   and inline-code stashing, so a file:// inside a code block / backtick span
   stays literal text instead of becoming an auto-loaded <img>. The MEDIA: stash
   keeps its first-position precedent.
Adds behavioral renderer tests (real renderMd via node) for fenced + inline code
file:// staying literal, bare file:// becoming media, and anchors keeping the
link path. Closes the last Codex review items for nesquena#3234.
…hecks, fix workspace over-block, protect raw <pre> (Codex review #6)

1. routes.py: hoist a single case-folded path helper (_norm/_within_ci/_equal_ci)
   used for ALL deny + carve-out comparisons (consistent macOS/Windows safety).
2. routes.py: split the deny into (a) dir-based denies that ALWAYS fire (even
   inside the active workspace — so a workspace overlapping a state dir cannot
   expose sessions/memories), and (b) filename denies relaxed only by the
   carve-out. Fix the over-block: a workspace that is a proper DESCENDANT of a
   Hermes root (e.g. STATE_DIR/workspace) is a legit project workspace and keeps
   the carve-out; only a root-itself / ancestor / $HOME / profiles / state-subdir
   workspace disables it.
3. ui.js: move the bare file:// media-stash pass after the raw-<pre> stash too,
   so file:// inside a raw <pre> block stays literal (not just fenced/inline code).
…TATE_DIR/workspace media (Codex review #7)

The default workspace lives at STATE_DIR/workspace, so denying STATE_DIR itself
403'd legitimate workspace media. STATE_DIR is already in _hermes_roots, so its
sensitive subdirs (STATE_DIR/sessions, /memories, /profiles, etc.) are still
covered by the per-root subdir loop; direct sensitive files are still caught by
the filename denies. Drop the wholesale _state_dir deny. Adds a regression test
proving STATE_DIR/workspace/shot.png serves while STATE_DIR/sessions/*.json 403s.
…esale profiles deny (Codex review #8)

Denying the whole <root>/profiles tree 403'd legitimate named-profile workspace
media (<base>/profiles/p1/workspace/shot.png). Fix: remove 'profiles' from
_DENY_SUBDIRS and instead enumerate each <root>/profiles/<name> directory as its
own Hermes root — so each profile's sensitive subdirs (sessions/memories/cron/
logs/checkpoints/backups) + secret filenames are denied, while that profile's
workspace/ is allowed via the carve-out. Adds a regression test: named-profile
workspace media serves, profile + sibling-profile auth.json stay 403.
….json + .login_attempts.json (Codex review #9)

Codex direct probe found three more auth-state basenames under STATE_DIR that
/api/media still served: passkeys.json + .passkey_challenges.json (api/passkeys.py)
and .login_attempts.json (api/auth.py). Add them to _DENY_FILENAMES.
…under Hermes roots (Codex review #10)

auth.py/passkeys.py write via tmp*.<name>.tmp sidecars then rename; deny those
suffixes (.sessions.tmp, .login_attempts.tmp, .passkeys.tmp, .passkey_challenges.tmp)
under Hermes roots so a momentary temp file can't be fetched via /api/media.
…rs (Codex review #11)

Per-profile WebUI state lives at <root>/webui_state (api/workspace.py), so
<base>/profiles/<name>/webui_state/sessions/*.json was reachable — it is not a
direct child of the profile root, so the prior deny-subdir loop missed it. Add
<root>/webui_state/<state-subdir> to the deny dirs for every Hermes root. Adds a
regression assertion (profile webui_state/sessions/*.json → 403).
Release stage-batchC → v0.51.183 (nesquena#3219 inline file:// media + nesquena#3234 /api/media secret-file confinement)
… transcribing

- Settings toggle in Sound section (after voice mode button)
- Mic button shows 'RAW' badge when raw audio mode is active
- Raw mode: record → pending file → send() (auto-send if textarea empty)
- Dictation mode: unchanged (transcribe → textarea)
- 12 locales with full i18n keys and translations
- 4 backend tests for upload + transcribe regression

PR: Thinking Path
- Hermes WebUI has dictation via Web Speech API / MediaRecorder → /api/transcribe
- Telegram sends raw audio as attachment, agent decides what to do
- Raw audio mode enables external STT, emotion/noise analysis, multimodal models

What Changed
- static/index.html: settingsRawAudio checkbox in Sound section
- static/boot.js: raw audio preference, send raw blob as pending file
- static/style.css: badge RAW on mic button
- static/i18n.js: 12 locales with translations
- tests/test_raw_audio_upload.py: 4 tests for upload + regressions

AI Usage
- Provider: opencode-go
- Model: deepseek-v4-flash
…title

The global _setButtonTooltip only sets data-tooltip, not data-i18n-title.
The i18n system reads data-i18n-title for locale-aware tooltip updates.
Switched to _setButtonTooltipAndKey which manages both attributes.
The .has-tooltip CSS uses ::after to show the tooltip text.
Using ::after for the RAW badge overwrote the tooltip content.
Fixed by adding a <span class="mic-raw-badge"> child element via JS.

Also: use _setButtonTooltipAndKey for consistent i18n tooltip management.
…ests

- 'Badge' naming was misleading since the visual badge was removed
- Use monkeypatch.setitem with pytest fixture instead of manual
  import sys + del sys.modules (safer cleanup, no inline import)
- All 9 tests pass
Bug: when raw audio mode is enabled, SpeechRecognition is
skipped and MediaRecorder is used instead. But _stopMic()
checked for a truthy 'recognition' reference (which exists
even when unused) and called recognition.stop() — a no-op
since speech recognition was never started — never reaching
the mediaRecorder.stop() branch. Recording could never be
stopped by the mic button.

Fix: add !_rawAudioMode guard to the recognition.stop() branch.
…w-audio mode (Codex review)

Codex pre-release gate: clicking Send while raw-audio recording with text in the
composer attached the audio but never sent — btnSend sets _micPendingSend=true
before _stopMic, but _sendRawAudio only called send() when the textarea was empty.
Mirror the transcribe path: if _micPendingSend is set, clear it and send()
regardless of composer text; otherwise keep the empty-composer auto-send + toast.

Co-authored-by: lucasrc <lrclucas@gmail.com>
…ode) — toggle-mid-record safety (Codex review #2)

Codex round-2: _stopMic and mediaRecorder.onstop read the CURRENT _rawAudioMode
to choose backend/dispatch, but the recording was started on the OLD mode — so
toggling Settings→Sound mid-recording could stop the wrong backend (orphaning
the other) or dispatch raw-vs-transcribe wrongly. Pin _activeCaptureMode
(speech | media-raw | media-transcribe) at start; _stopMic + onstop use it.
Adds front-end source-invariant regression tests.

Co-authored-by: lucasrc <lrclucas@gmail.com>
Release stage-batchD → v0.51.184 (raw audio mode nesquena#3169 + scroll-preserve nesquena#3237 + non-POSIX test skip nesquena#3235)
Release DD: stage-batch14 — 4 PRs (replayed-context, interrupted-response, shutdown affordance, passkey opt-in)

# Conflicts:
#	api/auth.py
#	api/routes.py
#	static/login.js
@Du7chManiac Du7chManiac added the sync-upstream PRs from .github/workflows/sync-upstream.yml label May 31, 2026
v0.51.184 — Release FD (stage-batchD): raw audio upload mode (nesquena#3169) + scroll-preserve on CLI import (nesquena#3237) + non-POSIX test skip (nesquena#3235)

# Conflicts:
#	.github/workflows/docker-smoke.yml
#	api/auth.py
#	api/routes.py
#	api/updates.py
#	static/ui.js
#	tests/conftest.py
#	tests/test_update_apply_ui.py
#	tests/test_update_banner_fixes.py
@Du7chManiac Du7chManiac changed the title sync: upstream v0.51.132 sync: upstream v0.51.184 (full catch-up from v0.51.131) May 31, 2026
The docker-smoke workflow built and cached ghcr.io/nesquena/hermes-webui
but every compose file references ghcr.io/thecouchcoder-com/hermes-webui,
so the build-image job's GHA layer cache fed no smoke variant and each
variant rebuilt the image from scratch via a separate unconditional
"Build local Dockerfile" step.

Rename the cache image to the fork name in both the build-image job and
the smoke "Restore from cache" step, and drop the redundant local build —
the restore step now produces ghcr.io/thecouchcoder-com/hermes-webui:latest
straight from the cache, so the multi-container variants exercise the PR's
Dockerfile without a from-scratch rebuild. Single-container is unaffected
(it uses build: .). Realizes the ~1-3 min/variant saving the comments
already promised.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Du7chManiac Du7chManiac merged commit 7e85656 into master May 31, 2026
16 checks passed
@Du7chManiac Du7chManiac deleted the sync/upstream-v0.51.132 branch May 31, 2026 07:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sync-upstream PRs from .github/workflows/sync-upstream.yml

Projects

None yet

Development

Successfully merging this pull request may close these issues.