Hard 16 MB cap on readFileDataUrl blocked larger local attaches with no way to raise it. Settings -> Chat now has a free-form MB field. Main process owns the persisted value and clamps only absurd inputs.
Critical fixes to salvaged PR #73020:
- Use SessionDB.append_message instead of raw INSERT INTO messages.
The original used wrong column names (session_key/created_at vs
session_id/timestamp) and bypassed FTS indexing, session metadata
updates, display_kind, and all other columns append_message handles.
- Use get_hermes_home() instead of hardcoded Path.home()/'.hermes'.
Profile-aware path resolution under HERMES_HOME override and active
profile isolation.
- Add 11 tests covering flush, recovery, serialisation, edge cases.
When FTS5 index corruption prevents INSERT INTO messages, the gateway
accumulates messages in _pending_messages (memory-only). On shutdown,
.clear() discards the only surviving copy — permanent user data loss.
Changes:
- Add gateway/shutdown_flush.py with flush_pending_to_file() and
recover_pending_to_db() for two-phase data preservation.
- Patch gateway/run.py: flush runner._pending_messages before clear()
in _stop_impl_body, and recover on startup after runner.start().
- Patch gateway/platforms/base.py: flush adapter._pending_messages
before clear() in the adapter shutdown path.
Recovery behavior:
- Reads pending JSON files from ~/.hermes/pending_messages/
- Inserts messages into state.db directly
- Per-session isolation: one corrupt session doesn't block others
- Successful recovery deletes the flush file
- Failed recovery re-saves for next startup retry
With HTTP(S)_PROXY (and similar mounted transports), live connections sit
on client._mounts rather than the default _transport. force_close_tcp_sockets
only walked the default pool, so stranger-thread interrupt abort logged
tcp_force_closed=0 and left the request alive for minutes (#72975). Also
walk nested proxy _connection wrappers and WARNING when an abort finds no
sockets.
- Change await self._send_read_receipt to asyncio.create_task to avoid
blocking message dispatch on slow bridge responses (matches BlueBubbles
pattern). Up to 5s per-message latency eliminated.
- Update tests: assert_called_once_with instead of assert_awaited_once_with
since the receipt is now scheduled, not directly awaited.
- Add send_read_receipts documentation to whatsapp.md following the
BlueBubbles docs pattern.
- Replace _load_cron_jobs_for_config_warning with lazy import of
cron.jobs.load_jobs — picks up BOM handling, corruption repair,
and context-local store resolution for free
- Re-add model.name to axis mapping (was dropped during merge);
model.name is a legacy alias for model.default
- Fix grammar: '1 enabled unpinned cron job have' -> 'has'
- Pass user_config to cron_model_drift_guard_enabled in set_config_value
to avoid a redundant load_config() re-read of the file just written
When an operator changes the global model/provider config, warn that
unpinned cron jobs with stored snapshots will fail-closed on their next
run. Adds a cron.model_drift_guard config opt-out (default true) for
fleets that should deliberately track changing global defaults.
Addresses #59031. Original PR #59177 by @doncazper.
Skip hashing active copies when the bundled origin is unchanged, build rename and optional migration indexes lazily, and reuse one optional-skill directory index per sync.
Add regression coverage for unchanged copies, deferred modification detection, lazy rename recovery, and optional provenance scans.
A session is meant to be either the main thread or a tile, never both.
openSessionTile enforced this from the tile side (refusing to tile the
selected session), but the main side never dropped an existing tile — so
any path that routes main to a session that's already tiled (cold-start
remembered-session restore, a pasted/Cmd-K route, a notification jump)
painted the same transcript twice: the workspace pane from the route and
the tile pane in parallel, both fighting one runtime.
resumeSession is the single chokepoint every load-into-main path funnels
through, so close the redundant tile there once the session becomes the
selection. The warm cache/runtime binding survives for main to reuse.
Mirror the picker dropdown's provider collapse into the Edit Models dialog so
curating is one click per provider instead of scrolling through 30 models.
Each provider header is a full-width clickable label (same style as the
composer context-menu labels) with a DisclosureCaret next to the text and a
select-all Checkbox (indeterminate when partial). Model rows stay Switches.
The dropdown's collapse is fixed too: the current provider is now
collapsible (was forced open), and the label style matches the rest of the app.
Adds a search icon to the dialog input matching every other search field.
The subpath entry existed only so the TUI could import the client-side
projection fallback. The TUI spawns its gateway from this same checkout and
cannot version-skew with it, so that fallback was dead weight — it reads
`display` directly now. With its last caller gone the dispatch helper folds
back into the desktop, where an older backend is genuinely reachable.
apps/shared/package.json is byte-identical to main again.
Pass the display type into the turn so the row is born typed, and
recognize the note's fixed prefix in _history_to_messages so the
untyped rows already on disk stop painting as user bubbles.
The auto-continue recovery note was typed only after run_conversation
returned, so its row sat untyped for the whole turn — and permanently
when the continuation was itself killed, which is the case it exists
for. persist_user_display_kind stamps the type on the live message
before the crash persist writes it, in the same insert as the content.
The flush also carries display_metadata through, which it was dropping.
Add data-[state=indeterminate] styling (same primary fill as checked) and a
dash glyph that shows when the root is in the indeterminate state, so a
partial select-all reads as a dash rather than a full check.
expandProviderDefaults prefers a provider's featured_models when present and
falls back to the existing top-N for providers that ship none (single-lab,
local, custom). Only aggregators get curated, so exactly the providers with
the everything-under-the-sun problem are trimmed; the rest are unchanged.
Every non-featured model stays one search or Edit Models toggle away.
Aggregator providers (nous, openrouter) serve dozens of models across many
labs. Add a featured=True enricher to build_models_payload that attaches a
featured_models shortlist to each row: within every lab keep the newest
_FEATURED_PER_LAB models by models.dev release_date, ranked among that row's
own models — never against the current date, so the choice is stable as
models age. Same-date ties fall back to curated list order. Single-lab
providers get an empty list. Derived live from the models.dev catalog already
loaded on this path; no hand-maintained allowlist.
#71664 asserted a leading slash never chips, correctly: a command only ever
executed, so it never reached a rendered message as text. Projecting a skill
turn back onto its invocation removes that precondition.
The bubble, the queue panel, and the queue editor all showed a queued or
sent /skill turn's expanded body. Thread the invocation through submit and
the queue entry, and chip a leading slash command the way a mid-prose one
already chips.
Carry a display string through the submit path so the transcript renders
what the user typed while the agent still receives the expanded skill.
Drops the '⚡ loading skill' line — the invocation bubble says it.
The client-side twin of the gateway's projection, shared by the desktop and
the TUI so a surface talking to an older gateway still renders the
invocation rather than the whole skill body.
A slash-skill invocation is persisted expanded — activation note plus the
entire skill body. _history_to_messages is the single display projection
every surface reads, so that payload rendered as a chat bubble anywhere a
session was resumed.
Project it here onto the invocation the user typed, and tag skill/bundle
dispatches with the same string so the live send matches. Rewind and
regenerate replay from what the transcript shows, so re-expand the
invocation server-side before running the turn: the replayed prompt is
identical to the original and no client ever holds the body.
Fresh installs now default to ~91%, but Playwright hit-testing and
visual baselines still assume 100%. Seed zoom-state.json so isolated
E2E profiles don't inherit the product default.
The composer re-asked the gateway for the command catalog on every open
and for complete.slash on every keystroke. Both scan the skills dir on
the backend, so opening the menu cost a round trip against data that
only moves when a skill is added, removed, or toggled.
Hold both behind a one-hour cache keyed per query, and invalidate it
where the skill set actually changes: hub install/uninstall/update, a
Capabilities toggle, bulk toggle, archive, the agent's own skill_manage
call, and a profile switch. A cached query also skips the debounce and
the spinner, so a warm menu opens in the same frame.
The bare-slash list showed no skills at all: the backend categorizes
registry commands but appends skills to the flat pairs list only, and
the popover prefers the categorized layout. Re-add the uncategorized
leftovers under a Skills header, so /clean and /work are visible on /
and not just after typing enough of the name to match.
A list of short rows — goal, model, timer — reads better narrow than stretched across the full reading column. Scoped in styles.css rather than as a utility: the rule above it sets width on every tool block and wins over one.
A fan-out showed up in the transcript as one grey row and a JSON blob, so the several agents it started were invisible until they finished. Give the call its own card: one line per child with the goal, the model running it, and a live timer, over a single ticking line of that child's relayed activity.