Commit Graph

192 Commits

Author SHA1 Message Date
teknium1
a3cce7b974 fix(gateway): warn when a live gateway's heartbeat goes stale instead of printing running
The reporter's case in #113372 is "not a crash": the process stays alive,
`gateway_state.json` keeps saying `running`, and housekeeping, cron and the
kanban dispatcher are frozen. Nothing wrote `updated_at` periodically, so the
file was a stored status, not a heartbeat, and both `hermes gateway status`
and `/api/status` rendered a wedged gateway as healthy (the existing stale arm
only fires when the recorded PID is gone).

Make the housekeeping tick re-stamp `gateway_state.json` first thing every
tick (60 s), so `updated_at` is a heartbeat that stops when the thread — or a
chore blocked on the loop — wedges. Readers then warn on `running`/`starting`
+ stale stamp + live PID: `hermes gateway status` prints
`⚠ Gateway heartbeat stale: housekeeping has not refreshed gateway_state.json
for N s (event loop or housekeeping wedged; pid X alive)`, `/api/status`
carries `gateway_heartbeat_stale_s` (null when healthy) and the sidebar strip
shows "Heartbeat stale". Liveness (`gateway_running`, busy/drainable) still
keys off the PID, never the stamp. Draining is excluded: shutdown drains the
housekeeping thread before the process exits, so its stamp legitimately ages.

Invariant tests: the CLI line names the age and the live PID and is silent on
a fresh stamp; /api/status sets/clears `gateway_heartbeat_stale_s` the same way.
2026-09-18 09:25:59 -07:00
teknium1
993d1b9891 fix(status): dashboard keeps a watchdog-exited gateway degraded with its reason
`/api/status` mapped a not-running gateway's retained record through
`retained_gateway_state`, which only kept `startup_failed`; a watchdog-stamped
`degraded` + `exit_reason` of a dead PID became a bare `stopped` with the
reason nulled, so the sidebar strip and System page read "Stopped" while
`hermes gateway status` said "exited degraded: event loop stopped dispatching".

Retain `degraded` under the same rule as `startup_failed` (only while
`desired_state` still wants the gateway running, and only for the watchdog
reasons in the new `gateway.status.WATCHDOG_EXIT_REASONS`); the resolver
already keeps `gateway_exit_reason` for any non-`stopped` verdict. The sidebar
strip gains the `degraded` label (warning while live, destructive when the
process is gone) and the System page describes a dead `degraded` record as a
watchdog exit. `/api/messaging/platforms` keeps yielding `gateway_stopped` for
it — the channels really are down.

Invariant tests: retained_gateway_state keeps/drops the verdict by
desired_state and exit_reason; /api/status carries degraded + exit_reason for
the dead PID and stopped/null after `hermes gateway stop`.
2026-09-18 09:25:59 -07:00
teknium1
115706aa53 fix(web): release xterm WebGL contexts on reconnect instead of dropping the renderer
Follow-up to the salvaged #111918. That commit fixed the WebGL context
pile-up by removing the WebGL renderer altogether. With @xterm/xterm 6 the
fallback is the DOM renderer, so wide layouts would have lost the crisp,
fast rendering the renderer split in 63975aa deliberately gave them, and
the salvaged comment ("default canvas renderer") described a renderer that
no longer exists.

The leak itself is one missing call: @xterm/addon-webgl 0.19 removes its
canvas on dispose but never calls WEBGL_lose_context.loseContext(), so
every PTY reconnect (which rebuilds the Terminal) leaves a live GL context
until GC. Browsers cap live contexts at ~16 and force-lose the oldest, so a
reconnect storm eventually blanks the terminal the user is looking at.
`loseWebglContexts(host)` runs in the terminal effect's cleanup before
`term.dispose()` and releases every WebGL context under the host; WebGL
stays on for wide layouts exactly as before.

The keepalive is no longer gated on tab visibility / chat activity: an open
PTY socket on a hidden or backgrounded tab still owns its PTY and is the one
most likely to sit idle through a proxy timeout, and the frame is ~20 bytes.
That removes the `shouldSendPtyKeepalive` helper and its two tests; the
socket-open check already lives in `sendTerminalResize`. The ChatPage test
drops its "no WebglAddon constructed" assertion, which passed on base too
(jsdom hosts are 0px wide, so the wide-layout WebGL branch never ran).
2026-09-15 18:38:16 -07:00
KoNit-K
b23c4fad36 fix(web): keep chat PTY connections alive 2026-09-15 18:38:16 -07:00
teknium1
c30b3c1a2c refactor(web): drop the dead shouldPinScroll helper and trim the reveal tests to two invariants
ChatPage no longer pins the page to (0, 0); keyboardRevealScrollDelta is the only caller-facing
helper left in keyboard-inset.ts. Update the isActive-effect comment that still described the pin.
2026-09-15 06:32:12 -07:00
edrethardo
e72813393c fix(web): jump the iOS viewport to the chat composer when the keyboard opens
Pinning the dashboard to (0, 0) on keyboard inset fights Safari's visual
viewport and leaves the Ink input line off-screen. Scroll by the delta
between the xterm host bottom and visualViewport bottom instead, and
re-sync after focus while the keyboard animates.

