Commit Graph

21988 Commits

Author SHA1 Message Date
Teknium 657fd6a116 fix(tests): close SessionDB before rmtree in codex persist test
The codex app-server no-usage path (_record_codex_app_server_usage) calls
queue_token_counts(), which spawns the daemon session-db-token-writer
thread. test_codex_turn_persists_each_message_exactly_once tore down with
a bare shutil.rmtree(tmp) while that writer could still be mid-commit,
racing WAL/-shm file re-creation against rmtree's unlink pass — os.rmdir
then fails with ENOTEMPTY. Seen as a CI FLAKY retry in run 31681231786
(slice 6/12). db.close() drains the queue and joins the writer, making
teardown deterministic.
2026-08-13 01:30:33 -07:00
Teknium a7f0abc845 fix(email): dispatch partial batches, seen-after-fetch UIDs, reconnect UID baseline restore
Follow-ups to the salvaged #80032 fatal-error escalation, closing the
gaps its review thread identified plus a sibling of the same class:

1. Partial-batch loss: _check_inbox now dispatches whatever the fetch
   returned BEFORE escalating a failure — the early-return dropped
   already-fetched messages whose UIDs were marked seen.
2. Seen-after-fetch: UIDs enter _seen_uids only after their fetch
   returns a response, so a mid-batch connection failure leaves the
   remaining UIDs eligible for the next poll. Per-message processing
   moved to _parse_fetched_message behind a poison guard: a message
   that fails parsing/auth-verification is marked seen, logged with
   its UID, and skipped once — never an eternal crash loop.
3. Reconnect mail loss: connect(is_reconnect=True) restores the
   account's seen-UID baseline from a class-level snapshot instead of
   re-marking the entire mailbox seen — mail that arrived during an
   outage is now processed after the reconnect the escalation triggers.

7 new regression tests.
2026-08-13 01:24:54 -07:00
kyssta-exe 9b8da52f41 fix(email): surface IMAP fetch failures through the fatal-error hook (#80016)
_fetch_new_messages() wrapped the whole IMAP connect/login/select/search/
fetch sequence in a bare except that logged and returned an empty list —
indistinguishable from a genuinely empty inbox. The adapter never invoked
its fatal-error handler, so the gateway's reconnect/backoff/status
machinery never learned the mailbox was unreachable; outages lasted until
a manual restart.

Track fetch failure on the adapter and, when the poll loop observes it,
set a retryable fatal error (email_imap_fetch_failed) and notify the
gateway handler so the platform enters the reconnect queue just like a
startup connection failure.
2026-08-13 01:24:54 -07:00
hermes-seaeye[bot] 90c7180dce
fmt(js): `npm run fix` on merge (#85140)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-13 08:15:48 +00:00
kshitij a4f468e832 refactor(gateway/desktop): consent-first truncation precedence + dedup (simplify pass)
Final-diff simplify/review pass findings on #83785:

- Consent gate (confirm_truncate -> 4029) now checked BEFORE target
  resolution, restoring the pre-PR precedence: an unconfirmed submit
  carrying truncation params refuses without paying the durable-transcript
  read or heal-stamping live history dicts, and an unconfirmed out-of-range
  ordinal returns 4029 (not 4018). Malformed params still refuse first
  with 4004. Regression test added (spy DB asserts zero reads pre-consent;
  mutation-checked against the previous commit).
- _coerce_truncate_ordinal generalized to _coerce_truncate_int(param_name):
  the row_id branch was inlining the exact bool-guard + int() -> 4004
  pattern the helper had just extracted.
- Deleted the dead user_indices re-read after _resolve_truncate_row_id
  (heal mutates dicts in place; the filter output is identical) and the
  duplicate range check that had deadened the pre-existing guard.
- Desktop: exported isVisibleUserMessage from use-prompt-actions/utils and
  used it in visibleUserOrdinal / visibleUserIndexAtOrdinal /
  rebindSurvivorRowIds — one predicate for the ordinal parity all three
  depend on instead of three verbatim copies.
- Docs: programmatic-integration.md documents survivor_user_row_ids.
2026-08-13 13:35:55 +05:30
kshitij f6081b6647 style(desktop): satisfy perfectionist import order + padding lint rules
check:lint failed on the two sort-named-imports errors (survivorRowIdsFrom
before type SurvivorUserRowIds) introduced by the rebind commit; also adds
the blank line eslint wanted in the new test helpers.
2026-08-13 13:35:55 +05:30
kshitij 42eec4ab38 fix: return survivor row ids after rewind so clients can rebind stale rowIds
Review follow-up (StanleyStetson + egilewski on #83785/#83202): a successful
rewind's replace_messages(archive_dropped=True) re-inserts the surviving
prefix as NEW SQLite rows. Gateway memory picks up the fresh _row_id stamps
via lastrowid, but the Desktop's surviving bubbles kept their pre-rewind
ChatMessage.rowId — so a second rewind/edit/regenerate of an older surviving
turn sent a stale truncate_before_row_id and was (correctly) refused with
4018 until a transcript reload. Fail-closed stays untouched, per both
reviews; the fix is rebinding, not ordinal fallback.

Server: prompt.submit now returns survivor_user_row_ids (fresh post-rewrite
ids of surviving visible user turns, in visible-user-ordinal order) on both
the inline and compute-host paths whenever a durable truncation committed.

Desktop: runRewindSubmit surfaces the field; restore/edit/reload on both the
primary chat and session tiles rebind surviving user bubbles positionally
(same visible-user filter the ordinal math uses) and clear any rowId they
cannot rebind — a cleared id degrades to the ordinal path instead of a 4018.
Absent field (older gateway) leaves state untouched.

Tests: consecutive-rewind regression on a real SessionDB (stale id 4018s,
returned id succeeds; mutation-checked) + vitest for survivorRowIdsFrom /
rebindSurvivorRowIds (rebind, null-clear, past-end clear, hidden skip,
identity preservation).
2026-08-13 13:35:55 +05:30
kshitij 4aeb6f4a4f style(desktop): drop stray semicolons in rewind.ts
Three PR-introduced trailing semicolons in a semicolon-free file;
rewind.test.ts re-run green (5/5) with the change in place.
2026-08-13 13:35:55 +05:30
kshitij 040420bd11 refactor(gateway): dedupe truncation-target validation; drop dead state and redundant test
Review cleanup on the #83202 salvage (findings from the 4-angle + 3-reviewer
passes, all verified against the diff):

- Extract _coerce_truncate_ordinal() and _reconcile_client_ordinal(): the
  bool-check/int-coercion block was duplicated verbatim 3x and the 4030
  ordinal-mismatch block 2x across the row-id/message-id/ordinal branches
  (~90 lines of copy-paste with drift risk between the two durable branches).
- Delete target_idx (4 assignments, 0 reads — the cut uses
  user_indices[ordinal]) and replace the stale inline user-indices
  comprehension with the _history_user_indices helper it duplicated.
- Drop test_reproduce_row_id_truncation: a strict subset of
  test_prompt_submit_truncates_by_row_id +
  test_prompt_submit_refuses_ordinal_and_row_id_mismatch with weaker asserts.
- Collapse PR-introduced blank-line runs in the test file.

Behavior-preserving: error codes, messages, and log fields unchanged
(4004/4018/4029/4030 wording identical); full test_tui_gateway_server.py
suite green (549 passed).
2026-08-13 13:35:55 +05:30
kshitij 16de3c3f1b fix(gateway): verify memory/durable alignment before trusting position in row-id resolve
The #83202 heal path zip-stamped _row_id onto live-memory dicts purely by
position whenever the durable and live lists had equal length, and the DB
fallback mapped durable user-ordinals onto live indices with only a bounds
check. Equal length is not proof of alignment: the durable copy is loaded
with repair_alternation=True (merges user;user pairs, collapses consecutive
assistants, drops orphan tool rows) while live memory is unrepaired and can
carry optimistic/marker rows — the two can coincide in length while
position-shifted. A misaligned stamp is sticky: it permanently attaches the
wrong durable id to a live dict and re-aims every later rewind (E2E probes
showed a wrong-content cut and a persisted alternation break).

_mem_db_pair_agrees() now gates both paths: the heal loop stamps only when
EVERY zip pair agrees on role, display-marker status, and (for addressable
user turns) content; the ordinal fallback verifies the mapped live turn
shows the durable target's content, else refuses via the existing
fail-closed 4018. Regression tests derived from the review probes (content
swap, role shift, repaired-merge ordinal shift); the misalignment guards
fail on the pre-fix code.

Surfaced during review of PR #83202 for #82959.
2026-08-13 13:35:55 +05:30
StanleyStetson 23da6d6fe2 fix(gateway/desktop): durable row-id addressing for rewind truncation
Address rewinds/edits via SQLite messages.id (truncate_before_row_id)
instead of shifting user ordinals. Resolve against in-memory stamps,
then durable session history when live turns drop _row_id; refuse
unknown durable targets with 4018 (no ordinal fallback) and 4030 on
ordinal/row_id mismatch. Stamp _row_id on insert, load row ids on
resume paths, send rowId from Desktop, filter renderer-synthetic ids,
and stop silently resending failed targeted edits without truncation.
Add production-shaped SessionDB tests for resolve and fail-closed paths.

Fixes #82959
2026-08-13 13:35:55 +05:30
Teknium 9460cc11d4
fix: profiles.create mirrors launch credentials so new profiles can run (#85111)
A profile created through the headless ws door (profiles.create, #85093)
was born with no inference provider: create_profile() seeds a comment-only
.env, never copies auth.json, and a fresh profile has no config.yaml. Its
first message failed with 'No inference provider configured' and the flow
has no interactive setup step to recover with.

New mirror_credentials param (default true): copy the launch profile's
.env (only over the seeded stub — never clobber cloned secrets) and
auth.json (only when absent), both chmod 600, and inherit
model.provider/model.default when the caller gave no explicit pin and no
config was cloned. mirror_credentials:false preserves the old isolated
behavior byte-for-byte. Result gains a mirrored:{env,auth,model_inherited}
receipt. CLI and REST create paths untouched.
2026-08-13 00:25:04 -07:00
Brooklyn Nicholson 9deb0302ca fix(desktop): the sidebar remembers grouping per workspace scope
The Project-grouping flag was one global bool while the grouping beneath it
was already stored per scope (workspace vs all-profiles). Picking Project
inside a workspace therefore dragged the all-profiles view into the project
tree and vice versa — "I have to re-set grouping every time I switch."

The flag now lives per scope like its sibling grouping atoms (the flat key
keeps its historical name so existing choices survive), setSidebarGrouping
writes to the scope it just switched INTO when Profile flips the view, and
reset clears both scopes.
2026-08-13 02:18:53 -05:00
Brooklyn Nicholson 9be9925467 fix(desktop): themed fade scrollbar on the virtual list, not overlay
scrollbar-overlay opts out of the themed thin scrollbar; on Windows there
are no native overlay scrollbars, so Chromium painted the classic
always-visible gutter instead — a permanent scrollbar next to the recents
list. The themed fade bar reserves its 4px on every platform but stays
invisible until hover, and the wrapper no longer stacks a second scroller,
which is what the overlay class was originally working around.
2026-08-13 02:18:53 -05:00
Brooklyn Nicholson 81587c4f8f fix(desktop): inbox cards render in every sidebar view, not just flat recents
The card prop was gated off whenever Project grouping was active, so the
Inbox style toggle silently did nothing there. It is a render variant, not
a grouping: project lanes and overview previews now render the same card
the flat list does.

Also mirrors the section's real virtualization inputs (projectOverview /
entered-project content, not the persistent agentProjectTree cache) when
deciding the wrapper's scroll classes, and stops gating SCROLL_Y on that
parallel guess — the section is the single authority on which scroller
lives, so the recents pane can no longer end up with no scroller at all
(the "no sessions under Updated grouping after toggling settings" blank).
2026-08-13 02:18:53 -05:00
Brooklyn Nicholson e7032bb267 feat(desktop): recurrence-to-cron suggestion provider
Third draft provider: recurring phrasing in the draft ("every morning",
"daily", "each week") offers a Schedule-this pill. Click prefixes the
draft with an explicit scheduling instruction and the agent creates the
job via its cronjob tool on send — the pill never schedules anything
itself. Proper-noun guard keeps titles like "the Daily Prophet" quiet;
hyphen-as-word-char keeps "weekly-report.pdf" quiet.
2026-08-13 02:17:42 -05:00
Brooklyn Nicholson c8cad1cc9e feat(desktop): skill-match and connection-repair suggestion providers
Two new sources on the suggestion bus, one per provider shape:

- skill (draft): the draft names an enabled skill (whole-word,
  4+ chars), so offer to lead the message with its /command. Invoke
  prefixes the draft via the new 'prefix' insert mode and stands down
  once the draft starts with a slash; skill_manage invalidates the
  cached index alongside the slash-completion cache.
- repair (event): an mcp__ tool call failing with auth/connection-
  shaped output offers a one-click reconnect for that server, fed from
  the gateway tool.complete handler. Reconnect runs the shared OAuth
  flow with server-side cancel and reloads live tools before claiming
  success; a later successful call to the same server withdraws the
  offer on its own.
2026-08-13 02:17:42 -05:00
Brooklyn Nicholson a3da6d8071 feat(desktop): quiet suggestions the user has repeatedly ignored
The bus now keeps a session-scoped declined ledger: a pill the user
watched appear and let die three times stops re-offering for the rest
of the session. Acting on a pill clears its count, so a suggestion
that was taken can come back for the next trigger. In-memory on
purpose — a fresh session is a fresh chance.
2026-08-13 02:17:42 -05:00
Teknium fe5e7799f2 refactor: fold tailored intents guidance into the connect classifier
The cherry-picked #79448 predated #85049's _classify_connect_exception,
so it added a parallel PrivilegedIntentsRequired branch ahead of the
classifier (plus its own _is_privileged_intents_required detector).
Fold the tailored guidance into the classifier's existing intents arm
instead: one classification path, one error code (discord_intents_required),
and the message now names exactly the intents Hermes requested (Message
Content always; Server Members only when username/role allowlists need it).
Wizard callout, docs corrections, and tests from #79448 kept as-is.
2026-08-13 00:10:30 -07:00
rainbowgits b10e7890b6 fix(discord): name missing privileged intents and stop reconnect loop
PrivilegedIntentsRequired is a Developer Portal config error; surface which
intents Hermes requested as a non-retryable fatal and teach setup/docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 00:10:30 -07:00
Daniel Magro 590d547b40 fix(auth): tolerate legacy Codex suppression data 2026-08-12 23:47:38 -07:00
Daniel Magro c28114a5f8 fix(tests): isolate suite fixtures from host auth 2026-08-12 23:47:28 -07:00
Carl Taylor 654435210c feat(cron): surface model drift impact in Desktop 2026-08-12 23:47:22 -07:00
hermes-seaeye[bot] 1ef0a366da
fmt(js): `npm run fix` on merge (#85098)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-13 06:44:04 +00:00
Teknium 89a84e1ae6
feat: profiles.list/profiles.create ws RPC + plugin session-navigation doors (#85093)
Desktop plugins reach the backend exclusively through the generic ws
JSON-RPC door (host.request), but profile enumeration/creation only
existed on the dashboard REST router, which plugins cannot reach — so
anything 'one chat per agent profile'-shaped (bot rosters, profile
pickers, team panes) was impossible to build as a plugin.

- tui_gateway/methods_profiles.py: new @method handlers
  * profiles.list — profiles + optional last_session preview per profile
    (mirrors session.list's kanban/tool deny-list; best-effort per-profile
    state.db probe degrades to null instead of failing the call)
  * profiles.create — ws twin of POST /api/profiles (clone_from/clone_all/
    no_skills/description), plus optional SOUL.md content and a best-effort
    model+provider pin; mirrors the CLI flow (seed skills, safe alias)
  Both run on the RPC pool, not the WS reader thread (list_profiles walks
  skill trees; create copies bundles).
- SDK: host.openSession(id, { profile, intent }) — open a stored session
  the way core surfaces do, soft-swapping to the owning profile's backend
  first (ensureGatewayProfile), and host.newChat(profile) — fresh draft in
  a named profile (same door as the sidebar's per-profile '+').
- Docs: desktop-plugin-sdk.md gains both surfaces.

First consumer: a Grok Bot-style 'Bots' roster plugin (one persistent
chat per agent profile with a New Agent dialog) built on exactly these
four doors.
2026-08-12 23:33:58 -07:00
Brooklyn Nicholson 91a30705eb refactor(desktop): generalize the composer suggestion pills into a provider bus
The pill strip from the inline-MCP work is worth more than one source, so
the MCP-specific store splits into two layers with the same UX contract
(session-scoped, capped, self-limiting, one-click with narrated
idle→working→done):

- store/composer-suggestions.ts — the bus. Draft providers register into
  the existing debounced sampler; event providers push/withdraw directly.
  Offerings merge (event before draft), dedupe by provider-namespaced key,
  and keep reference identity on no-ops.
- store/suggestion-providers/mcp.ts — the founding provider, behavior
  unchanged: directory keyword/host matching, configured-server exclusion,
  one-click connect with OAuth cancel + config rollback.
- composer/suggestion-pills.tsx — the generic strip; phases and cancel
  live here, action/rollback/toasts stay with the provider's invoke.

No new pills yet — this is the seam for them.
2026-08-13 01:22:35 -05:00
Brooklyn Nicholson c7a1bfea07 docs: note the clarify recommended-choice ordering in the tools reference 2026-08-13 01:19:42 -05:00
Brooklyn Nicholson fd6af8f832 feat(desktop): render the clarify (Recommended) label in tertiary text
The card reads the labelled choices off the gateway request rather than
the raw tool args -- the backend applies the label there, and the card
only mounts once the request exists, so the args are a hydration-race
fallback. RECOMMENDED_LABEL and bareChoice live in the clarify store so
the component and the choice-length guard share one definition; without
the guard a long option could be dropped for length the label added.
2026-08-13 01:19:42 -05:00
Brooklyn Nicholson 10cf651484 feat(clarify): label the agent's recommended choice on every surface
The clarify schema now tells the model to order choices best-first, and
mark_recommended tags element 0 with "(Recommended)" at the tool layer --
the one platform-agnostic entry point -- so CLI, TUI, desktop, and every
messaging adapter inherit the label without a copy each. Each surface
already defaults its cursor to index 0, so the recommendation is the
pre-highlighted row too.

The label is presentation only: strip_recommended takes it back off
user_response, and choices_offered reports the bare list, so the agent
never reasons about (or echoes back) a string it did not write. Typed
replies on messaging platforms match with or without the suffix.
2026-08-13 01:19:42 -05:00
Brooklyn Nicholson 08a3b20dff test: register setup_mcp in the desktop_ui toolset + post-hook contracts
The toolset inventory and the post-hook ownership contract both
enumerate the GUI tools; the new tool joins both lists (and the
emit-once parametrization actually exercises its executor path).
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson 6ef0fc4f62 fix(desktop): cancel MCP OAuth flows server-side so a retry doesn't 409 2026-08-13 01:06:51 -05:00
Brooklyn Nicholson 3efce9b98c feat(desktop): suggest MCP servers from the composer draft as brand pills
A renderer-local directory of official hosted MCP remotes (URL-only,
vendor-documented endpoints — deliberately not the reviewed install
catalog) powers keyword and pasted-link suggestions: typing jira or
pasting a *.atlassian.net URL floats an 'Add Atlassian' pill in the
composer's micro-action strip. Matching is whole-word/phrase (unicode
boundaries) plus strict host-suffix on links, host hits outrank
keywords, capped at two, debounced 600ms, and excludes servers already
in mcp_servers. Pills are session-scoped like the micro-action badges
and self-limiting rather than dismissible — they exist only while a
trigger is in the draft. A click drafts the setup request; the agent's
setup_mcp card carries the consent. Brand glyphs extracted from the
mcp-tab into lib/mcp-brands (shared, monochrome marks follow the theme
so GitHub/Notion/Vercel survive dark mode).
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson 6cd4793081 feat(desktop): render setup_mcp as an interactive consent card in the transcript
The card follows the approval bar's consent vocabulary (primary-tinted
action + ghost decline, ⌘⏎/Esc with clarify's focus-stand-down rule) on
clarify's widget shell. Install prefers the reviewed catalog entry (env
prompts inline, background installs polled to completion) and falls back
to the desktop suggestion directory via the validated add-server POST +
OAuth; success reloads live MCP tools before unblocking the agent so it
resumes with the tools it was just promised. Esc stays live mid-flight
as cancel — the abandoned flow aborts at its next poll and a post-write
cancel rolls the config entry back. Typing while the card is pending
declines it and sends normally (skipClarifyRequest's pattern), and the
request/tool.start rows merge on the server arg so reconnects can't
double-render the card.
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson adbc77eb50 feat(desktop): setup_mcp tool — inline MCP consent card over the clarify-style blocking bridge
New desktop_ui tool: the agent proposes an MCP server (install/enable/
authorize + a one-line reason) and blocks on mcp.setup.request until the
renderer's consent card answers mcp.setup.respond with the outcome
(installed/enabled/authorized/declined/unanswered/error). Same lifecycle
as clarify: 10-min timeout, allow_expired late answers, tool lifecycle
events forced on so the card mounts even with tool progress off. Desktop
prompt hint steers the model to the tool instead of hand-editing config;
every other surface keeps the schema out and is pointed at hermes mcp
install.
2026-08-13 01:06:51 -05:00
Brooklyn Nicholson a9eb7e09d9 feat(desktop): marquee clipped inline row titles on hover
The one-line session row gets the exact treatment the inbox card's title
already has: hovering a truncated title glides the clipped tail into view —
one direction at constant speed, a short hold at each end, then a snap back.
Same armMarquee/disarmMarquee handlers, same CSS, so overflow is measured on
pointerenter, short titles never move, hover state lives in DOM attributes
(no re-render of the memoized row), and reduced motion disables it.
2026-08-13 00:55:33 -05:00
Teknium f84ecd3607 chore: map zhjay@stu.xjtu.edu.cn -> ZHJay for contributor attribution
(cherry picked from commit 06899b54437f7bb36b126d96a1e740afeb34d855)
2026-08-12 22:38:17 -07:00
张豪杰 1535c114c9 test(desktop): cover connection.json owner-only mode end to end
The helpers were tested; nothing proved main.ts called them. Reverting both
call sites and both imports in readDesktopConnectionConfig /
writeDesktopConnectionConfig left the whole suite green (947 passed / 2
skipped, tsc 0, eslint clean, e2e 1 passed 1 skipped) while connection.json
went back to 0644 — the user-visible fix this PR promises was untested.

The e2e spec could not catch it by construction: it asserts the ENCRYPTION
contract with a raw-bytes scan, and safeStorage keeps the token opaque
regardless of the file's mode, so a 0644 file passes that scan every time.
There was no mode assertion anywhere in e2e/.

Adds the missing third contract — unreadable by other local accounts — on all
three paths that can produce the file:

- write: assert the mode of the artifact test 1 already proves the app wrote.
- read, valid file: seed the app's own encrypted connection.json back to 0644
  and assert launch tightens it. Scoped to the MODE only, so it is independent
  of the still-deferred plaintext migration — the fixture's token is already
  ciphertext, so nothing re-encrypts, no #62319 opt-in marker is involved, and
  no rotation guidance is owed.
- read, corrupt file: a truncated file still holds the token bytes and throws
  into the swallowing catch, so it would be the one file never tightened. This
  is the only test that distinguishes the chmod's placement relative to the
  parse.

Also moves the tighten above JSON.parse for exactly that reason, and pins the
cache invariant the placement depends on: the tighten must be a chmod, not a
rewrite, because it sits inside the function whose cache keys on mtimeMs.

Asserted as `mode & 0o077 === 0` rather than `=== 0o600` to avoid a
change-detector, and skipped on win32, where chmod maps to the read-only bit
and the fix deliberately no-ops (ACLs are PR #77527).

Every assertion was mutation-tested: reverting the full wiring fails all three;
reverting only the write path fails only the write test; deleting only the
tighten-on-read fails only the two read tests; moving the tighten below the
parse fails only the corrupt test; making the tighten a rewrite instead of a
chmod fails the mtime assertions. Bundle greps confirmed each mutation reached
dist/electron-main.mjs before the run.

(cherry picked from commit 99cfc16e7c)
2026-08-12 22:38:17 -07:00
张豪杰 7e151bd9d3 fix(desktop): create connection.json owner-only
`connection.json` under the desktop app's Electron `userData` was written with no
file mode, so it landed at the `0644` umask default — while its two
credential-bearing neighbours in the same directory, `desktop-installation.json`
and `native-oauth-tokens.json`, were already `0600`. That file holds the
safeStorage-encrypted gateway token plus the fields that are NOT encrypted: the
gateway URL and the SSH host, user, and key path.

- Route the single write choke point through a helper that creates the file
  owner-only and atomically.
- Tighten an already-existing `0644` file once per launch on the read path, so
  installs that already have one do not stay world-readable until the next save.
- Refuse to act on a path that is a symlink or not owned by the current user,
  matching the guards `desktop-installation.ts` already applies to its sibling.

The symlink guard alone turned out to be insufficient, and that is worth
recording: `writeSecretFileAtomic` tightens its *temp* path, so a symlink planted
at `connection.json.tmp` meant `writeFileSync` followed it, the guard correctly
bailed, and `renameSync` then moved the link onto `connection.json` permanently.
Measured, guard-only vs. as-landed:

    guards only          token leaked: true    config is a symlink: true   755
    guards + temp unlink token leaked: false   config is a symlink: false  600

So the temp path is unlinked before the write.

Issue #77486's headline claim — that a dashboard session token is persisted in
plaintext — does not hold against main. The token has been safeStorage-encrypted
since the desktop app reached mainline in 51c68d4ab, and `encryptDesktopSecret`
aborts with an actionable message rather than degrading to plaintext when
safeStorage is unavailable. The `{ encoding: 'plain', value }` literal does exist
at main.ts:7084, but only on the `persistToken: false` branch, whose sole caller
is the connection-test handler, which never writes. So no mainline path *writes*
a plaintext token. The commits that did contain a plaintext-writing fallback
(d3d177283, d208f2c2c) are not ancestors of main — they live only on
upstream/bb/gui-* and the desktop-pr20059-installers pre-release tag.

At-rest migration of legacy non-safeStorage payloads is deliberately NOT included.
An earlier revision of this branch implemented it and it was removed after review
reproduced two token-loss paths: it force-converts the opt-in plaintext choice
PR #62319 adds (silently reverting the user's decision, then destroying the token
on the next launch without the `--password-store=basic` flag), and it converts a
portable credential into a keychain-bound one with no consent — destroying the
only recoverable copy while not remediating the real exposure, since every
existing backup still holds the plaintext and the true remedy is rotation. It also
persisted raw `parsed`, bypassing `sanitizeConnectionProfiles`. A comment at the
read path records the three preconditions any future attempt needs.

`decryptDesktopSecret`'s non-safeStorage read fallback is untouched — it is what
lets a pre-release or hand-edited config work at all.

Windows still inherits the userData directory ACL rather than an explicit
owner-only one; mode bits are advisory there, so that half is deferred to
PR #77527 rather than growing a second ACL implementation here.

e2e: `at-rest-connection-token.spec.ts` asserts the at-rest contract
implementation-independently — the token's plaintext value (and its base64 form)
must not appear in a raw-bytes scan of any file under userData or HERMES_HOME,
AND the app must still put the exact original token on the wire after a restart,
so a fix that simply drops the token cannot pass. Proven non-vacuous by mutation:
writing `{ encoding: 'plain', value }` still fails the scan while the
file-exists and gateway-URL guards pass. The migration case is a documented
`test.fixme` naming its three blockers.

Electron project 928 -> 924 tests (-9 migration, +5 new guard and
mechanism-isolation). Two of those five exist because reverting either owner-only
mechanism alone initially scored zero failures — they were masking each other, so
either could have been deleted green.

(cherry picked from commit 6e01add657)
2026-08-12 22:38:17 -07:00
brooklyn! 7626105380
Merge pull request #84943 from NousResearch/bb/review-summary-chrome
Self-improvement review row wears the same gold→purple chrome as a memory write
2026-08-13 00:18:44 -05:00
Teknium 6397776fe8 refactor: simplify discord classifier + move attention threshold to config.yaml
- Collapse the duplicated discord LoginFailure/PrivilegedIntentsRequired
  classification (name-match + isinstance blocks repeated the same code/
  message tuples) into a single _is() helper — one message per failure.
- Replace the user-facing HERMES_RECONNECT_ATTENTION_AFTER_SECONDS env var
  with agent.reconnect_attention_after in config.yaml (default 7200, 0
  disables), bridged internally like gateway_timeout. .env is for secrets.
- Use _float_env for robust parsing instead of bare int(os.getenv(...)).
- Document terminal classification + needs_attention escalation in
  website/docs/user-guide/configuration.md.
2026-08-12 22:16:12 -07:00
Shannon Sands 91bc822330 fix(gateway): classify terminal adapter connect failures + escalate long-lived retry loops (OOF-156)
Fleet triage after the 2026-08-11 storm resolution found agents whose sole
platform had been silently 'retrying' for weeks: revoked Telegram tokens,
Discord privileged-intent rejections, and Photon sidecars that can never
start were all funnelled into the indefinite reconnect queue with no owner
signal (OOF-151/152/153, epic OOF-156).

Two-part fix:

1. Per-adapter classification — by exception TYPE only, never message text:
   - telegram: InvalidToken/Forbidden -> telegram_auth_error, retryable=False
     (new _looks_like_auth_error, mirrors _looks_like_network_error)
   - discord: LoginFailure -> discord_auth_error, PrivilegedIntentsRequired
     -> discord_intents_required (both retryable=False); every other path now
     sets an explicit code (previously the generic branch set NO fatal info,
     which the gateway read as 'probably transient')
   - photon: new typed PhotonSidecarStartupError; deps-install failure ->
     SIDECAR_DEPS_MISSING and missing node binary -> SIDECAR_NODE_MISSING
     (retryable=False); ambiguous startup crashes stay retryable
   - email: IMAP/SMTP failures now always set a fatal code;
     SMTPAuthenticationError -> email_auth_error, retryable=False (IMAP4.error
     is type-ambiguous between bad creds and transient NOs, so IMAP stays
     retryable)

2. Gateway escalation — platforms continuously in the reconnect queue past
   HERMES_RECONNECT_ATTENTION_AFTER_SECONDS (default 2h, 0 disables) get
   needs_attention=true + retrying_since stamped into runtime status, once
   per episode, cleared on successful reconnect.

Deliberately NOT a circuit breaker: retries never stop. The auto-pause
mechanism was removed for good reason (transient DNS outages left bots
silently dead); this preserves that and only adds visibility. No new
platform_state enum values — NAS's status schema is strict — only additive
fields.

Unknown exception types always stay retryable: a false terminal recreates
the silently-dead-bot problem, and the escalation path covers
misclassified permanent failures.
2026-08-12 22:16:12 -07:00
Brooklyn Nicholson 5af2c2ff52 fix(desktop): messages typed during approval/sudo/secret prompts run as the next turn
Typing while the turn was parked on a blocking prompt routed the text
through steer (session.redirect), which sat undelivered behind the
blocked tool batch — nothing rendered, and stopping the turn to force it
through resolved the prompt to empty and ended the turn as the literal
"Operation interrupted." row, eating the message.

Clarify already had a carve-out (typing skips the question and steers)
because a real message IS an answer to a clarify. Approval/sudo/secret
have no such answer path, so the busy submit now queues the words as the
next turn instead: the prompt stays answerable, the queue drains on
settle, and the busy button advertises queue rather than steer while one
is pending. Slash commands still execute inline, and another session's
prompt never affects this one.
2026-08-13 00:13:36 -05:00
Hermes Agent 04846b2120 ci: retrigger after 0-job workflow startup failure 2026-08-12 22:11:15 -07:00
Teknium 1706502aa7
feat(computer_use): spill full element tree to a cache file and report numeric bounds_scale (#85047) 2026-08-12 21:53:11 -07:00
Teknium 4ea2a0e546 Revert "Inspired by Perplexity Computer: Model Council mode for Mixture of Agents"
This reverts commit 8d9e18d40b.
2026-08-12 21:50:35 -07:00
Teknium 825a9753c1
fix(computer_use): resolve cua-driver at its official Windows installer path (#85038) 2026-08-12 21:30:42 -07:00
Teknium 6c9d6d9d5b
fix(computer_use): keep capture responses inside the tool-result budget and surface coordinate-space + typed-page hints (#85037) 2026-08-12 21:28:34 -07:00
hermes-seaeye[bot] 08606fc231
fmt(js): `npm run fix` on merge (#85024)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-13 04:08:31 +00:00
Brooklyn Nicholson 4d9202b963 feat(desktop): right-click the shell chrome for window-level actions
Right-clicking anywhere the app owns no menu of its own — the titlebar
gutter, an empty pane body, the sidebar background — did nothing at all.
Electron's native handler bails on non-editable, non-selected content by
design, so those surfaces had no menu to fall back to.

Wrap the shell in a fallback context menu carrying the verbs that belong
to the window rather than to a row: new session, new window, command
palette, toggle the status bar, settings, update Hermes. Every row reuses
the store action and the copy its Cmd+K twin already uses, so the two
can't drift.

A guard on an inner element keeps it a fallback: a right-click that lands
inside a surface with its own context menu, on an editable, or on a live
selection stops propagating before Radix's trigger sees it, leaving that
surface's menu — or Electron's native edit menu — in charge.
2026-08-12 22:55:37 -05:00
Brooklyn Nicholson 071d27d1c3 feat(desktop): paste a PR review comment as structured composer context
A pasted GitHub PR comment deep link (#discussion_r… / #issuecomment-…)
now lands as a typed review attachment instead of a bare url chip. The
card attaches optimistically and resolves through gh in the background —
author, file:line anchor, body, and the diff hunk — expanding at send
into an anchored fenced block, so "address this" carries exactly what
"this" is. When gh can't answer (offline, unauthenticated, foreign repo,
remote gateway) the card downgrades to the plain url ref and nothing is
lost.
2026-08-12 22:48:00 -05:00