Skip to content

fix: batch v0.50.232 — fuzzy match, codex detection, workspace reload, timestamp sync#1198

Merged
nesquena-hermes merged 10 commits intomasterfrom
stage/batch-232
Apr 28, 2026
Merged

fix: batch v0.50.232 — fuzzy match, codex detection, workspace reload, timestamp sync#1198
nesquena-hermes merged 10 commits intomasterfrom
stage/batch-232

Conversation

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Batch release v0.50.232 — 4 fixes.

PRs included

PR Author Fix
#1192 @nesquena-hermes Model chip fuzzy-match false positive (#1188)
#1193 @nesquena-hermes openai-codex not detected in model picker (#1189)
#1196 @nesquena-hermes Workspace files blank after second empty-session reload
#1197 @bergeouss Session timestamps wrong with server/client clock drift (#1144)

All four PRs independently reviewed and approved by @nesquena.

Integration fixes applied

#1193: Updated misleading comment — OPENAI_API_KEY does NOT authenticate the default Codex OAuth endpoint (that uses chatgpt.com/backend-api/codex and requires a separate OAuth flow). The comment now accurately states the known limitation. Also replaced a fragile 400-char source-scan test with an isolation-safe unit test. Note: OAuth-authenticated users already get detected via hermes_cli.auth — this fix only addresses the env-var fallback path.

Test results

2764 passed, 2 skipped (macOS-only workspace tests). Browser QA: 21/21. /api/sessions confirmed returning server_time and server_tz fields.