Fixes #110414
2026-09-15 06:32:12 -07:00
teknium1
bdc7916196 fix(ux): plain-language, actionable user-facing messages (dashboard)
Squashed integration of the user-facing message audit for this surface set.
Full per-finding receipts: /tmp/ux-audit/lanes/*-receipt.md (campaign artifacts).
2026-09-15 03:36:22 -07:00
teknium1
c446c45f1d fix(web): dashboard imports the generated ModelOptionsResult (Docker + nix builds run tsc -b, which covers files the -p check skipped) 2026-09-14 06:12:19 -07:00
teknium1
057c2c85fc refactor(slash): delete the dead web slash re-implementation; one slash parser + command.dispatch narrowing in @hermes/shared
web/src/lib/slashExec.ts and web/src/components/SlashPopover.tsx had zero
importers since the React composer was replaced by the PTY-embedded TUI
(f49afd3122) — exactly what web/AGENTS.md forbids, now orphaned. Their
parseSlash still carried the `(.*)` newline bug and lacked the `prefill`
variant. Desktop and the TUI each hand-rolled the same slash split and the
same command.dispatch narrowing; the multi-line fix (#41323, #55510) had to
be applied to each copy separately.

Sites:
  web/src/lib/slashExec.ts::executeSlash/parseSlash/parseCommandDispatch  -> deleted
  web/src/components/SlashPopover.tsx::SlashPopover                        -> deleted
  apps/desktop/src/lib/chat-runtime.ts::parseSlashCommand                  -> apps/shared/src/slash.ts::parseSlashCommand
  apps/desktop/src/lib/chat-runtime.ts::parseCommandDispatch               -> apps/shared/src/slash.ts::parseCommandDispatch
  apps/desktop/src/lib/chat-runtime.ts::SLASH_COMMAND_RE                   -> apps/shared/src/slash.ts::SLASH_COMMAND_RE
  apps/desktop/src/app/types.ts::*CommandDispatchResponse (5 interfaces)   -> apps/shared/src/slash.ts
  ui-tui/src/domain/slash.ts::parseSlashCommand/looksLikeSlashCommand      -> apps/shared/src/slash.ts
  ui-tui/src/lib/rpc.ts::asCommandDispatch                                 -> apps/shared/src/slash.ts::parseCommandDispatch
  ui-tui/src/gatewayTypes.ts::CommandDispatchResponse                      -> apps/shared/src/slash.ts
  9 desktop importers + 3 TUI importers repointed.

Behavior change: desktop `parseSlashCommand` now lower-cases the command
name like the TUI, backend `resolve_command` and `slash.exec` already do
(`/Help` resolved before via the case-insensitive backend; local desktop
action lookups were case-sensitive). TUI's parsed result no longer carries
the redundant `cmd` echo (no consumer read it).

Tests: apps/shared/src/slash.test.ts (parseSlashCommand multi-line /
newline-boundary / degenerate cases; parseCommandDispatch every variant +
malformed rejection). Sabotage: restoring `(.*)` in SLASH_PARTS_RE fails
2 tests; restored -> 7 pass. Desktop chat-runtime.test.ts and TUI
asCommandDispatch.test.ts cases moved here; slashParity.test.ts repointed.
2026-09-13 06:50:57 -07:00
teknium1
172b2a722b refactor(ts): one compactNumber and one reasoning-effort value set in @hermes/shared
Three hand-rolled compact-number formatters and two mirrored copies of the
reasoning-effort value set collapse into apps/shared/src/format.ts and
apps/shared/src/reasoning-effort.ts, exported from the package root and as
the subpaths `@hermes/shared/format` / `@hermes/shared/reasoning-effort`
(the TUI compiles with lib ES2023 and imports subpaths only). Surfaces keep
their own label maps and UI helpers. No re-export shims remain.

Convention for compactNumber (desktop's implementation, moved verbatim):
lowercase 'k', uppercase 'M', promotion-guarded thresholds (>= 999.5 -> k,
>= 999_950 -> M) so rounding can never print "1000k", trailing ".0"
stripped, non-finite / <= 0 -> "0".

Sites (path::symbol -> canonical):

  apps/desktop/src/lib/format.ts::compactNumber          -> apps/shared/src/format.ts::compactNumber (moved; file deleted)
  web/src/lib/format.ts::formatTokenCount                -> deleted
  ui-tui/src/lib/text.ts::fmtK                           -> deleted (text.ts's own callers use compactNumber)
  apps/desktop/src/app/agents/index.tsx                  -> @hermes/shared
  apps/desktop/src/app/chat/sidebar/chrome.tsx           -> @hermes/shared
  apps/desktop/src/app/chat/sidebar/session-row.tsx      -> @hermes/shared
  apps/desktop/src/app/command-center/index.tsx          -> @hermes/shared
  apps/desktop/src/app/shell/context-usage-panel.tsx     -> @hermes/shared
  apps/desktop/src/app/shell/titlebar-controls.tsx       -> @hermes/shared
  apps/desktop/src/app/skills/index.tsx                  -> @hermes/shared
  apps/desktop/src/app/skills/mcp-tab.tsx                -> @hermes/shared
  apps/desktop/src/components/ui/tab-dropdown.tsx        -> @hermes/shared
  apps/desktop/src/lib/statusbar.tsx                     -> @hermes/shared
  apps/desktop/src/sdk/index.ts::compactNumber           -> re-exported from @hermes/shared (plugin SDK surface unchanged)
  apps/desktop/src/plugins/kanban/{board,drawer}.tsx     -> unchanged (import via @hermes/plugin-sdk)
  web/src/components/ModelInfoCard.tsx::formatTokenCount -> @hermes/shared::compactNumber
  web/src/pages/ModelsPage.tsx::formatTokenCount         -> @hermes/shared::compactNumber
  ui-tui/src/components/appChrome.tsx::fmtK              -> @hermes/shared/format::compactNumber
  ui-tui/src/components/thinking.tsx::fmtK               -> @hermes/shared/format::compactNumber
  ui-tui/src/app/slash/commands/session.ts::fmtK         -> @hermes/shared/format::compactNumber
  ui-tui/src/__tests__/text.test.ts::fmtK suite          -> apps/shared/src/format.test.ts (table incl. promotion guard)

  apps/desktop/src/lib/reasoning-effort.ts::REASONING_EFFORTS/REASONING_EFFORT_VALUES/
      DEFAULT_REASONING_EFFORT/ReasoningEffort/isReasoningEffort  -> apps/shared/src/reasoning-effort.ts
      (SHORT_LABELS, reasoningEffortLabel, isThinkingEnabled, resolveReasoningEffort stay local)
  apps/desktop/src/app/settings/constants.ts             -> @hermes/shared
  apps/desktop/src/app/settings/model-settings.tsx       -> @hermes/shared
  apps/desktop/src/app/shell/model-catalog-menu.tsx      -> @hermes/shared (+ local reasoningEffortLabel)
  apps/desktop/src/app/shell/model-edit-submenu.tsx      -> @hermes/shared (+ local UI helpers)
  apps/desktop/src/app/shell/model-menu-panel.tsx        -> @hermes/shared
  apps/desktop/src/lib/model-status-label.ts             -> @hermes/shared (+ local reasoningEffortLabel)
  apps/desktop/src/sdk/index.ts                          -> value set re-exported from @hermes/shared; label helper stays from '@/lib/reasoning-effort'
  apps/desktop/src/lib/reasoning-effort.test.ts          -> value-set + isReasoningEffort cases moved to apps/shared/src/reasoning-effort.test.ts
  web/src/lib/reasoning-effort.ts::EFFORT_OPTIONS        -> labels mapped over shared REASONING_EFFORT_VALUES (same order: none, then 7 levels)
  web/src/lib/reasoning-effort.ts::VALID_EFFORTS         -> Set(REASONING_EFFORT_VALUES); normalizeEffort falls back to DEFAULT_REASONING_EFFORT

Semantics kept: web `none` is selectable; desktop `none` resolves to ''
(thinking off); desktop isReasoningEffort still trims + lowercases.

Behavior change:
  - web: token counts on the Models page and ModelInfoCard now print a
    lowercase 'k' and are promotion-guarded: 128_000 "128K" -> "128k",
    999_999 "1000.0K" -> "1M", 1_500 "1.5K" -> "1.5k". 'M' is unchanged.
  - TUI: fmtK used Intl compact notation; compactNumber differs only in
    suffix case and the guard: 1_000_000 "1m" -> "1M", and billions no
    longer get a 'b' suffix (1_000_000_000 "1b" -> "1000M"). Sub-million
    values are identical ("999", "1k", "1.5k"). Non-positive values now
    print "0" instead of "-1k".
  - desktop: none (its formatter moved verbatim).

Tests: apps/shared/src/format.test.ts::"compactNumber" (table incl.
999_999 -> "1M", 999_949 -> "999.9k"; fails when the promotion guard is
removed) and apps/shared/src/reasoning-effort.test.ts::"reasoning-effort"
(no duplicate values, `none` is the only non-level, default is a member;
fails on a duplicated level or a `none`-accepting isReasoningEffort).
2026-09-13 06:50:57 -07:00
teknium1
a2ae8f229d refactor(ts): one fuzzy + model-search-text helper in @hermes/shared; desktop picker ranks with fuzzyRank
Three byte-identical (modulo prettier and a "keep in sync" header comment)
copies of model-search-text.ts and two of fuzzy.ts collapse into one copy
each under apps/shared/src, exported from the package root and as the
subpaths `@hermes/shared/fuzzy` / `@hermes/shared/model-search-text` (the
TUI compiles with lib ES2023 and imports subpaths, never the DOM-typed
root). The vitest suites move with the code; no re-export shims remain.

Sites (path::symbol -> canonical):

  ui-tui/src/lib/fuzzy.ts::fuzzyScore/fuzzyScoreMulti/fuzzyRank   -> apps/shared/src/fuzzy.ts (moved)
  web/src/lib/fuzzy.ts::fuzzyScore/fuzzyScoreMulti/fuzzyRank      -> deleted
  ui-tui/src/lib/model-search-text.ts::modelSearchText            -> apps/shared/src/model-search-text.ts (moved)
  web/src/lib/model-search-text.ts::modelSearchText               -> deleted
  apps/desktop/src/lib/model-search-text.ts::modelSearchText      -> deleted
  ui-tui/src/lib/fuzzy.test.ts                                    -> apps/shared/src/fuzzy.test.ts (moved)
  ui-tui/src/lib/model-search-text.test.ts                        -> apps/shared/src/model-search-text.test.ts (moved)
  ui-tui/src/components/modelPicker.tsx::fuzzyRank, modelSearchText     -> @hermes/shared/fuzzy, @hermes/shared/model-search-text
  web/src/components/ModelPickerDialog.tsx::fuzzyRank, modelSearchText  -> @hermes/shared
  web/src/lib/model-picker-filter.ts::fuzzyScoreMulti                   -> @hermes/shared
  apps/desktop/src/components/model-picker.tsx::modelSearchText         -> @hermes/shared (+ fuzzyRank, see below)

The header comment now names only the cross-language twin
(hermes_cli/model_search.py) as the thing to keep in sync.

Behavior change (desktop only): the desktop model picker used to filter
model rows with `foldIncludes` substring matching and keep the curated
order; it now ranks them with the same `fuzzyRank(models, query,
modelSearchText)` the web and TUI pickers use. What a user sees
differently while typing a query:

  - subsequence queries match: "g4o" now finds "gpt-4o" (previously only
    a literal substring such as "gpt-4" or "4o" matched);
  - the best match floats to the top instead of rows staying in curated
    order (exact > prefix > word-boundary > contiguous > scattered);
  - a query that matches the provider name/slug still shows that
    provider's full curated list in order, exactly as before;
  - an empty query still shows the curated list verbatim.

The in-row highlight is unchanged (substring emphasis via HighlightMatches),
so a fuzzy-only hit renders without emphasis rather than mis-highlighting.

Tests: apps/desktop/src/components/model-picker.test.tsx::"orders model
rows exactly as the shared fuzzyRank does" asserts the rendered row order
equals the shared fuzzyRank order for the same inputs (fails on both the
old substring filter and a reversed ranking).
2026-09-13 06:50:57 -07:00
teknium1
bab5cece78 refactor(ts): one reconnect backoff in apps/shared; web events feed rides the shared client and survives reconnects
Four backoff formulas (ui-tui 1000/30s, desktop 300/15s jittered, web events
1000/30s, web PTY inline 250/3s cap 5 — untested) collapse into
apps/shared/src/reconnect-backoff.ts::reconnectBackoffDelayMs(attempt,
{baseDelayMs, capMs, jitter}). Every caller keeps its own parameters
(table in the PR body); the PTY ladder gains a test.

web/src/components/ChatSidebar.tsx hand-rolled a third WebSocket frame
dispatcher (`new WebSocket` + JSON.parse + `frame.method === "event"` switch
+ a private RpcEnvelope re-declaring shared JsonRpcFrame) for /api/events.
That socket now goes through EventsFeedClient, a notification-only subclass
of the shared JsonRpcGatewayClient (replay off, heartbeat off, connect
timeout covering ticket minting); the effect keeps only the retry ladder and
the banner. Both sidebar clients are now created once per component instead
of per `version` bump, so the shared client's seq watermarks survive a drop
and its `session.events.since` gap replay can actually fire for web
(previously the client was rebuilt on every reconnect and replay never ran).

Behavior change: web sidecar reconnects reuse the same JsonRpcGatewayClient
(gap replay now runs); the events feed's handshake `error`+`close` pair is one
`closed` transition (one retry timer, as before); no parameter of any
backoff ladder changed.
2026-09-13 05:42:31 -07:00
teknium1
36773e0d78 refactor(ts): one GatewayEventMap in apps/shared typed from tui_gateway emitters; drop never-emitted tool.progress
Three TypeScript clients each declared their own copy of the tui_gateway wire
types and had drifted apart: apps/shared had a partial GatewayEventName union
with a `(string & {})` escape hatch, ui-tui/gatewayTypes.ts a 150-line
discriminated union, and apps/desktop an `RpcEvent<T>` that was field-for-field
the shared GatewayEvent with `type: string`. None matched the emitter:
message.complete lacked warning/status/error/recoverable/error_surface,
tool.start/tool.complete lacked args/result, SessionResumeResponse lacked
session_key/messages_omitted/hydrating/auto_continue/todo_state, three
different ModelOptionProvider shapes disagreed on fields, and all three unions
handled a `tool.progress` event that no Python emitter has ever produced.

Now:

* `apps/shared/src/gateway-events.ts` is the single home: payload interfaces
  typed from the Python emitters (file::symbol cited per interface),
  `BackendGatewayEventMap` (89 backend names) + `ClientLocalGatewayEventMap`
  (5 TUI-synthetic transport events, clearly marked, excluded from the
  contract) merged into `GatewayEventMap`; `GatewayEvent<K>` is discriminated
  on `type` with `seq` typed. RPC shapes shared by 2+ surfaces live beside it
  (ModelOptionProvider = union of every field hermes_cli/inventory.py sets,
  incl. pricing_pending/free_tier_pending; SessionResumeResponse<Info>;
  SessionListItem with resolved_id; Usage).
* `JsonRpcGatewayClient.on<K>` is keyed by event name; the gateway.ready
  heartbeat/replay_epoch and per-frame `seq` reads are typed instead of cast.
* ui-tui and apps/desktop import the shared names; their local duplicates are
  deleted (no re-export shims — importers are repointed; the desktop plugin
  SDK barrel keeps its public `RpcEvent` name as an alias of GatewayEvent).
  web/src repoints ModelOptionProvider/ModelOptionsResponse.
* `tool.progress` handling is removed from the TUI handler/turnController,
  desktop event sets/tools handler, shared union, tests, and two docs
  (`grep '"tool.progress"' tui_gateway/` = 0 hits; the `display.tool_progress`
  config mode is unrelated and untouched).
* `message.complete.warning` (history-commit note from
  prompt_turn.py::_complete_turn_payload) is typed and surfaced on both
  surfaces through their existing notice paths (TUI pushActivity 'warn',
  desktop notify kind 'warning').

Contract: `apps/shared/src/gateway-events.json` is the sorted list of
backend-emitted names. `tests/tui_gateway/test_gateway_event_contract.py`
collects names from the Python emitter side (emit-helper literals, the
`.request → .expire` table, change-watcher table, child delta mirror,
subagent relay, desktop_ui tool emitters, gateway.ready/setup.ready/
browser-controller frames) and asserts emitted == JSON in both directions.
`apps/shared/src/gateway-events.test.ts` asserts BACKEND_EVENT_NAMES (which
the map type is `satisfies`-checked against) == JSON. Sabotage-verified: a
fake JSON name fails both tests; a fake TS name fails tsc + vitest; a fake
Python `_emit("...")` fails pytest.
2026-09-13 05:42:31 -07:00
teknium1
6a66a5d481 fix(desktop,dashboard): served profile's api_server/webhook read connected with their /p/<profile>/ URL; shared-gateway restart asks first
Under gateway.multiplex_profiles a secondary's api_server and webhook are never built as
adapters (run_adapters skips SHARED_LISTENER_MIRROR_PLATFORMS: the default's listener answers
/p/<profile>/...). The multiplexer record therefore has no `<profile>:api_server` entry,
profile_platforms_from_multiplexer() returned {} for them and both /api/messaging/platforms
and /api/status?profile= fell through to `pending_restart`: the Desktop Messaging card and
Command Center said "Restart needed" forever for a platform that was answering.

- gateway.status.shared_listener_mirror_platforms projects the default's LIVE api_server /
  webhook entry onto every served secondary with `ingress_url` = `<listener>/p/<profile>/v1`
  (`.../webhooks/<route>`); a dead default listener is not mirrored. The api_server / webhook
  adapters stamp the listener they actually bound (`listener_base`) on connect so the URL is
  the real one, not a config guess. `hermes status` lists those URLs beside the other
  shared-ingress platforms.
- /api/status?profile= reports `gateway_shared_with` (every profile the multiplexer carries)
  when the served rung answered; null for a standalone gateway.
- Desktop: the messaging card shows the URL line; "Restart gateway" from a served profile
  (statusbar menu, Cmd+K, messaging/webhooks banners, Command Center) confirms "Restart the
  shared gateway? All bots on this device reconnect: default, alpha, beta" (Restart all /
  Cancel) and toasts "Shared gateway restarted (3 bots)". Standalone keeps the silent path.
- Dashboard: same confirm + toast on the System page and the sidebar restart; the 409 from
  start/stop on a served profile renders as an inline notice instead of a raw error toast.
2026-09-12 12:52:19 -07:00
teknium1
d1dbb0ac9e feat(gateway): multiplexer hot-serves profiles created while it runs, unroutes deleted ones
A `gateway.multiplex_profiles` gateway enumerated `profiles/` once at boot, so a profile
created afterwards (CLI, dashboard, Desktop, TUI) was never served until `hermes gateway
restart`; Desktop and the dashboard gave no reminder, so a new profile's bot simply never
connected.

The served set is now reconciled at runtime (`gateway/run_profile_reconcile.py`):
- `hermes_cli/profiles.py` create/delete ping the multiplexer over its control socket
  (new `rescan-profiles` verb); a supervised watcher rescans every 30s as the safety net.
- A new profile gets its adapters under its own runtime scope from its config/.env
  (`_start_one_profile_adapters`, same duplicate-credential guard as boot, now seeded
  with the LIVE secondaries' claims), `served_profiles` in gateway_state.json is
  updated, MCP discovery + log routing run for it. Other profiles' adapters are never
  touched.
- A served profile whose config.yaml/.env changed is re-scanned so a token added after
  create builds the adapter; already-live/queued platforms are skipped (no second poller).
- A deleted profile (tombstone) has its reconnects cancelled, adapters torn down,
  pairing/busy bookkeeping and cached agents dropped, and this process's SQLite /
  memory-store handles released so the deleter's rmtree succeeds.
- The in-process cron ticker takes a live enumerator so new profiles' jobs fire.
- PUT /api/messaging/platforms/<id>?profile=X returns `hot_served` when a live
  multiplexer rebuilt X's adapters; Desktop/dashboard skip the restart banner then.
- `hermes profile create` confirms hot-serve; the restart reminder stays for a gateway
  that did not pick the profile up (older build / signal failed).
2026-09-12 08:49:16 -07:00
Teknium
65fd3a2b9c feat(status): show each served profile's shared-listener callback URLs
hermes -p <name> gateway status, hermes gateway status and hermes status (under
Serves:) list the /p/<profile>/<path> URL per inbound-port platform the live
multiplexer serves, read from the <profile>:<platform> ingress_url in the default
home's gateway_state.json (hermes_cli/gateway_multiplex_served.py). The dashboard's
messaging payload carries the same ingress_url and the Channels page renders it.
The dashboard's 409 guard now covers only api_server/webhook (the mirrored pair):
enabling Twilio/LINE/Teams/... on a secondary is allowed because the gateway serves
it.
2026-09-12 01:53:15 -07:00
Teknium
df95f378a6 feat(dashboard): "Migrate to a single multiplexed gateway" on the System page
GET /api/gateway/migrate/plan returns the CLI plan JSON; POST
/api/gateway/migrate spawns `hermes gateway migrate --multiplex --yes`
detached (action log gateway-migrate.log). The Gateway card shows the
button only for a multi-profile install that is not yet multiplexed, and
disables it while listing the blockers.
2026-09-12 01:49:28 -07:00
Teknium
0dcadf6f41 revert: remove Collective Wisdom V1 (#94266)
Reverts the in-tree org skill-marketplace: hermes_wisdom package, three
model tools, CLI/gateway/desktop/dashboard/Telegram/Slack surfaces.

Later non-Wisdom work on shared files (guest onboarding i18n, dashboard
startup schema, Slack adapter, tui_gateway) is kept; Wisdom-only call
sites and config were stripped from those files.
2026-09-11 11:54:49 -07:00
shannonsands
a6ee31f55a feat(wisdom): add Hermes Collective Wisdom Agent V1 (#94266)
* feat(wisdom): add trusted publish and install foundation

* feat(wisdom): add private contribution loop

* feat(wisdom): add managed consumption workflows

* fix(wisdom): close cross-repository safety gaps

* fix(wisdom): align local package and lifecycle policy

* fix(wisdom): require explicit profile setup

* docs(wisdom): repin reconciled gateway head

* fix(wisdom): fence content downloads and approval receipts

* docs(wisdom): record generation-fenced downloads

* docs(wisdom): record unified delivery PR

* fix(ci): stop passing invalid classifier inputs

* docs(wisdom): remove internal requirements ledger

* feat(wisdom): localize dashboard and desktop copy

* feat(wisdom): complete local contribution and consumption UX

* style(wisdom): satisfy desktop lint

* chore(wisdom): refresh requirements pin

* test(dashboard): allow formatted profile copy

* test(wisdom): stabilize desktop interaction coverage

* fix(wisdom): surface dashboard action failures

* fix(wisdom): add repeatable Portal demo login

* feat(wisdom): add actionable skill notifications

* feat(wisdom): add notification install and update actions

* fix(wisdom): make Telegram skill alerts actionable

* fix(wisdom): always refresh demo Agent login

* feat(wisdom): embed Telegram notification actions

* fix(wisdom): preserve Telegram notifications after actions

* fix(wisdom): keep Telegram notification cards readable

* feat(wisdom): add Telegram candidate approval flow

* feat(wisdom): explain Telegram qualification reasons

* fix(wisdom): reconcile cross-surface candidate actions

* feat(telegram): add Collective Wisdom management command

* chore(wisdom): refresh Gateway contract pin

* chore(wisdom): advance Gateway contract pin

* feat(wisdom): align command UX across clients

* feat(slack): add Collective Wisdom management parity

* feat(wisdom): add security and professionalism reviews

* feat(wisdom): add first-time qualification guidance

* feat(wisdom): simplify qualification sharing choices

* feat(skills): add optional editorial metadata

* feat(wisdom): enrich legacy skill presentation

* fix(wisdom): harden review and update boundaries

* fix(wisdom): emit canonical review timestamps

* fix(wisdom): align with merged gateway and main

* wisdom: add agent-led sharing core (policy, evidence, schemas, templates, delivery, weekly job, share/install flows)

- hermes_wisdom/agent_led/: policy resolution (server > local > defaults),
  7-day evidence builder that excludes bundled/hub/managed skills and
  dismissed/handled/recently-suggested content hashes, strict pydantic
  schemas for agent output with repair-or-reject, fixed copy templates
  (Share / Teammate / Published / Update / Mute), idempotent retried
  delivery ledger with stale-action resolution, weekly review job,
  resumable Share and Install flows.
- prompts/: candidate review, recipient recommendation, share packaging.
- tests/wisdom/test_agent_led.py: 30 tests.

* wisdom: agent-led renderers and button action dispatcher

- render.py: Telegram HTML, Slack blocks, Desktop payload; editorial name
  is the emphasized line, product label stays separate.
- actions.py: resolve opaque wa:<action>:<dedup> targets via the delivery
  ledger; Not now -> dismissal, Mute -> fixed options, Share -> resumable
  packaging flow, Install/Update -> plan command. Never publishes/installs.

* wisdom: CLI verbs, agent_led config default, conversational catalog skill

- hermes wisdom browse/review-week/act/share/dismiss/mute (all --json).
- wisdom.agent_led config block, default enabled.
- SKILL.md rewritten so natural-language catalog questions map to the CLI
  verbs, share/install flows and fixed notification templates.

* wisdom: wire agent-led weekly review into gateway tick and Telegram buttons

- gateway housekeeping tick calls maybe_run_weekly_review with a home
  channel sender when a Telegram adapter is available.
- Telegram: wa: callbacks resolved through the ledger (stale-safe), mute
  duration keyboard, send_wisdom_agent_recommendation rich card + fallback.

* fix(wisdom): integrate local mediation and harden model and setup boundaries

* fix(wisdom): honor authoritative recommendation policy and defer on failure

* fix(wisdom): synchronize opaque suppression and recheck delivery preferences

* feat(wisdom): route weekly selection through the session-owned assessment queue

* fix(wisdom): prepare and submit the reviewed generated share package

* feat(wisdom): separate native Share preparation from publication consent

* feat(wisdom): sync native mute choices through a leased preference outbox

* feat(wisdom): bind native mute controls to durable preference choices

* feat(wisdom): add scoped desktop and dashboard notification settings

* fix(wisdom): revalidate feed recommendations before assessment and delivery

* fix(wisdom): persist validated delivery receipts before completing notices

* feat(wisdom): add private notification claim and receipt client

* Persist Wisdom send reservations and recover delivery acknowledgements

* Route legacy Wisdom controls through current native review

* Add typed private Wisdom operation outcome client

* fix(wisdom): make agent-led advice usable in the local demo

* fix(wisdom): keep requested consent outside proactive limits

* fix(wisdom): distinguish unavailable assessments and preserve digest text

* fix(wisdom): assess ongoing usefulness beyond the current task

* fix(wisdom): restore immediate qualification sharing controls

* fix(wisdom): separate qualification review from installation advice

* fix(wisdom): collapse review checklists and simplify sharing copy

* fix(wisdom): show compact sharing progress and publication receipts

* fix(wisdom): require credential prefixes rather than matching skill names

* fix(wisdom): finish package checks before presenting sharing consent

* fix(wisdom): scan local skills before qualification cards

* fix(wisdom): update moderation results on existing sharing cards

* fix(wisdom): keep sharing review accessible from receipt cards

* fix(wisdom): align mediated review cards and collapsible checks

* fix(wisdom): clarify clean security summary wording

* fix(wisdom): normalize consent plans and add explicit recheck

* fix(wisdom): keep install and update receipts concise

* fix(wisdom): collapse assessments and deduplicate operation cards

* fix(wisdom): restore private Portal review from native cards

* fix(wisdom): sync Portal publication to original consent card

* fix(wisdom): show local skill version on sharing cards

* fix(wisdom): skip agent recommendations for self-published versions

* fix(wisdom): simplify candidate notices and local-edit recovery copy

* feat(wisdom): submit locally reviewed packages with one confirmation

* feat(wisdom): expose safe receipt and outcome sync recovery

* wisdom: onboarding notice says detect and share, names the user's own skill

Copy review from the product owner on the first and returning
qualification notices (fixed delivery mode):
- the feature blurb now says the org enabled detection *and sharing*
- both notices say the detected skill is one the user created
- both close with an exclamation mark

Applied identically to hermes_wisdom.notice, the desktop and web i18n
strings, and the tests that assert the sentences.

* wisdom: one opener, no approval line, ask to share after the skill is shown

Product owner review of the candidate card.

- The Hermes written card now opens with the same sentence as the fixed card
  ("Your organisation has enabled Collective Wisdom, a feature designed to
  automatically detect and share useful skills across all team members.")
  instead of its own blurb, so there is one first time message.
- "Nothing is shared without your approval." removed from Telegram, Slack
  and Desktop. The buttons already make the permission explicit.
- "Would you like to share?" no longer appears before the skill is named.
  It is now the last line, after the skill name, description, why suggested
  and the checks, and reads "Would you like to share it?" (matching the
  agent led template wording).

Tests updated for the new order; proposalNotice removed from all desktop locales.

* wisdom: American spelling, organization

Product owner decision: user facing copy uses American spelling.
Changes "Your organisation" to "Your organization" in the chat notice,
the Hermes written card opener, the desktop and web strings, and the
tests that assert them. Identifiers such as nas_organisation:* and the
German and French locales are untouched.

* wisdom: candidate card copy round 4 (owner review)

Apply the product owner's round 4 copy decisions to the Hermes Collective
Wisdom candidate card on Telegram, Slack, Desktop and the shared views:

1. Hermes-written cards are titled "Hermes Collective Wisdom" instead of
   the bare "Collective Wisdom".
2. The "Reusable skill ready to review" line is gone from the candidate
   card (Telegram rich card and plain fallback, legacy agent-led share
   template).
3. The skill name and description are labelled: "Skill name: <name>" and
   "What it does: <description>" (Telegram, Slack, Desktop).
4. "Why suggested:" is now "Why others might benefit:".
5. A passing professionalism review reads "Safe to share at work ✓ (no
   inappropriate content found)" with no per-check bullets and no "Pass";
   a failed review reads "Needs a look before sharing at work (possible
   inappropriate content)" and lists only the checks that flagged
   something. Pending/unavailable wording is unchanged.
6. Telegram button toasts: "Will ask later...", "Preparing more
   details...", "Sharing...".
7. Qualification reasons: "You used this skill consistently across many
   days." and "You've really refined this skill."
8. prompts/wisdom_candidate_review.md asks for a compelling
   editorial_name, a simple one_line_description and a compelling
   why_coworkers_benefit under 300 characters; "Be concise and
   convincing." becomes "Be concise and compelling: the goal is that the
   user wants to share it."

Tests updated for the new strings; review_text() gains direct coverage.

* wisdom: re-apply owner copy after rebase

- Native share cards (advice_view/interaction_view): drop the approval line, ask "Would you like to share it?" as the last line after the checks
- Hermes-written completion card titled "Hermes Collective Wisdom"
- Qualification reasons use the owner wording (consistently across many days / really refined)
- American spelling (organization) in remaining English copy
- Desktop test asserts the current Share button; web test matches the returning notice

* fix(wisdom): pin reconciled Gateway and verify Unicode hash vectors

Pin Gateway 60cd2d6b613ae3cd4a6e65155d1142006d907e78 and byte-identical producer artifacts. Verify every content-order case and package-manifest binding. Validation: 186 focused Python tests, Ruff and contract verifier.

* fix(wisdom): reconcile optional SDK tests and frontend lint

* fix(wisdom): default to agent-written notification summaries

* fix(wisdom): restore deferred install review and browse controls

* feat(wisdom): inspect installed setup with exact package provenance

* feat(wisdom): run native-approved installed setup steps with durable evidence

* fix(wisdom): recover interrupted setup with explicit native consent

* feat(wisdom): hand native installs into guided setup review

* fix(wisdom): continue requested setup with fixed notification copy

* fix(wisdom): preserve setup while waiting for a session model

* fix(wisdom): expose canonical setup review controls on desktop

* fix(wisdom): resume setup after recorded automatic updates

* fix(wisdom): make missing setup prerequisites recheckable

* chore(wisdom): align Agent with verified Gateway contract

* fix(wisdom): stop guessing team slugs in portal links

* fix(wisdom): retire pending advice on account sign-out

* fix(wisdom): cancel advice after terminal account revocation

* fix(wisdom): fence feed responses across account sign-out

* fix(wisdom): checkpoint signed-out feed before reactivation

* fix(wisdom): link proactive advice to scoped notification settings

* fix(wisdom): coalesce queued publication recommendations by version

* fix(wisdom): keep package review navigation local and deferable

* fix(wisdom): reflect installed state in discovery controls

* fix(wisdom): show exact checks before command confirmation

* chore(wisdom): pin bounded analytics privacy contract

* chore(wisdom): pin retired legacy notification contract

* feat(wisdom): review publisher usage with exact sharing copy

* fix(wisdom): align discovery and review check summaries

* fix(wisdom): show expired consent before confirmation

* fix(wisdom): require fresh review for legacy install controls

* fix(wisdom): preserve review expiry across check toggles

* fix(wisdom): retain update policy in native install reviews

* fix(wisdom): surface failed native card edits

* fix(wisdom): persist local command approval reviews

* fix(wisdom): use saved approvals for messaging commands

* test(wisdom): provide scan result in setup handoff fixture

* test(wisdom): exercise Telegram approvals with saved review state

* fix(wisdom): retain suppression policy for offline deferral

* fix(wisdom): reconsider candidates after deferred suppression expires

* fix(wisdom): bind review checks and report verified readiness separately

* fix(wisdom): persist accepted publication intent and recover exact outcomes

* fix(sync): pin UTF-8 tree ordering across writers

* chore(wisdom): pin organisation-scoped Gateway authorization

* fix(wisdom): restrict consent delivery to user-facing sessions

* chore(wisdom): refresh reviewed Gateway contract pin

* fix(wisdom): preserve kept tools in Blank Slate exclusions

* test(auth): reset anonymous fixture with a profile-scoped cache

* fix(wisdom): gate local surfaces and work on current profile entitlement

* fix(wisdom): invalidate quiet tool cache on entitlement changes

* test(wisdom): authorize local consent gateway fixtures

* fix(wisdom): keep entitlement decoding free of native crypto imports

* test(wisdom): provide local entitlement to demo CLI subprocess

* ci: leave upstream workflow unchanged in Wisdom PR

* fix(wisdom): ship package and contracts in Nix wheels

---------

Co-authored-by: hbizi <36184542+hbizi@users.noreply.github.com>
2026-09-11 19:04:06 +10:00
teknium1
87cc4de430 fix(dashboard): place the deactivation ref reset after the callbacks that also write those refs
PR #106445's effect was inserted above `reconnectPty` / `startFreshPty` /
`startFreshDashboardChat`. React Compiler's `react-hooks/immutability`
rule then treats `ptyInputLineRef` and `mobileReplacementInputUntilRef`
as effect-owned values and flags the six pre-existing assignments in
those callbacks as errors ("This value cannot be modified"), which is
what turned the `JS & TS checks` job red. Declaring the effect after the
callbacks keeps the rule quiet; eslint on the file is back to main's
0 errors / 3 warnings.

Adds one pure-function vitest on `normalizePtyMobileInput` proving the
symptom: a stale tracked line ("hel") makes an in-window re-emission of
"hello" come out as 3×DEL + "hello", which over a composer already
holding "hello" leaves "hehello"; with the tracker reset the same input
passes through untouched.
2026-09-09 10:48:49 -07:00
Teknium
d47adec28f Merge origin/main into feat/plugin-catalog
Python plugin CLI/loader/web/tui files taken from main wholesale; the
catalog layer is re-ported onto main's decomposed shapes in the
following commits. plugin_index.py removed (catalog is the sole
discovery system).
2026-09-09 04:15:27 -07:00
Teknium
fd3565deec fix: remove dedicated user-facing output cap controls 2026-09-07 06:15:43 -07:00
Austin Pickett
04fd0172cd fix(dashboard): stop the embedded TUI repainting on every OS app-switch (#103165)
* feat(pty_bridge): mark the dashboard-spawned TUI with HERMES_PTY_HOST

Ink needs to know when its emulator is the dashboard's xterm.js rather
than a native terminal, so it can drop hidden-tab recovery work that only
makes sense for emulators that coalesce output.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

* fix(tui): skip the focus-in erase+repaint under the dashboard PTY

Ink answers a DECSET 1004 focus-in with a full clear+repaint to heal rows
a native emulator may have dropped while the tab was hidden. xterm.js fed
by the dashboard WebSocket never drops frames, and it reports focus on
every OS window blur/focus, so under the dashboard that repaint was a
visible "session reloaded" flash on every alt-tab. Keep the mode
re-assert and keep delivering the focus report to TerminalFocusProvider
(the composer hides its cursor on blur); only the repaint is skipped.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

* fix(web): restore terminal focus after an OS app-switch

Alt-tabbing away and back lands browser focus on <body>, so Ctrl+V never
reached the composer. Pull focus back into xterm on window focus under
the same ownership rule tab activation already uses, extracted into
shouldRestoreTerminalFocus so both paths share it.

Co-authored-by: Raymond <supere989@users.noreply.github.com>

---------

Co-authored-by: Raymond <supere989@users.noreply.github.com>
2026-09-04 20:24:02 -04:00
liuhao1024
74b00d7a97 fix(dashboard): route every per-row session request at the row's owning profile (salvage #99387)
The Sessions page listed rows stamped with their owning profile but sent
delete/bulk-delete to the global management profile, which stays "" while
the sticky active profile equals the dashboard process's own — so the
request opened the process store, missed, and returned a false
`already_absent` success while the row survived in profiles/<p>/state.db.

Same class at three sibling sites the PR didn't touch: renameSession,
exportSessionUrl and the expanded-row getSessionMessages read. One
`rowProfile(id)` owner now feeds all four (+ bulk delete); unstamped rows
(search results) fall back to the management profile as before.

Test trimmed to one jsdom scenario driving all four row actions.

Co-authored-by: Teknium <teknium@nousresearch.com>
2026-09-02 06:47:55 -07:00
Teknium
df4b3733ba fix(cron): every last_status consumer renders delivery_failed explicitly (dashboard badge, Desktop inspector, /cron list, docs)
Audit of every last_status reader outside the scheduler (rg last_status across
web/, apps/desktop/, hermes_cli/, tui_gateway/, tools/, scripts/, website/):

- web dashboard CronPage: last_status was never rendered at all — a
  delivery_failed job showed a green 'scheduled' badge and only a small red
  'delivery: ...' line. New pure cronLastResult() helper maps the closed
  literal set to tones (ok=success, delivery_failed/blocked_config=warning,
  error/unknown=destructive) and the card now shows an amber
  'delivery_failed' badge (title = last_delivery_error).
- Desktop hermes-bots routine inspector: 'Last result' printed the raw
  literal; routineLastResult() spells out each one ('Ran, but delivery
  failed', 'Blocked by configuration (not run)', ...), unknown passes through.
- /cron list (cli_commands_mixin): 'Last run: <ts> (delivery_failed)' now
  appends the delivery reason, since last_error is None for those runs.
- hermes cron list/doctor and the cronjob tool already handled the literal
  on this branch; no consumer compared == 'ok' for success apart from the
  cronjob manual-run path, which the branch already fixed.
- developer-guide/cron-internals.md: table of last_status literals + which
  detail field carries the reason.

Live repro (real 'hermes dashboard' on a temp HERMES_HOME with a
delivery_failed job, CronPage rendered against the live /api/cron/jobs):
before — badges [scheduled, default, telegram:123]; after — badges
[scheduled, delivery_failed (warning tone, title 'telegram: 502 Bad
Gateway'), default, telegram:123].
2026-09-02 00:52:58 -07:00
Ryan Currah
c8a7c6c36b fix(dashboard): remove deleted custom env keys 2026-08-31 03:37:43 -07:00
joaomarcos
0099f250c2 fix(auth): close Anthropic OAuth review gaps 2026-08-29 18:34:35 -07:00
Teknium
7e17ff0ab3 Merge origin/main into feat/plugin-catalog — reconcile with landed index/manifest-v2 tracks 2026-08-27 21:29:42 -07:00
chelsealong
9c013eaaf8 fix(dashboard): follow scroll on implicit active-session resume (#93518)
pty_ws already fell back to the per-channel active-session file when a
/chat WS connects with no ?resume= param, replaying the whole session
into the PTY, but the frontend only pinned xterm's viewport to the
bottom when resumeParam came from the URL (#59591). The implicit path
had no way to learn a replay was happening, so the viewport stayed at
the top of the scrollback.

pty_ws now sends a one-off JSON control frame naming the session id it
resolved from the active-session file, before any PTY bytes; PTY
output itself always arrives as binary frames, so this is unambiguous
on the wire. ChatPage tracks an `effectiveResume` value seeded from
resumeParam and updated when this control frame arrives, and the
existing follow-scroll/sanitizer/hydration logic keys off it instead
of the URL param alone.

Fixes #93518.
2026-08-24 03:21:49 -07:00
Teknium
1bf8bd2c7d feat(models): 'ox alpha' now finds x-preview-f-free in every model picker
The OpenCode Zen wire slug for the Ox Alpha stealth model is opaque
(x-preview-f-free); users searching the picker for 'ox' or 'ox-alpha'
found nothing. Adds the search alias across all four synced alias
tables (CLI, desktop, web, TUI) plus tests. Wire id is unchanged and
still what renders and gets sent to the provider, matching the k3 →
kimi-k3 precedent. No canonical-dedup collision with opencode-go's
keyed ox-alpha-free slug.
2026-08-21 14:38:19 -07:00
Teknium
a1682376ca feat(profiles): rename any agent — the default profile gets a display name (#45624)
`hermes profile rename default <name>` (and the Desktop/dashboard rename
flows) now set a presentation-only `display_name` in profile.yaml instead
of erroring. The canonical id stays "default"; resolution, comparison,
and spawn paths are untouched. Named profiles keep real renames and their
display_name survives the move.

Surfaces: profile list/show/status, /profile (text only — data.profile
stays canonical), dashboard ProfilesPage, TUI-gateway profiles.list, and
Desktop (rail, switcher, Manage page, and the Bot Mode roster via a
displayName fallback so a renamed default shows its name, not "default").

Slimmer redo of the direction in PR #87760 by @yxssxn — thanks; see PR
body for what changed vs that approach.
2026-08-18 02:27:18 -07:00
Teknium
cb1b1da219 fix: surface missed cron fires as last_fire_error on the job record
On hosted deployments a scheduled fire that cannot be forwarded to the
gateway api_server (dead 8642 listener, gateway down) was invisible
outside gui.log: no execution row is created because the claim never
happens, so `cronjob list` showed a healthy job that silently missed
days of scheduled runs (4 consecutive nightly misses in the field,
diagnosed only by log grep).

Changes:
- cron/jobs.py: note_fire_forward_failure() durably stamps
  last_fire_error ({at, detail}) on the job record; mark_job_run clears
  it on the next successful run so it always describes current
  auto-fire health (mirrors preflight_alerted/drift_alerted).
- hermes_cli/web_routers/cron.py: the dashboard fire webhook stamps the
  job on the gateway-unreachable path, best-effort (never disturbs the
  503/Retry-After retry contract or the OOF-266 intentional-stop drop).
- tools/cronjob_tools.py: _format_job carries last_fire_error so the
  agent-facing cronjob list surfaces it.
- hermes_cli/cron.py: `hermes cron list` prints a red
  "Missed scheduled fire" line.
- web/: dashboard CronPage renders the miss; api.ts type updated.
- gateway/run.py: one-time startup warning when an external cron
  provider is active but the api_server adapter is not running (the
  fire path is dead-on-arrival; most common cause is API_SERVER_KEY
  missing from an unsupervised gateway relaunch).
- website/docs: cron doc section on missed fires.
2026-08-17 11:29:10 -07:00
Teknium
3ef63373f8 Port from paperclipai/paperclip#10875: route all dashboard copy actions through the HTTP-safe clipboard helper
Self-hosted dashboards served over plain HTTP on a LAN have no
navigator.clipboard (insecure context), so every direct writeText call
silently failed. web/src/lib/clipboard.ts already ships the HTTP-safe
copyTextToClipboard fallback but only OAuthLoginModal used it; ChatPage
(OSC 52 + Ctrl/Cmd+Shift+C), ProfilesPage, SystemPage, and WebhooksPage
all bypassed it. Route them through the helper and add a source-level
regression test that rejects any new direct clipboard write outside
lib/clipboard.ts (clipboard reads are exempt: no legacy fallback exists).

Sabotage-verified: the guard test fails when a direct write is introduced.
2026-08-16 22:10:29 -07:00
Teknium
ea29702749 feat(cron): --continuity / --no-continuity flags on hermes cron create/edit
CLI parity for the continuity toggle:

- subcommands/cron.py: --continuity on create; --continuity / --no-continuity
  tri-state pair on edit (same store_const pattern as --no-agent/--agent)
- cron.py: forwarded to the cronjob tool; created/edited job summaries print
  a "Continuity: on" line
- cronjob_tools._format_job: reports continuity as an explicit boolean and
  strips the reserved 'self' entry from the reported context_from list
- cron-job.ts: form reader accepts both shapes (raw store record with 'self'
  inside context_from, or formatted record with the explicit flag)
- docs: CLI flag examples in the continuity section

E2E (real argparse -> cron_create/cron_edit -> jobs.json in temp HERMES_HOME):
create --continuity stores ['self']; edit --no-continuity clears; edit
--continuity restores; default-off unchanged. 91 cron/tool tests + 16 CLI
cron tests + vitest 10/10 pass.
2026-08-16 22:09:28 -07:00
Teknium
0b13cafffa feat(cron): continuity toggle across dashboard, Bot Mode routines, and TUI cron RPC
Wire the continuity flag through every cron-creation surface, not just the
model tool:

- dashboard (web/): checkbox in the cron job editor; form state round-trips
  the stored reserved 'self' entry into the toggle and strips it from the
  context_from textarea; web_server dashboard validator skips 'self'
  (create precedes the job's existence)
- Bot Mode Routines tab (hermes-bots plugin): Continuity checkbox in the
  New Cronjob dialog, forwarded through cron.manage
- tui_gateway cron.manage RPC: optional continuity param on action=add

vitest cron-job suite 10/10 (4 new), tsc app project clean, py_compile clean.
2026-08-16 22:09:28 -07:00
Shannon Sands
67710548ec fix(web): keep the chat terminal input line above the mobile soft keyboard (NS-434)
On mobile the on-screen keyboard overlays the layout viewport instead of
resizing it (iOS Safari always; Android Chrome under its default
interactive-widget=resizes-visual). The dashboard shell is a fixed h-dvh
column, so the xterm host's bounding box never changed when the keyboard
opened: fit() computed identical (cols, rows), no RESIZE reached the PTY,
and the Ink input line — drawn at the bottom of the grid — stayed hidden
under the keyboard.

Fix, in three parts:

1. Keyboard-inset handling (new web/src/lib/keyboard-inset.ts).
   computeKeyboardInset() measures the layout-viewport region obscured by
   the keyboard via window.visualViewport
   (innerHeight - vv.height - vv.offsetTop, with an 80px floor so
   collapsing URL-bar chrome doesn't thrash the grid). ChatPage applies
   it as bottom padding on the terminal wrapper, which shrinks the host →
   the existing ResizeObserver/fit path recomputes rows and sends RESIZE →
   Ink redraws the input line above the keyboard. Listens on both vv
   resize and scroll (offsetTop changes arrive as scroll events on iOS).

2. interactive-widget=resizes-content in the viewport meta. Android
   Chrome 108+ then resizes the layout viewport natively and the JS inset
   computes ~0 (harmless no-op); iOS ignores the directive and takes the
   JS path.

3. Scroll pinning. iOS auto-scrolls the page to reveal xterm's hidden
   textarea on focus, which drags the fixed shell offscreen. While a
   keyboard inset is active we pin window/scrollingElement scroll back to
   0 and term.scrollToBottom() so the freshly-resized input line stays in
   view.

Unit tests cover the inset math (thresholds, offsetTop, rotation races,
fractional geometry, non-finite guards). Grid-level behavior needs a real
device pass — DevTools emulation doesn't model keyboard insets.
2026-08-16 22:06:16 -07:00
fangliquanflq
0b8a09759c fix(sessions): address prune skip review notes 2026-08-16 01:55:25 -07:00
fangliquanflq
29dfbf2d6a fix(sessions): surface open sessions skipped by prune 2026-08-16 01:55:25 -07:00
Teknium
fbe4d73051 test: type the WebSocket mock to satisfy docker-build tsc strictness 2026-08-15 01:05:39 -07:00
null-runner
4e6d863d0e fix(dashboard): gate shortcut sends on PTY state
The word-delete branches sent ^W / ESC d whenever the socket was OPEN,
bypassing the shouldBlockPtyInput gate that term.onData applies to
every other keystroke, so a reconnecting/closed session could still
receive shortcut bytes. Route both branches through a shared guarded
sender that applies the identical socket + PTY state check, and cover
the non-open PTY states in the lib tests.
2026-08-15 01:05:39 -07:00
null-runner
be250390db fix(tui): restore copy and cut shortcuts 2026-08-15 01:05:39 -07:00
Denis H
00516e6e8e test(dashboard): cover delayed IME fallback after unrelated input 2026-08-15 00:33:49 -07:00
Denis H
0d40955fd1 fix(dashboard): handle chunked IME composition input 2026-08-15 00:33:49 -07:00
Denis H
434f1e954c fix(dashboard): retain composition after unrelated input 2026-08-15 00:33:49 -07:00
Denis H
a428531ef6 fix(dashboard): preserve consecutive IME composition input 2026-08-15 00:33:49 -07:00
Denis H
874ae74a10 fix(dashboard): keep mouse input out of IME fallback 2026-08-15 00:33:49 -07:00
Denis H
3d9fdb2e2b fix(dashboard): avoid duplicate IME fallback input 2026-08-15 00:33:49 -07:00
Denis H
c066b5a1d3 test(dashboard): cover IME fallback lifecycle 2026-08-15 00:33:49 -07:00
Denis H
0924e33369 fix(dashboard): defer IME fallback until xterm input settles 2026-08-15 00:33:49 -07:00
Denis H
6ece2da738 fix(dashboard): forward committed dead-key input to PTY 2026-08-15 00:33:49 -07:00