Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
143 commits
Select commit Hold shift + click to select a range
3dd8059
fix(tests): eliminate flaky/broken tests — shadow sys.path inserts, u…
teknium1 Jul 29, 2026
6b81590
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 tes…
teknium1 Jul 29, 2026
3997561
test: prune wave 2 + speed fixes — 28,106 → 19,757 test functions, su…
teknium1 Jul 29, 2026
40ec417
Merge remote-tracking branch 'origin/main' into tests/prune-low-value
teknium1 Jul 29, 2026
a17ac2c
Merge remote-tracking branch 'origin/main' into tests/prune-low-value
teknium1 Jul 29, 2026
7c5a98d
Merge remote-tracking branch 'origin/main' into tests/prune-low-value
teknium1 Jul 29, 2026
f8e07a3
feat(desktop): platforms.changed broadcast retires the Messaging page…
OutThisLife Jul 29, 2026
b3daf16
fix(installer): never let a stale --commit pin roll an install backwards
OutThisLife Jul 29, 2026
3913ac2
test: pair load_config_readonly stubs at pruned-file sites (post-merg…
teknium1 Jul 29, 2026
fe8e4d9
fix(update): make the in-progress marker a real cross-process lock
OutThisLife Jul 29, 2026
38d5f44
fix(installer): refuse a second updater instead of clobbering the marker
OutThisLife Jul 29, 2026
92856bc
Merge pull request #74383 from NousResearch/tests/prune-low-value
teknium1 Jul 29, 2026
6a444eb
fix(installer): release the update marker on success, not just on drop
OutThisLife Jul 29, 2026
774d92f
fix: approve listed pairing requests
itsflownium Jun 15, 2026
57ad222
test: cover pairing CLI approval paths
itsflownium Jul 14, 2026
c352a32
fix(pairing): reset the failed-approval counter on a successful approval
solyanviktor-star Jul 10, 2026
37d0766
fix(pairing): keep GUI approvals off the code brute-force lockout
OutThisLife Jul 29, 2026
37519b4
fix(update): use the no-kill pid probe, not os.kill(pid, 0)
OutThisLife Jul 29, 2026
800c8a0
fix(desktop): stop an unowned publisher poisoning the thread clearanc…
OutThisLife Jul 29, 2026
4a8697a
Merge pull request #74438 from NousResearch/bb/thread-clearance
OutThisLife Jul 29, 2026
7002ed9
Merge pull request #74427 from NousResearch/bb/pairing-approve-entry
OutThisLife Jul 29, 2026
4b7f709
Merge pull request #74436 from NousResearch/bb/update-mutex
OutThisLife Jul 29, 2026
fb120f8
refactor(desktop): move the main-is-occupied check into the session door
OutThisLife Jul 29, 2026
272d3be
feat(desktop): find and reuse an open blank "New session" tab
OutThisLife Jul 29, 2026
93e5413
fix(desktop): stop ⌘K and notification clicks stealing the main tab
OutThisLife Jul 29, 2026
15a55cb
feat(desktop): tab verbs follow the pane under the pointer
OutThisLife Jul 29, 2026
1a3a9de
refactor(gateway): extract run_sync onto TurnRunner (completes the Tu…
teknium1 Jul 29, 2026
011ec45
refactor(web): extract sessions/mcp/skills/tools routes to APIRouter …
teknium1 Jul 29, 2026
5010b94
Merge pull request #74445 from NousResearch/bb/cmdk-session-tab
OutThisLife Jul 29, 2026
1d95a22
Merge pull request #74447 from NousResearch/bb/hover-tab-slots
OutThisLife Jul 29, 2026
5c07ba2
Merge pull request #74422 from NousResearch/bb/platforms-changed
OutThisLife Jul 29, 2026
cd8ea6f
feat(desktop): approve pairing requests from the messaging page
OutThisLife Jul 29, 2026
d063684
style(desktop): sort the confirm-dialog import (eslint perfectionist)
OutThisLife Jul 29, 2026
4cbd465
fix(gateway): scope pairing platform discovery to the profile dir
briandevans Jul 7, 2026
75657f8
test(gateway): patch hermes_constants.get_hermes_home so profile stor…
briandevans Jul 9, 2026
a6397c3
fix(gateway): align multiplex pairing stores
wgu9 Jul 24, 2026
f174c0b
fix(pairing): scope the approve/revoke endpoints to a profile
OutThisLife Jul 29, 2026
4b33e56
refactor: config auto-migration support floor at v12 + deprecated shi…
teknium1 Jul 29, 2026
4ae5efa
fix: floor gate only refuses configs with an EXPLICIT below-floor _co…
teknium1 Jul 29, 2026
c0364fa
fix(desktop): treat landed memory writes as success, not warnings
OutThisLife Jul 29, 2026
508e73a
style(desktop): gold→purple chrome on landed memory saves
OutThisLife Jul 29, 2026
3c5da53
feat(pairing): give pairing its own change signal
OutThisLife Jul 29, 2026
4d9541b
Merge pull request #74450 from NousResearch/bb/memory-save-chip
OutThisLife Jul 29, 2026
3ca4220
fix(desktop): stop composer chips demoting to plaintext on every re-r…
OutThisLife Jul 30, 2026
08e428a
Merge pull request #74446 from NousResearch/bb/desktop-pairing
OutThisLife Jul 30, 2026
1ec5928
fix(managed_uv): repair vulnerable SQLite runtime in .venv installs too
teknium1 Jul 29, 2026
5fa03c6
fix(desktop): keep chips atomic to composer trigger detection
OutThisLife Jul 30, 2026
e4b21ef
test(desktop): cover the composer chip plaintext-demotion bug class
OutThisLife Jul 30, 2026
22ccebc
Merge pull request #74455 from NousResearch/bb/composer-chip-stability
OutThisLife Jul 30, 2026
240afd0
fix(telegram): batch near-limit command chunks so split /queue pastes…
teknium1 Jul 29, 2026
8da8a78
fix(state): time-based write-lock patience so busy sibling processes …
teknium1 Jul 30, 2026
d26983e
fix(gateway): relay TTS attachments + semantic auto-thread rename on …
benbarclay Jul 30, 2026
04ec841
fix(state): configurable journal_mode + centralize all DB openers
Jul 21, 2026
92914e9
fix(state): restore async_delegation.py symbols, keep only journal_mo…
Jul 25, 2026
91351b7
fix(state): make journal mode canonical and behaviorally verified
Jul 26, 2026
7dbf6c2
fix(gateway): add disk I/O error to WAL-fallback markers for ZFS (#55…
Sahil-SS9 Jun 30, 2026
f50d80e
fix(state): detect silent WAL→DELETE fallback on macOS NFS / SMB (no …
connorblack Jun 15, 2026
0f60cda
fix(state): escalate silent WAL→DELETE fallback to ERROR + opt-in req…
connorblack Jun 18, 2026
144e563
test(state): prove silent WAL fallback in callers
connorblack Jun 19, 2026
5567846
fix(state): retry transient disk I/O errors before WAL fallback
teknium1 Jul 30, 2026
74d6cc2
fix(config): respect database.journal_mode from config.yaml
Jul 3, 2026
75528cd
fix(state): reconcile salvaged WAL fixes with current main
teknium1 Jul 30, 2026
52b16c2
chore: map salvaged contributor emails for attribution
teknium1 Jul 30, 2026
9d4bfd5
fix(config): register WAL sizing pragmas in DEFAULT_CONFIG
teknium1 Jul 30, 2026
819357d
fix(cli): remove venv.stale.runtime marker after successful managed r…
webtecnica Jul 28, 2026
adf217b
fix(cli): sweep aged venv.stale.runtime-* backups on hermes update
teknium1 Jul 30, 2026
36f8855
chore: map webtecnica contributor email for attribution
teknium1 Jul 30, 2026
7a83f44
fix(gateway): preserve explicit send-image requests from session-wide…
webtecnica Jul 29, 2026
646761c
fix(gateway): widen explicit-MEDIA resend fix to the streaming path +…
teknium1 Jul 30, 2026
483b9e3
test(tests): assert SessionDB timeout without wall-clock
fangliquanflq Jul 22, 2026
67435c9
fix(tests): restore original module identities after vision-routing r…
teknium1 Jul 30, 2026
2c37b1a
test(approval): event-based waits for blocking-approval E2E polls (PR…
jethac Jul 30, 2026
bcec6c8
fix(test): hermetic env-detection tests — pin container/supervisor/HO…
Kyzcreig Jul 25, 2026
2472793
Refresh on upstream/main: resolve conflicts (no behavior change)
Jul 5, 2026
66c4c9c
fix(tests): forward Windows location vars through the hermetic runner…
webtecnica Jul 30, 2026
3e54a36
fix(test): patch _launch_configured_cwd in completion tests for herme…
Jul 30, 2026
05afea6
fix(session): resolve default state DB path at call time
JorkeyLiu Jul 30, 2026
74d9110
test(tui): scope close_race orphan-cleanup assert to own session key
AIalliAI Jul 30, 2026
7ba456e
fix(test): fail-closed kanban write guard prevents real HERMES_HOME p…
Jul 30, 2026
e461e86
test(cli): remove importlib.reload seam from web_server session-token…
rodboev Jul 30, 2026
8d009e4
test: guard browser and Keychain side effects suite-wide (#35404)
y0shua1ee Jul 30, 2026
bd1a850
fix(honcho): network-hermetic unit tests + lazy async writer start
Eapwrk Jul 30, 2026
aae6e52
test: opt install-ladder tests back into lazy installs under the herm…
teknium1 Jul 30, 2026
0bd82a8
chore: contributor mapping for eapwrk (honcho salvage, #67576)
teknium1 Jul 30, 2026
4aa672b
fix(desktop): stop cold-start resume homing focus to the workspace ta…
OutThisLife Jul 30, 2026
4dc9fb8
fix(gateway): exclude current-turn media from dedup
Jul 27, 2026
8f4122e
test(gateway): cover current-turn TTS media dedup
Jul 29, 2026
cea4c33
fix(gateway): collect quoted/spaced/home-relative MEDIA paths into th…
AlexxRussell Jul 30, 2026
f1120ad
chore: contributor mappings for FraserHum + AlexxRussell (#72542/#739…
teknium1 Jul 30, 2026
dd51931
Merge pull request #74526 from NousResearch/bb/tab-restore-homing
OutThisLife Jul 30, 2026
611c6aa
feat(desktop): highlight matched letters in searchable pickers
OutThisLife Jul 30, 2026
33bbf24
fix(desktop): make the composer model dropdown's search commit honestly
OutThisLife Jul 30, 2026
789d692
feat(desktop): ⌘⇧M opens the model menu on the pane under the pointer
OutThisLife Jul 30, 2026
5a23e3c
fmt(js): `npm run fix` on merge (#74544)
hermes-seaeye[bot] Jul 30, 2026
fee0eae
feat(mcp): add Comfy Cloud to the MCP catalog (remote HTTP + native O…
mattmillerai Jul 2, 2026
d9101be
fix(mcp): curate comfy-cloud default tool set + drop legacy packaging…
teknium1 Jul 17, 2026
0524ecc
chore: contributor mapping for mattmiller@comfy.org (@mattmillerai)
teknium1 Jul 30, 2026
ad12df6
Revert "remove Vercel AI Gateway and Vercel Sandbox (#33067)"
teknium1 Jul 30, 2026
c770515
modernize re-added Vercel integrations: SDK 0.7.2, telemetry off, sib…
teknium1 Jul 30, 2026
7ac6397
test: fix restored-test regressions vs current main
teknium1 Jul 30, 2026
dda2899
test(cli): fix order-dependent test_resume_quiet_stderr flake at the …
mehmetkr-31 Jul 21, 2026
ef1b06d
fix(tests): stop the suite writing into the operator's real Hermes logs
joelbrilliant Jul 25, 2026
2d40494
fix(tests): tolerate mid-import kanban_db in the write-guard sys.modu…
teknium1 Jul 30, 2026
b614521
feat(desktop): full cmdk-style keyboard selection in the model dropdown
OutThisLife Jul 30, 2026
788126e
Merge pull request #74545 from NousResearch/bb/model-picker-hotkey
OutThisLife Jul 30, 2026
3233de9
fix(tests): preserve config module identity in backup tests
frizikk Jul 28, 2026
f04fd1e
fix(update): test runs never mutate the live checkout — pytest-guard …
fcavalcantirj Jul 30, 2026
9c28600
test: prove concurrency with barriers/witnesses instead of wall-clock…
Kyzcreig Jul 30, 2026
f708a85
test(tui_gateway): make the tui gateway suite order-independent
lEWFkRAD Jul 30, 2026
b631f80
fix(tests): register no_isolate and ssh pytest markers
blut-agent Jul 30, 2026
00cd9b2
test(fal): pin fal_common behavioral contracts
Christopher-Schulze Jul 30, 2026
7216ca1
fix(tests): live-system guard treats only argv[0] as the executable
CaptureClient Jul 30, 2026
0860b98
test(tui_gateway): pin goal-command config home against collection-ti…
lEWFkRAD Jul 30, 2026
3e7a11c
fix(test-runner): native Windows venv probe + glyph-safe stdio
TheSmokeDev Jul 30, 2026
230a2c2
test: harden yolo and kanban signal tests on macOS
sunwz1115 Jul 30, 2026
9e9c220
chore(tests): drop accidental temp guard probe file
CaptureClient Jul 30, 2026
080bb83
test(homeassistant): prevent unit tests from calling live instances
jeeves-assistant Jul 30, 2026
6cf4bdd
test(gateway): fix order-dependent telegram-mock flake cluster
mehmetkr-31 Jul 30, 2026
0fe6a36
chore: gitignore the .lazy-refresh-incomplete runtime marker
teknium1 Jul 30, 2026
e7bf0ad
chore: keep LEGACY_AUTHOR_MAP frozen — mehmetkr-31 mapping lives in c…
teknium1 Jul 30, 2026
8eb06e7
fix(tests): stub _ensure_vercel_sdk in vercel sandbox tests — CI has …
teknium1 Jul 30, 2026
0ad06ae
fix(desktop): keep ⌘1-9 working when the pointer is off the panes
OutThisLife Jul 30, 2026
71a8610
Merge pull request #74601 from NousResearch/bb/hover-tab-fallback
OutThisLife Jul 30, 2026
913882c
feat(desktop): keyboard-first overlay primitives
OutThisLife Jul 30, 2026
aaf3298
fix(desktop): pickers stop eating the keyboard and the pointer
OutThisLife Jul 30, 2026
7d92056
feat(gateway): iMessage-style message reactions — storage, RPC, agent…
OutThisLife Jul 30, 2026
6ec319f
feat(desktop): two-way reaction UI — tapback pill, frimousse picker, …
OutThisLife Jul 30, 2026
a90ccd4
feat(desktop): :shortcode: emoji completions in both composers
OutThisLife Jul 30, 2026
1af8839
fix(state): make _row_id opt-in per consumer instead of universal
OutThisLife Jul 30, 2026
fec1ac0
feat(desktop): reactions are opt-in under Settings → Appearance, off …
OutThisLife Jul 30, 2026
5ecc35f
fix(tests): teach gateway-server fakes the include_row_ids kwarg
OutThisLife Jul 30, 2026
745d138
fix(deps): add frimousse + emojibase to the lockfile without pruning …
OutThisLife Jul 30, 2026
ba7d214
fmt(js): `npm run fix` on merge (#74605)
hermes-seaeye[bot] Jul 30, 2026
f790d5a
fix(desktop): stop the checkbox painting the check and dash at once
OutThisLife Jul 30, 2026
e17d058
refactor(desktop): move the composer strips out of the drag region
OutThisLife Jul 30, 2026
98e43be
Merge pull request #74602 from NousResearch/bb/pointer-intent
OutThisLife Jul 30, 2026
fa18306
Merge pull request #74533 from NousResearch/bb/emoji-reactions
OutThisLife Jul 30, 2026
eefcc09
Merge pull request #74611 from NousResearch/bb/composer-strips-outside
OutThisLife Jul 30, 2026
382282d
Merge pull request #74610 from NousResearch/bb/checkbox-indeterminate
OutThisLife Jul 30, 2026
7a9385c
fix(kanban): surface missing assignee profiles
Jul 29, 2026
2f0553e
feat(kanban): validate dispatch capabilities before claim
SSC-ENG Jul 30, 2026
554d5d3
test(kanban): preserve legacy profile fixtures
SSC-ENG Jul 30, 2026
ddeeb6f
chore: map contributor email
SSC-ENG Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
18 changes: 16 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,18 @@ jobs:
# fails if the lock is out of sync with pyproject.toml), giving a
# reproducible env. It also creates .venv itself, so no separate
# `uv venv` step is needed.
#
# The trailing extras beyond all/dev are the lazy-install features
# (tools/lazy_deps.py) that tests exercise for real: provider.anthropic,
# stt/tts.mistral, image.fal, terminal.modal, terminal.daytona,
# memory.hindsight, search.parallel. The hermetic test env forbids
# mid-run pip installs (HERMES_DISABLE_LAZY_INSTALLS=1 in
# tests/conftest.py), so the SDKs those tests need must be in the
# venv up front — resolved from uv.lock like everything else, which
# also honors the exact supply-chain pins these extras carry.
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra all --extra dev
command: uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web

- name: Minimize uv cache
# Optimized for CI: prunes pre-built wheels that are cheap to
Expand Down Expand Up @@ -216,9 +225,14 @@ jobs:
# fails if the lock is out of sync with pyproject.toml), giving a
# reproducible env. It also creates .venv itself, so no separate
# `uv venv` step is needed.
#
# Same extras as the test job's sync above: the hermetic test env
# forbids mid-run pip installs (HERMES_DISABLE_LAZY_INSTALLS=1 in
# tests/conftest.py), so lazy-install SDKs exercised by tests must be
# in the venv up front.
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.11 --extra all --extra dev
command: uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web

- name: Minimize uv cache
# Optimized for CI: prunes pre-built wheels that are cheap to
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,7 @@ infographics/
infograficos/
infografico/
native/fts5_cjk/*.so
# Runtime marker written by hermes update when a lazy dependency refresh is
# interrupted; consumed by launch-time recovery. Never commit it (was tracked
# by accident via 3a69e34702, removed in the #72002 salvage).
.lazy-refresh-incomplete
2 changes: 0 additions & 2 deletions .lazy-refresh-incomplete

This file was deleted.

146 changes: 146 additions & 0 deletions .plans/message-reactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Message reactions (desktop tapbacks)

Two-way emoji reactions on individual messages in the desktop transcript: the
user reacts to any message, the agent reacts to a user message, and both sides
read the other's reactions as conversational signal.

## What already exists

Hermes already models reactions on the **platform** side — the desktop is the
only surface without them.

| Surface | Reaction support | Where |
|---|---|---|
| Agent → platform message | `send_message(action="react"/"unreact")` | `tools/send_message_tool.py:266` `_handle_react()` |
| Photon / iMessage | tapbacks in + out, routed only for messages we sent | `plugins/platforms/photon/adapter.py:1240-1283` |
| Telegram | `setMessageReaction`, config-gated | `plugins/platforms/telegram/adapter.py:9669+` |
| Slack / Matrix / Feishu / Discord | inbound reaction events → hooks | `gateway/run.py:4688` `_handle_reaction_event()` → `HookRegistry.emit("reaction:added")` |
| Adapter contract | `add_reaction()` / `remove_reaction()` coroutines, `set_reaction_handler()` | `gateway/platforms/base.py:3330` |
| Core "affection" detector | regex on user text → `vibe`, drives CLI pet / TUI heart / desktop hearts | `agent/reactions.py`, `agent/turn_context.py:592-604` |

Two things follow from that table:

1. **The agent-facing verb already exists.** `send_message(action="react")` is
the established shape. A desktop reaction should extend that tool, not add a
new core tool — every new tool ships on every API call (AGENTS.md footprint
ladder).
2. **The inbound convention already exists.** Photon turns a tapback into a
normal message event with `reply_to_message_id` + `reply_to_is_own_message`,
and the gateway prefixes `[Replying to your previous message: "…"]`
(`gateway/run.py:13125-13132`). Desktop reactions should read the same way to
the model.

Nothing exists on the desktop side: `grep -ri reaction` across `apps/desktop`
finds only the pet-overlay hearts.

## Prior art

**iOS Tapback** ([Apple](https://support.apple.com/guide/iphone/react-with-tapbacks-iph018d3c336/ios)):
double-tap or touch-and-hold a message → floating pill above the bubble with
heart / thumbs-up / thumbs-down / haha / ‼️ / ❓, swipe left for suggested emoji
and stickers, or tap the emoji button for the full keyboard. **One tapback per
message per person** — tapping the same one again removes it, tapping a
different one replaces it. Multiple people's tapbacks stack on the badge.

**Platform data models** converge on the same shape:

| Platform | Model | Add / remove |
|---|---|---|
| Slack | `{name, count, users[]}` | [`reactions.add`](https://docs.slack.dev/reference/methods/reactions.add) / `reactions.remove`, emits `reaction_added` |
| Discord | `{emoji, count, me}` on the message object | `PUT`/`DELETE .../reactions/{emoji}/@me` |
| Telegram | `reaction: [{type:"emoji", emoji:"👍"}]` — replaces the whole set | `setMessageReaction`, `is_big` for the big animation |

Telegram's "set the whole array" is the closest match to iOS semantics and the
simplest thing to persist.

**assistant-ui has no reaction primitive.** `@assistant-ui/react` 0.14.24 (MIT,
vendored at `apps/desktop/node_modules`): zero hits for "reaction" in `core/src`,
`react/src`, `dist/`, or the 2.2 MB `llms-full.txt` docs dump. What exists is a
hard-coded binary `FeedbackAdapter` (`"positive" | "negative"`,
`core/src/adapters/feedback.ts`) that throws when unconfigured and only writes
back onto assistant messages. Not usable for emoji, not usable on user messages.

**But `metadata.custom` is the supported extension channel** and this repo
already uses it: `ThreadUserMessage`/`ThreadAssistantMessage`/`ThreadSystemMessage`
all carry `metadata.custom: Record<string, unknown>` (`core/src/types/message.ts:319-366`),
and `chat-runtime.ts:397` already ships `custom: { attachmentRefs }` through it.

**Emoji picker survey** (npm week of 2026-07-22, sizes measured from the
published ESM entry):

| Library | License | Weekly DL | gzip | Headless | Latest |
|---|---|---|---|---|---|
| **frimousse** | MIT | 573k | **8.5 kB** | ✅ fully unstyled, composable parts | 0.3.0 · 2025-07-15 |
| emoji-picker-react | MIT | 1.31M | 87 kB | ❌ own CSS-in-JS (flairup) | 4.19.1 · 2026-04-27 |
| emoji-mart | MIT | 2.22M | ~120 kB w/ data | ❌ Preact + shadow styling | 5.6.0 · **2024-04-25**, 217 open issues |
| emoji-picker-element | Apache-2.0 | 183k | — | ❌ Web Component / Shadow DOM | 1.29.1 · 2026-03-01 |

No picker is currently a dependency (only `emoji-regex`, transitive). Already
paid for and reusable: `radix-ui` (Popover), `motion`, `@tanstack/react-virtual`,
Tailwind v4.

## Recommendation

**Hand-roll the tapback pill; add frimousse only behind the "+".** Six fixed
emoji in a pill is ~40 lines of JSX against existing tokens — pulling 87 kB of
`emoji-picker-react` to render six buttons, plus a CSS engine that fights
`DESIGN.md`, is backwards. frimousse is headless, dependency-free, 10× smaller,
and exposes `emojibaseUrl` so the data can be bundled as a Vite asset instead of
hitting jsDelivr (Electron must work offline).

### Data model

One reaction per author per message, Telegram-style whole-set replacement:

```ts
type MessageReaction = { emoji: string; author: 'user' | 'agent'; at: number }
```

Persisted in the existing `messages.display_metadata` JSON column
(`hermes_state_common.py:215`) — no new table. It already survives insert,
compaction, and every read projection, and
`set_latest_matching_message_display_kind()` (`hermes_state.py:5292`) is the
precedent for stamping metadata onto an already-persisted row.

### Model context

Reactions must reach the model **without breaking prompt caching**. The
`api_messages` build loop strips `display_metadata` from every outgoing copy
(`agent/conversation_loop.py:1443-1446`) precisely so display state never
becomes a provider field. Two candidate paths:

| Path | Cache impact | Notes |
|---|---|---|
| Rewrite the reacted-to message's content to carry the annotation | **Breaks the cached prefix** — mutates past context | Rejected. AGENTS.md: prompt caching is sacred. |
| Deliver the reaction as the *next* turn's leading annotation, mirroring photon | Prefix untouched; only the new turn carries it | Matches `[Replying to your previous message: "…"]` (`gateway/run.py:13125`), which the agent already understands |

The second is the same trick the platform adapters already use, so the model
sees a familiar shape and no existing conversation is rewritten.

### Attach points

| Concern | File | Lines |
|---|---|---|
| Assistant hover bar | `apps/desktop/src/components/assistant-ui/thread/assistant-message.tsx` | 134–175 |
| User hover cluster | `apps/desktop/src/components/assistant-ui/thread/user-message.tsx` | 296–336 |
| Callback threading (ref caveat 79–99) | `apps/desktop/src/components/assistant-ui/thread/index.tsx` | 109–133 |
| `metadata.custom` → runtime | `apps/desktop/src/lib/chat-runtime.ts` | 384–432 |
| RPC client ↔ server pattern | `sidebar/session-actions-menu.tsx:62-89` ↔ `tui_gateway/server.py:8322` | — |
| Persistence | `hermes_state_common.py:192-216`, `hermes_state.py:5292-5324` | — |
| Prompt injection / strip | `agent/conversation_loop.py` | 1430–1529 |

### Known gaps to solve first

- **No durable message id crosses the gateway RPC path.** `_history_to_messages()`
(`tui_gateway/server.py:6545`) builds `{"role", "text"}` and drops the id. The
REST path carries `messages.id` incidentally via `SELECT *` but TS
`SessionMessage` (`types/hermes.ts:513-533`) doesn't declare it. Renderer ids
are ephemeral and change shape between rehydrated (`<ts>-<i>-<role>`), live
(`assistant-<ms>`), and optimistic (`user-<ms>-<rand>`) messages. A reaction
needs a stable key — this is the first thing to fix.
- **WeakMap identity cache** in `apps/desktop/src/app/chat/runtime-repository.ts:26-66`
keys normalized `ThreadMessage` by `ChatMessage` identity. A reaction change
must produce a **new** `ChatMessage` object or the UI renders stale.
- **Rewind rewrites rows** (`replace_messages`), so anything keyed by row id
needs cascade handling — an argument for keeping reactions in
`display_metadata` on the row itself rather than a side table.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Use any model you want — [Nous Portal](https://portal.nousresearch.com), OpenR
<tr><td><b>A closed learning loop</b></td><td>Agent-curated memory with periodic nudges. Autonomous skill creation after complex tasks. Skills self-improve during use. FTS5 session search with LLM summarization for cross-session recall. <a href="https://github.com/plastic-labs/honcho">Honcho</a> dialectic user modeling. Compatible with the <a href="https://agentskills.io">agentskills.io</a> open standard.</td></tr>
<tr><td><b>Scheduled automations</b></td><td>Built-in cron scheduler with delivery to any platform. Daily reports, nightly backups, weekly audits — all in natural language, running unattended.</td></tr>
<tr><td><b>Delegates and parallelizes</b></td><td>Spawn isolated subagents for parallel workstreams. Write Python scripts that call tools via RPC, collapsing multi-step pipelines into zero-context-cost turns.</td></tr>
<tr><td><b>Runs anywhere, not just your laptop</b></td><td>Six terminal backends — local, Docker, SSH, Singularity, Modal, and Daytona. Daytona and Modal offer serverless persistence — your agent's environment hibernates when idle and wakes on demand, costing nearly nothing between sessions. Run it on a $5 VPS or a GPU cluster.</td></tr>
<tr><td><b>Runs anywhere, not just your laptop</b></td><td>Seven terminal backends — local, Docker, SSH, Singularity, Modal, Daytona, and Vercel Sandbox. Daytona and Modal offer serverless persistence — your agent's environment hibernates when idle and wakes on demand, costing nearly nothing between sessions. Run it on a $5 VPS or a GPU cluster.</td></tr>
<tr><td><b>Research-ready</b></td><td>Batch trajectory generation, trajectory compression for training the next generation of tool-calling models.</td></tr>
</table>

Expand Down
4 changes: 2 additions & 2 deletions agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1161,8 +1161,8 @@ def init_agent(
client_kwargs["default_headers"] = hermes_xai_default_headers()
elif "default_headers" not in client_kwargs:
# Fall back to profile.default_headers for providers that
# declare custom headers (e.g. Kimi User-Agent on non-kimi.com
# endpoints).
# declare custom headers (e.g. Vercel AI Gateway attribution,
# Kimi User-Agent on non-kimi.com endpoints).
try:
from providers import get_provider_profile as _gpf
_ph = _gpf(agent.provider)
Expand Down
13 changes: 12 additions & 1 deletion agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ def _get_aux_model_for_provider(provider_id: str) -> str:
"kimi-coding-cn": "kimi-k2-turbo-preview",
"gmi": "google/gemini-3.1-flash-lite-preview",
"anthropic": "claude-haiku-4-5-20251001",
"ai-gateway": "google/gemini-3-flash",
"opencode-zen": "gemini-3-flash",
"opencode-go": "glm-5",
"kilocode": "google/gemini-3.6-flash",
Expand Down Expand Up @@ -729,6 +730,15 @@ def build_nvidia_nim_headers(base_url: str | None) -> dict:
return {}


# Vercel AI Gateway app attribution headers. HTTP-Referer maps to
# referrerUrl and X-Title maps to appName in the gateway's analytics.
from hermes_cli import __version__ as _HERMES_VERSION

_AI_GATEWAY_HEADERS = {
"HTTP-Referer": "https://hermes-agent.nousresearch.com",
"X-Title": "Hermes Agent",
"User-Agent": f"HermesAgent/{_HERMES_VERSION}",
}

# Nous Portal extra_body for product attribution.
# Callers should pass this as extra_body in chat.completions.create()
Expand Down Expand Up @@ -5725,7 +5735,8 @@ def _wrap_if_needed(client_obj, final_model_str: str, base_url_str: str = "",
else:
# Fall back to profile.default_headers for providers that declare
# client-level attribution headers on their profile (e.g. GMI
# User-Agent for traffic identification).
# User-Agent for traffic identification, Vercel AI Gateway
# Referer/Title for analytics).
try:
from providers import get_provider_profile as _gpf_main
_ph_main = _gpf_main(provider)
Expand Down
6 changes: 6 additions & 0 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,12 @@ def run_conversation(
api_msg.pop("display_kind", None)
api_msg.pop("display_metadata", None)

# Durable row identity stamped by _rows_to_conversation so the
# desktop can address a specific persisted message (reactions).
# Bookkeeping, never a provider field — only the chat-completions
# transport strips underscore keys, so drop it centrally here.
api_msg.pop("_row_id", None)

# Inject ephemeral context into the current turn's user message.
# Sources: memory manager prefetch + plugin pre_llm_call hooks
# with target="user_message" (the default). Both are
Expand Down
4 changes: 2 additions & 2 deletions agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def _resolve_requests_verify() -> bool | str:
_PROVIDER_PREFIXES: frozenset[str] = frozenset({
"openrouter", "nous", "openai-codex", "copilot", "copilot-acp",
"gemini", "ollama-cloud", "zai", "kimi-coding", "kimi-coding-cn", "stepfun", "minimax", "minimax-oauth", "minimax-cn", "anthropic", "deepseek", "deepinfra",
"opencode-zen", "opencode-go", "kilocode", "alibaba", "novita",
"opencode-zen", "opencode-go", "ai-gateway", "kilocode", "alibaba", "novita",
"qwen-oauth",
"xiaomi",
"arcee",
Expand All @@ -84,7 +84,7 @@ def _resolve_requests_verify() -> bool | str:
"glm", "z-ai", "z.ai", "zhipu", "github", "github-copilot",
"github-models", "kimi", "moonshot", "kimi-cn", "moonshot-cn", "claude", "deep-seek", "deep-infra",
"ollama",
"stepfun", "opencode", "zen", "go", "kilo", "dashscope", "aliyun", "qwen",
"stepfun", "opencode", "zen", "go", "vercel", "kilo", "dashscope", "aliyun", "qwen",
"mimo", "xiaomi-mimo",
"tencent", "tokenhub", "tencent-cloud", "tencentmaas",
"arcee-ai", "arceeai",
Expand Down
1 change: 1 addition & 0 deletions agent/models_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class ProviderInfo:
"alibaba": "alibaba",
"qwen-oauth": "alibaba",
"copilot": "github-copilot",
"ai-gateway": "vercel",
"opencode-zen": "opencode",
"opencode-go": "opencode-go",
"kilocode": "kilo",
Expand Down
7 changes: 4 additions & 3 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,7 @@ def format_steer_marker(steer_text: str) -> str:
# misleading — the agent should only see the machine it can actually touch.
_REMOTE_TERMINAL_BACKENDS = frozenset({
"docker", "singularity", "modal", "daytona", "ssh",
"managed_modal",
"vercel_sandbox", "managed_modal",
})


Expand All @@ -995,6 +995,7 @@ def format_steer_marker(steer_text: str) -> str:
"modal": "a Modal sandbox (Linux)",
"managed_modal": "a managed Modal sandbox (Linux)",
"daytona": "a Daytona workspace (Linux)",
"vercel_sandbox": "a Vercel sandbox (Linux)",
"ssh": "a remote host reached over SSH (likely Linux)",
}

Expand Down Expand Up @@ -1069,7 +1070,7 @@ def _probe_remote_backend(env_type: str) -> str | None:
}

container_config = None
if env_type in {"docker", "singularity", "modal", "daytona"}:
if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}:
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
Expand Down Expand Up @@ -1159,7 +1160,7 @@ def build_environment_hints() -> str:
and a Windows-only note that `terminal` shells out to bash, not
PowerShell).
- For **remote / sandbox** terminal backends (docker, singularity,
modal, daytona, ssh): host info is **suppressed**
modal, daytona, ssh, vercel_sandbox): host info is **suppressed**
because the agent's tools can't touch the host — only the backend
matters. A live probe inside the backend reports its OS, user, $HOME,
and cwd. Falls back to a static summary if the probe fails.
Expand Down
4 changes: 2 additions & 2 deletions agent/usage_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1244,8 +1244,8 @@ def normalize_usage(
output_tokens = _to_int(getattr(response_usage, "completion_tokens", 0))
details = getattr(response_usage, "prompt_tokens_details", None)
# Primary: OpenAI-style prompt_tokens_details. Fallback: Anthropic-style
# top-level fields that some OpenAI-compatible proxies (OpenRouter, Cline)
# expose when routing Claude models — without this
# top-level fields that some OpenAI-compatible proxies (OpenRouter, Vercel
# AI Gateway, Cline) expose when routing Claude models — without this
# fallback, cache writes are undercounted as 0 and cache reads can be
# missed when the proxy only surfaces them at the top level.
# Port of cline/cline#10266.
Expand Down
4 changes: 4 additions & 0 deletions apps/bootstrap-installer/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ windows-sys = { version = "0.59", features = [
"Win32_UI_WindowsAndMessaging",
] }

# Signal-0 liveness probe for the update-lock marker owner (update.rs).
[target.'cfg(unix)'.dependencies]
libc = "0.2"

[profile.release]
# A 5-10MB signed installer is the goal. LTO + size-opt + single codegen unit.
panic = "abort"
Expand Down
Loading
Loading