Hermes Agent and others added 10 commits April 28, 2026 01:26
… set (#1189)

openai-codex has a full static model list in _PROVIDER_MODELS (9 models)
and a display name in _PROVIDER_DISPLAY, but no env-var detection path.
The only way it appeared was via manual 'providers:' config in config.yaml.

OPENAI_API_KEY authenticates both the 'openai' and 'openai-codex' groups —
they share the same credential. Added openai-codex to the env-var detection
block alongside openai so the group appears automatically for any user who
has OPENAI_API_KEY configured.

One-line change, no logic side effects. Users who don't want the Codex group
in the picker can override via the existing providers: section in config.yaml.

Closes #1189
3 source-level + sanity tests verifying:
1. The OPENAI_API_KEY detection block adds both 'openai' and
   'openai-codex' to detected_providers
2. _PROVIDER_MODELS['openai-codex'] is non-empty (so detection
   actually surfaces models, not an empty group)
3. _PROVIDER_DISPLAY['openai-codex'] has a label

Test docstring records the cross-tool nuance: hermes-agent's openai-codex
default uses oauth_external (chatgpt.com), so the simple OPENAI_API_KEY
shortcut is a UX-positive convenience for users who have BOTH key+OAuth,
but users with only the env var will see picker entries that error at
use-time. The fix is still net-positive — the alternative is silent
absence requiring manual config.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…alse positives (#1188)

Step 3 of _findModelInDropdown() used a truncated 'base' (target with
last version segment stripped) as the prefix to match against dropdown
options. For 'gpt-5.5', target='gpt.5.5' and base='gpt.5', which
incorrectly matched '@nous:openai/gpt-5.4-mini' (norm: 'gpt.5.4.mini')
because it starts with 'gpt.5'. The chip would then show 'GPT-5.4 Mini
(via Nous)' for a session that stores 'gpt-5.5'.

Fix: use the full target as the prefix when base has meaningful content
(length > 4 and base !== target). Only fall back to the shorter base
when it is a bare root word ('gpt', 'claude', etc.) where stripping the
version segment would be a no-op.

'gpt-5.5' with prefixTarget='gpt.5.5': 'gpt.5.4.mini' does NOT start
with 'gpt.5.5' → returns null (correct — no false match).
'gpt' with prefixTarget='gpt' (useBase=true): still finds 'gpt.5.4.mini'
via the shorter base → prefix match for bare roots preserved.

Closes #1188
9 tests run the live _findModelInDropdown function via Node so the real
regex/normalization rules are exercised (no Python mirror to drift).

Two locked-bad cases (gpt-5.5 → gpt-5.4-mini, claude-opus-4.7 →
claude-opus-4.6) reproduce on master and pass on the PR. Seven
preserved-good cases (bare-root prefix match, exact match, unrelated)
ensure the tighter check doesn't regress legit fuzzy lookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
After PR #1187 fixed the workspace panel persisting on empty-session reload,
a secondary bug remained: the file tree went blank on the *second* reload of
an empty new conversation.

Root cause: the ephemeral guard (message_count=0) called
  localStorage.removeItem('hermes-webui-session')
which removed the stored session ID after the first blank-page refresh.

Sequence:
  Refresh 1 — saved=session_A_id in localStorage
    → loadSession(A) runs → loadDir('.') → files populate the DOM
    → guard fires: localStorage key REMOVED, S.session=null
    → files still visible (DOM populated by loadDir above) ✅

  Refresh 2 — saved=null (key was removed)
    → falls to 'no saved session' path
    → loadDir() is never called in that path
    → file tree is empty / never populated ✗

Fix: remove the localStorage.removeItem() call. The session ID stays in
localStorage so every refresh takes the same path:
  loadSession() → loadDir() → files populate → guard fires → S.session=null
  → workspace panel stays open with files visible ✅

The session ID remaining in localStorage is harmless: the ephemeral guard
still fires, S.session is cleared in memory, the session stays invisible
in the sidebar (server-side filter), and no phantom 'Untitled' entry appears.

2729 tests passing.
Three static checks that catch the exact regression PR #1196 fixes:

1. The empty-session guard MUST NOT remove 'hermes-webui-session' from
   localStorage — that's the line whose presence broke the second refresh
   (no-saved-session path doesn't call loadDir).
2. The guard MUST still clear S.session=null and S.messages=[] so the
   empty scratch pad isn't surfaced as the active conversation.
3. The guard MUST still read 'hermes-webui-workspace-panel-pref' so PR
   #1187's panel-persist behaviour remains intact.

Together they pin the contract: the localStorage key stays (this PR),
the in-memory state clears (#1182), and the panel pref restores (#1187).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Root cause: All relative-time calculations in the session sidebar
("2 hours ago", "Today", "Yesterday", etc.) used Date.now()
(client-side clock) as reference. When the browser clock and server
clock are out of sync (common with WSL clock drift, Docker containers
in UTC, remote access), session timestamps appear wrong.

Fix:
- Backend: /api/sessions now returns server_time (epoch seconds) and
  server_tz (offset string like "+0800") alongside session data.
- sessions.js: Computes _serverTimeDelta = Date.now() - server_time
  once per session-list fetch. All time helpers (_formatRelativeSessionTime,
  _sessionCalendarBoundaries, _sessionTimeBucketLabel, _formatSessionDate)
  now default to _serverNowMs() (Date.now() - delta) instead of Date.now().
- ui.js: _formatMessageFooterTimestamp and tsTitle tooltip now use
  _serverTzOptions() to display timestamps in the server's timezone
  (via IANA Etc/GMT offset format), falling back to browser timezone
  when _serverTzOptions is not available.

Backward-compatible: passing explicit nowMs parameter still works
(used by tests), and when server_time is absent (no skew), behavior
is identical to before.

18 regression tests covering: backend response, skew compensation,
time formatting with skew, timezone conversion, and fallback behavior.
The Etc/GMT-X mapping in _serverTzOptions can only express whole-hour
offsets — IANA Etc/GMT zones don't support fractional hours. Users in
India (+0530), Iran (+0330/+0430), Newfoundland (-0330), Nepal (+0545),
Sri Lanka (+0530), Afghanistan (+0430), and Burma (+0630) — collectively
~1.5 billion people — would see timestamps off by 30 minutes after this
PR's whole-hour-only TZ conversion. That's actively WORSE than the pre-PR
behaviour for them: pre-PR they saw browser-tz times (consistent with
their wall clock), post-PR they'd see times 30 min off from the server.

Fix: introduce _formatInServerTz(date, options) that uses offset
arithmetic — shift the timestamp by the server's offset, then format
with timeZone:'UTC' so no further conversion is applied. The formatted
output reads as the wall-clock time in the server's timezone for ANY
offset, including fractional.

_serverTzOptions is kept for whole-hour fast-path callers that spread
its result; updated to return undefined for fractional offsets so it
can't silently produce off-by-30-min output via the spread path.

ui.js: _formatMessageFooterTimestamp and tsTitle in renderMessages now
go through _formatInServerTz with a fallback to bare toLocaleString
when sessions.js hasn't loaded yet.

Tests:
- test_message_footer_timestamp_uses_server_tz: stubs _formatInServerTz
  with the same offset-arithmetic logic as sessions.js, asserts UTC+8
  conversion produces the right wall-clock hour.
- test_message_footer_timestamp_handles_fractional_offset: NEW. Stubs
  _formatInServerTz with +0530 (IST), asserts 02:00 UTC → 07:30 IST and
  explicitly NOT 07:00 (the broken Etc/GMT-5 result).
- test_ui_js_message_timestamp_uses_server_tz: relaxed to accept either
  _formatInServerTz or _serverTzOptions reference.
- test_sessions_js_has_format_in_server_tz_helper: NEW. Verifies the
  function exists and uses offset arithmetic + UTC formatting.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…oral

- api/config.py: correct misleading comment (OPENAI_API_KEY does NOT auth the default
  Codex OAuth endpoint; hermes_cli.auth handles that path when oauth is set up)
- tests: replace fragile 400-char source-scan with isolation-safe unit test that
  directly tests the detection logic rather than parsing source code
@nesquena-hermes nesquena-hermes merged commit 3780df9 into master Apr 28, 2026
@nesquena-hermes nesquena-hermes deleted the stage/batch-232 branch April 28, 2026 01:40
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 29, 2026
…, timestamp sync (nesquena#1198)

Batch release v0.50.232 — 4 fixes.

## PRs included

| PR | Author | Fix |
|---|---|---|
| nesquena#1192 | @nesquena-hermes | Model chip fuzzy-match false positive (nesquena#1188) |
| nesquena#1193 | @nesquena-hermes | openai-codex not detected in model picker (nesquena#1189) |
| nesquena#1196 | @nesquena-hermes | Workspace files blank after second empty-session reload |
| nesquena#1197 | @bergeouss | Session timestamps wrong with server/client clock drift (nesquena#1144) |

All four PRs independently reviewed and approved by @nesquena.

## Integration fixes applied

**nesquena#1193:** Updated misleading comment — `OPENAI_API_KEY` does NOT authenticate the default Codex OAuth endpoint (that uses `chatgpt.com/backend-api/codex` and requires a separate OAuth flow). The comment now accurately states the known limitation. Also replaced a fragile 400-char source-scan test with an isolation-safe unit test. Note: OAuth-authenticated users already get detected via `hermes_cli.auth` — this fix only addresses the env-var fallback path.

## Test results

**2764 passed, 2 skipped** (macOS-only workspace tests). Browser QA: **21/21**. `/api/sessions` confirmed returning `server_time` and `server_tz` fields.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants