Commit graph

16659 commits

Author SHA1 Message Date
Enough1122
11e76f4e01 fix(state): probe FTS5 read path in _db_opens_cleanly so partial index corruption is detected (#66724)
`hermes sessions repair --check-only` opens cleanly on state.db files
with partial FTS5 index corruption — base tables read fine, the rolled-back
write probe from #50502 succeeds, and `PRAGMA integrity_check` returns
"ok". But every session_search / /resume title resolution / feature
backed by MATCH / snippet / rank queries errors out with
`database disk image is malformed` because internal shadow-table segments
are bad. The official repair tool then gives false confidence.

Add a representative FTS5 read probe against both `messages_fts` and
`messages_fts_trigram` (the latter backs title resolution). Empty MATCH
strings are accepted by every FTS5 index without requiring populated
content, so the probe is safe on a freshly-init'd DB; missing-table /
missing-column errors fall through to the existing "not yet a populated
DB" branch, matching the write-probe's behaviour. Any other OperationalError
is surfaced as the check reason, which sends `hermes sessions repair` to
its existing FTS 'rebuild' path (repair_state_db_schema, line 616).

Single-file change in hermes_state.py::_db_opens_cleanly. No public API
change. No new imports. Fixes #66724.
2026-07-21 12:40:48 -07:00
Teknium
5e4529c02f chore(contributors): map emails for PRs #66906, #66420, #63398 salvage 2026-07-21 12:40:48 -07:00
Teknium
ab76cf836f fix(gateway): reap the replaced gateway's orphaned children on POSIX
Builds on jbbottoms's #65178 takeover fix (cherry-picked as the previous
commit). Windows --replace already tree-kills via taskkill /T, but the
POSIX paths signalled only the recorded gateway PID — adapter
subprocesses that outlived their parent kept holding scoped token locks
and blocked the replacement gateway.

- gateway/status.py: _snapshot_gateway_children() captures the old
  gateway's descendants (psutil, recursive) while it is still alive;
  reap_gateway_children() SIGTERMs verified orphans after the main PID
  is confirmed dead, waits bounded, SIGKILLs survivors. Identity-aware
  (psutil is_running is PID+create-time), skips zombies and children
  whose ppid still equals the old gateway (parent actually alive), and
  never raises — best-effort with debug/info logging only.
- take_over_scoped_lock_holder() snapshots before terminating and reaps
  only on a confirmed successful handoff.
- gateway/run.py: start_gateway --replace snapshots before SIGTERM and
  reaps after the old PID is confirmed gone, mirroring taskkill /T.
- tests/gateway/test_replace_child_reap.py: reap/skip/never-raise unit
  coverage plus end-to-end --replace ordering (snapshot → terminate →
  reap) and the no---replace path never touching the old process.
2026-07-21 12:40:21 -07:00
Teknium
50fdf136e8 chore(contributors): map jaretbottoms@gmail.com -> jbbottoms (PR #65178 salvage) 2026-07-21 12:40:21 -07:00
Jaret Bottoms
155fdd59f8 fix(gateway): take over live platform-lock token holders once
When --replace misses a cross-HERMES_HOME Telegram token holder, platform
connect used to retry forever. Terminate a verified gateway holder once
(with the takeover marker) and re-acquire the scoped lock (#65176).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 12:40:21 -07:00
Cossackx
ad7b4c7d2e test(gateway): cover stale gateway_state.json detection (TTL + PID liveness)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 12:39:59 -07:00
Cossackx
e54723a754 fix(gateway): detect stale gateway_state.json in gateway status (TTL + PID liveness)
Verified: applies cleanly and the patched module compiles. Tests are
described in the PR body (not bundled in this commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 12:39:59 -07:00
Teknium
3fc0cfb902 fix(gateway): make stale scoped-lock removal atomic via tombstone rename
Replace the unlink()+O_EXCL sequence in acquire_scoped_lock with an
atomic os.replace() of the stale lock to a <lock>.stale tombstone
followed by the existing O_EXCL create. With plain unlink(), two racing
starters could both judge the lock stale and the second unlink() would
silently delete the first racer's freshly-created lock — both would then
'win'. os.replace() guarantees exactly one racer claims the stale file;
the loser gets FileNotFoundError and falls through to O_EXCL, which
admits at most one winner. Tombstones are cleaned up immediately;
behavior is otherwise identical.
2026-07-21 12:39:12 -07:00
Teknium
35fb2e428a fix(gateway): guard acquire_gateway_runtime_lock against root-owned lock PermissionError
Widen the PermissionError handling from is_gateway_runtime_lock_active
(#42689) to the sibling open() in acquire_gateway_runtime_lock: a stale
root-owned gateway.lock left by a launchd Background session previously
crashed the acquiring process. Unlink the stale file and retry once; if
the unlink or retry fails, return False cleanly instead of raising.
2026-07-21 12:39:12 -07:00
liuhao1024
6f50c5607b fix(gateway): handle PermissionError on stale root-owned lock file
When the macOS launchd service runs in a Background session, the gateway
process spawns as root and creates a root-owned gateway.lock. On restart
as the normal user, open() on that file raises PermissionError, crashing
the gateway immediately and entering a launchd crash loop.

Catch PermissionError in is_gateway_runtime_lock_active(), remove the
stale lock file, and return False so the new process can start cleanly.

Fixes #42685
2026-07-21 12:39:12 -07:00
liuhao1024
2b72e06662 fix(gateway): detect stale lock when macOS psutil returns valid start_time for recycled PID
On macOS, the lock record's start_time is None (no /proc at creation),
but psutil.Process(recycled_pid).create_time() returns a valid float
for the unrelated process that now owns the PID. The old condition
required both sides to be None before falling back to cmdline checking,
so the recycled PID was never detected as stale.

Change the fallback condition from AND to OR: when either side's
start_time is missing, fall back to cmdline-based gateway detection.

Fixes #53763
2026-07-21 12:39:12 -07:00
hermes-seaeye[bot]
0008868422
fmt(js): npm run fix on merge (#68867)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-21 19:13:24 +00:00
alelpoan
0c33db0564
fix(desktop): wrap missing sidebar icon-button tooltips (#67500)
* fix(desktop): wrap sidebar icon buttons in Tip tooltips

Several icon-only buttons in the sidebar (header actions, workspace
menu, project menu, session actions, load-more) had aria-label but
no visual tooltip on hover. Wrap them in the existing <Tip> component,
matching the pattern already used elsewhere (e.g. ProfilePill).

No behavioral changes -- purely wraps existing buttons.

Adds vitest coverage asserting the Tip wrapper (data-slot=tooltip-trigger)
for 6 of 7 files; index.tsx is a 1500+ line top-level page component and
was verified manually via screenshots instead.

* fix(desktop): satisfy consistent-type-imports lint rule in project-dialog test

* test(desktop): update session-row mocks for restored sessionColorById

* fix(desktop): compose Tip around the real trigger instead of inside it

Tip was being placed as SessionActionsMenu's/PlatformAvatar's DIRECT child,
which asChild then cloned instead of the actual button/span. Neither Tip nor
PlatformAvatar forwarded the injected onClick/ref, so both silently dropped
the wiring:

- session-actions-menu.tsx: Tip now wraps DropdownMenuTrigger internally
  (new 	ooltip prop) instead of the caller wrapping its children in Tip.
- platform-icon.tsx: PlatformAvatar now forwards ref and spreads rest props
  onto its span so a wrapping Tip's trigger actually attaches.
- session-row.tsx: updated call site to use the new tooltip prop.
- Added session-actions-menu.test.tsx exercising the real DropdownMenu open
  behavior end-to-end (no Tip/Dropdown mocks).
- session-row.test.tsx no longer mocks PlatformAvatar's behavior; it now
  exercises the real (fixed) component for the handoff-avatar tooltip.

* fix(desktop): compose Tip outside PopoverAnchor in ProjectMenu (#67500)

* test(desktop): update session-row test for the tooltip-prop composition (cbbbeb2fd)

* fix(desktop): satisfy consistent-type-imports in session-row.test.tsx mocks

* chore: retrigger CI

* test(desktop): stop mocking PlatformAvatar's behavior (#67500, third pass)

The mock was re-introduced by a prior edit that fixed an unrelated lint
error, silently undoing the earlier fix where this test started exercising
the real (forwardRef) PlatformAvatar. Removed the mock; updated the two
handoff-avatar tests to query the real component's rendered span instead of
text content, since it renders a brand SVG icon for known platforms rather
than the platform name as text.
2026-07-21 15:05:56 -04:00
Teknium
9cc475cc58 test(approval): cover allow_session tiers in Matrix reaction seeding and gateway payload
Update the Matrix reaction-seeding contract to the four-reaction default
(once/session/always/deny), add tirith-tier (session without always) and
no-session-tier cases, and assert allow_session=True in the tirith
gateway payload.
2026-07-21 12:04:47 -07:00
Teknium
02d8cbadec fix(approval): honor allow_session across all button adapters
Widen the allow_session tier from Matrix to every adapter the gateway
notifies: Telegram, Discord, Slack, Feishu, and Teams gate their Session
button on it; WhatsApp Cloud and qqbot accept the kwarg (no session tier
in their button sets). Also thread allow_session through the plugin-
escalation gate, the execute_code guard payload, and the plain-text
fallback so every notify path carries the same capability flags.
2026-07-21 12:04:47 -07:00
Joshua
a3297bd232 fix(approval): restore session approval for Tirith-flagged commands
Adds an allow_session flag to the gateway approval payload so adapters
can render the session tier independently of the permanent tier. Matrix
gains a session reaction (🌀) and a reaction legend; pure-tirith prompts
now offer once/session/deny instead of collapsing to once/deny.

Salvaged from PR #67312, adapted to the allow_permanent semantics that
landed in #68597 (Always offered when any dangerous-pattern warning is
persistable; pure-tirith prompts stay session-max).
2026-07-21 12:04:47 -07:00
ethernet
6b54582438
fix: tool_calls double-encoding on import (#68856)
* nix: add `cage` to devShell

* test(desktop): add pre-filled sessions support

Exports createSandbox, writeMockProviderConfig, writeEnvFile,
buildAppEnv, findElectron, and launchDesktop from fixtures.ts so
specs can compose their own seeded-backend fixtures without duplicating
the sandbox/config/launch logic.

* test(desktop): auto-fail e2e tests on error banner

Adds a shared test fixture (e2e/test.ts) that wraps @playwright/test's
page with an error-banner guard. When any [role="alert"] element
(error notification toast) appears in the DOM during a test, the test
fails with the error message text.

The guard uses:
- A MutationObserver (injected via addInitScript) that watches for
  [role="alert"] elements appearing at any point during the test
- A final DOM scan in afterEach for alerts still visible at teardown
- Deduplication so the same error text only fires once

All existing e2e specs updated to import { test, expect } from './test'
instead of '@playwright/test'. No per-spec setup needed — the guard is
auto-installed on every page via the extended fixture.

This catches issues like the "resume failed" error banner that can
appear during session loading — previously the test would pass while
an error toast was silently visible on screen.

* fix(state): parse tool_calls JSON string before re-serializing

_insert_message_rows and append_message both do json.dumps(tool_calls)
to serialize the field for SQLite storage. But when tool_calls arrives
as a JSON string (from import_sessions / export_session, which store it
as TEXT), json.dumps double-encodes it — wrapping the already-serialized
string in quotes and escaping the inner quotes.

When _rows_to_conversation later does json.loads(row['tool_calls']),
the double-encoded string parses back to a plain string (not a list).
_history_to_messages then iterates this string character-by-character,
calling tc.get('function', {}) on each char — 'str' object has no
attribute 'get'.

This was a pre-existing bug (on main), but only triggered by the
import_sessions path (the live agent always passes tool_calls as a
Python list). The e2e error-banner guard caught it via the 'Resume
failed' notification toast.

Fix: in both append_message and _insert_message_rows, parse tool_calls
with json.loads first if it's a string, then re-serialize.

* fix(desktop): exempt boot-failure from error guard

- boot-failure: add allowErrorBanners() beforeEach — these tests
  deliberately trigger boot errors, so error toasts are expected
- test.ts: export allowErrorBanners() opt-out + reset flag in afterEach
2026-07-21 18:53:05 +00:00
ethernet
4dd535c8ea
fix(ci): route critical supply-chain findings through review gate (#68833)
Let the scanner report critical findings without failing. The review-label
gate owns the action-required status and blocking result, allowing the
ci-reviewed label rerun to clear both CI and the PR comment.
2026-07-21 18:51:26 +00:00
Siddharth Balyan
a88512b114
fix(desktop): drop the decorative top-up credits bar (#68649)
The bar rendered full-or-empty (value 1|0) because top-ups have no
denominator — the wire carries only the current balance and the pool is
open-ended, so a fill fraction is fiction. Show the amount alone;
subscription credits and the monthly cap keep their bars (real
denominators).
2026-07-21 22:51:04 +05:30
Siddharth Balyan
5ed7137fc6
feat(billing): plan chips and rows deep-link their tier (#68666) 2026-07-21 22:42:14 +05:30
brooklyn!
11ae6bf0e3
Merge pull request #68725 from SHL0MS/fix/desktop-stop-parks-queue
Some checks failed
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
Build Skills Index / build-index (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
fix(desktop): Stop parks the queue instead of firing the next queued prompt
2026-07-21 10:54:58 -05:00
jonny
73c8d40464
Merge pull request #68130 from NousResearch/feat/desktop-remote-ssh-current
feat(desktop): SSH remote-backend connection mode
2026-07-21 17:30:43 +02:00
hermes-seaeye[bot]
d604141d09
fmt(js): npm run fix on merge (#68681)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-21 14:28:28 +00:00
xxxigm
d9dae17e97
fix(desktop): ⌘W closes visible file tab when preview selection is stale (#68639)
* fix(desktop): make ⌘W close visible file tab on stale preview selection

When the live preview target is gone but $rightRailActiveTabId still points
at preview, file tabs remain on screen while ⌘W fell through to a workspace
no-op. Close the visible file tab instead.

* test(desktop): cover ⌘W close for file tabs and ghost preview selection

Lock the happy path and the stale-preview regression so ⌘W keeps closing
the file tab the rail is actually showing.
2026-07-21 10:17:32 -04:00
Austin Pickett
d355e0e71d
feat(desktop): configure repository discovery (supersedes #67630) (#68642)
* feat(desktop): configure repository discovery

* fix(config): preserve additive default migration

* fix(desktop): stabilize session-actions-menu gateway mock for repo-scan subscribe

projects.ts now runs $gateway.subscribe(syncReposScanning) at module load, and
nanostores fires the subscriber synchronously. session-actions-menu.test.ts
reaches projects.ts transitively via the session store but mocked
@/store/gateway without $gateway, crashing the whole desktop vitest suite
("No \ export is defined"). Simply adding $gateway: atom(null) exposed a
second issue: the synchronous subscriber calls the mock's activeGateway()
during the transitive import, before the module-level const initializes (TDZ).

Hoist the mock fns via vi.hoisted() so activeGateway is defined before the
hoisted vi.mock factory runs, and add $gateway: atom(null) to the mock. Mirrors
the self-contained mock pattern already used in projects.test.ts. Also maps the
PR author's commit email for attribution.

Supersedes #67630; incorporates review feedback from that PR.

Co-authored-by: Rudimar Ronsoni <rudimar@outlook.com>

---------

Co-authored-by: Rudimar Ronsoni <rudimar@outlook.com>
Co-authored-by: Austin Pickett <austinpickett@users.noreply.github.com>
2026-07-21 10:09:31 -04:00
Teknium
d3b0e61429
feat(secrets): one-command token rotation + actionable startup errors for all secret sources (#68605)
* feat(secrets): one-command token rotation + actionable startup errors for all secret sources

When a Bitwarden machine-account token expired, users saw a raw Rust
error dump (invalid_client + Location: + backtrace hints) and the only
fix was manually editing .env or re-running the whole setup wizard.

- New `hermes secrets bitwarden token` / `hermes secrets onepassword
  token`: paste a new token (masked prompt or flag), the command probes
  the backend BEFORE persisting — a rejected token changes nothing; a
  good one is written to .env and the fetch caches are cleared.
- New optional SecretSource.remediation(kind, cfg) hook: startup
  warnings now print a '→ Run `hermes secrets <name> token`…' fix-it
  line after any fetch error, for bundled AND plugin sources (generic
  per-ErrorKind defaults in the ABC).
- bws stderr is summarized to its cause line (Location:/backtrace noise
  dropped) and invalid_client/invalid_grant/400 identity rejects are
  now classified AUTH_FAILED (was INTERNAL) with a plain-English
  explanation naming the token env var.
- op whoami probe accepts a candidate token so rotation validates the
  NEW credential, not the ambient one.

Additive hook with defaults — no SECRET_SOURCE_API_VERSION bump.

* docs: fix MDX parse error in secret-source-plugin hook table

Escaped backticks around a <name> placeholder made MDX parse it as an
unclosed JSX tag, breaking the docs-site build.  Use a plain code span
instead.
2026-07-21 06:41:04 -07:00
brooklyn!
ed3c39108b
Merge pull request #68331 from helix4u/fix/desktop-session-titlebar 2026-07-21 08:00:45 -05:00
Teknium
a31a31826c
fix(approval): raise gateway approval timeout to 300s, honest stale-tap UX, offer Always on mixed prompts (#68597)
Three related messaging-approval fixes:

1. approvals.timeout default 60 -> 300. PR #63501 collapsed the gateway
   wait onto the canonical approvals.timeout (previously
   gateway_timeout=300), silently shrinking messaging approval windows
   to 60s. Push-notification approvals routinely arrive later than a
   minute; taps landed after the wait had already failed closed.

2. Stale-tap honesty: adapters resolved the approval AFTER rendering
   '<checkmark> Approved by <user>' (Telegram/Discord/Slack), or ignored a zero
   resolve count (WhatsApp Cloud/Feishu). A tap on an expired prompt
   claimed approval while the command had already been denied. All
   button paths now resolve first and render 'Approval expired -
   command was not run' when nothing was waiting.

3. Mixed-warning prompts (dangerous pattern + tirith finding) now offer
   Always: the persistence layer already permanently allowlists the
   pattern key and downgrades the tirith key to session scope, but the
   UI hid Always whenever ANY tirith warning was present. Pure-tirith
   prompts still withhold Always (content findings are session-max by
   design), and Smart-DENY overrides remain once-only.
2026-07-21 05:41:41 -07:00
Teknium
afb7bf6a5a
feat(skills): bundle docx, xlsx, and pdf office skills; refresh powerpoint (#68595)
Non-technical users asking for Word docs, spreadsheets, or PDF work had
no bundled skill coverage — docx/xlsx creation required discovering and
installing hub skills, and PDF manipulation had no skill at all beyond
OCR extraction and nano-pdf edits.

- skills/productivity/docx: create (docx-js), edit (unzip -> XML -> zip),
  tracked changes, comments, validation. Adapted from anthropics/skills.
- skills/productivity/xlsx: openpyxl creation/editing, mandatory
  LibreOffice recalc gate, formula-compatibility rules, financial-model
  conventions. Points at optional excel-author for finance-grade work.
- skills/productivity/pdf: merge/split/rotate/watermark/encrypt, form
  filling (AcroForm + flat overlay scripts), text/table extraction,
  reportlab creation, forms.md + reference.md companions.
- skills/productivity/powerpoint: synced to current upstream pptx skill —
  richer pptxgenjs corruption footguns, template workflow, validate.py +
  validators + thumbnail.py, font-substitution QA guidance; drops the
  stale pack.py/editing.md/pptxgenjs.md workflow files.
- Cross-linked ocr-and-documents, nano-pdf, excel-author via
  related_skills so each office skill routes to its siblings.
- deliverable-mode docs mention the new skills; regenerated per-skill
  docs pages, catalogs, and sidebar.
- tests/skills/test_office_document_skills.py: frontmatter contracts,
  referenced-script existence, schema-map integrity, cross-link
  resolution, script compilation.

E2E validated: docx create->render->edit->validate, xlsx recalc
(SUM + _xlfn.TEXTJOIN evaluate correctly), pdf create->merge->extract,
pptx generate->validate->thumbnail.
2026-07-21 05:39:23 -07:00
Teknium
03841c9658
fix(tools): make the tool-search context gate provider-aware (#68589)
_resolve_active_context_length() called get_model_context_length() with the
model id alone, so provider-enforced windows (e.g. Codex OAuth's 272K for
gpt-5.x vs the direct API's 1.05M) never reached the tool-search activation
gate — it sized against generic metadata for the same slug.

Resolve the runtime provider for the configured model and pass provider,
base_url, and api_key through. If credential resolution fails (offline, no
keys), degrade to a provider+base_url-only lookup so the static
provider-aware fallbacks still apply; explicit model.context_length keeps
short-circuiting as before (#46620). Gap flagged during review of #16735.
2026-07-21 05:27:23 -07:00
TARS
b49b1e5b93 test(codex): cover ChatGPT-Account-Id header on /models probe
Add regression tests locking in the new behavior: a JWT carrying a
chatgpt_account_id claim causes the probe to send ChatGPT-Account-Id,
while a malformed token omits the header instead of crashing.
2026-07-21 05:27:13 -07:00
TARS
c44c2fbb0b fix(codex): send ChatGPT-Account-Id on /models probes
The Codex backend returns the per-account model catalog only when the
ChatGPT-Account-Id header is present. Without it, GET /backend-api/codex/models
responds 200 OK with {"models":[]} and the picker silently degrades to the
hardcoded fallback list — which is stale or wrong for the active plan
(no GPT-5.6 family, wrong context windows).

This was the upstream bug behind slow first responses and HTTP 520/120s SSE
hangs: Hermes was sending invalid slugs because the probe never saw them in
the catalog, and Codex's request builder also depends on the same JWT claim
that's now being threaded through both probe paths.

Fixes the probe-side paths in hermes_cli/codex_models.py and
agent/model_metadata.py by extracting chatgpt_account_id from the OAuth JWT
(mirroring the request-side logic already in auxiliary_client.py) and sending
it as a header.

Verified live:
- _fetch_models_from_api now returns the 10-model catalog (gpt-5.6-sol,
  gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gpt-5.4-mini,
  gpt-5.3-codex-spark, 3x -pro variants) instead of [].
- _fetch_codex_oauth_context_lengths resolves all 8 account models to 272K
  context (matches direct API probes of the same account).
- end-to-end: hermes chat -m gpt-5.6-sol -q 'Reply with one word: pong'
  returns 'pong' cleanly via the openai-codex route.

Same class of bug as PR #64760.
2026-07-21 05:27:13 -07:00
Teknium
64702f8f91 fix(compression): report live-resolved Codex window in the autoraise notice
The autoraise banner hardcoded '272K' for the gpt-5.4/5.5/5.6 family, but
the Codex /models catalog is authoritative and shifts server-side (gpt-5.6
served 372K during July 9-18, 2026 before OpenAI rolled it back). Pass the
compressor's live-resolved context_length through so the notice reports the
window the session actually got; the static 272K/128K text remains as the
fallback when no resolved value is available.
2026-07-21 04:29:34 -07:00
sbe27
0c0ec18d6f test(context): cover Codex context rollback 2026-07-21 04:29:34 -07:00
sbe27
60afc290a8 fix(context): scope Codex catalogue cache by credential 2026-07-21 04:29:34 -07:00
sbe27
9a34cc91a5 test(context): document Codex cache persistence coverage 2026-07-21 04:29:34 -07:00
sbe27
8a0701ca48 fix(context): revalidate Codex OAuth context windows 2026-07-21 04:29:34 -07:00
Teknium
279be8211d Revert "fix(agent): circuit-break AttributeError from commit-splice and detect code skew"
This reverts commit 3a9b9d65d5.
2026-07-21 03:26:17 -07:00
Gille
940fd969ce fix(desktop): preserve dragging with empty titlebar slots 2026-07-21 03:30:50 -06:00
yoniebans
25f4ba7b3c Merge remote-tracking branch 'origin/main' into feat/desktop-remote-ssh-current
# Conflicts:
#	apps/desktop/electron/connection-config.test.ts
#	apps/desktop/electron/connection-config.ts
#	apps/desktop/electron/main.ts
2026-07-21 10:16:01 +02:00
yoniebans
b14be881b9 build: declare pywin32 as a direct win32 dependency
hermes_cli/windows_ssh_runtime.py imports win32security/win32file/etc.
directly but pywin32 only arrived transitively via concurrent-log-handler
-> portalocker. Declare it with a sys_platform gate so the Windows SSH
runtime doesn't depend on the logging dep chain. Review follow-up on
PR #68130.
2026-07-21 10:02:28 +02:00
Ben Barclay
f4df260f26
fix(relay): attach metadata.user_id on guild replies for egress fallback (#68320)
The relay adapter re-attaches an egress discriminator on outbound replies
so the connector can resolve the owning tenant. It captured scope_id for
scoped (guild) messages and user_id for DMs, but as MUTUALLY EXCLUSIVE:
a scoped inbound hit an early return, so the author's user_id was never
recorded, and _with_scope only attached user_id when there was no
scope_id. Guild replies therefore went out with scope_id only.

That's fine while the guild has a provision-time route row. But a MANAGED
Discord agent joins guilds dynamically (the shared bot is added to /
removed from servers at runtime), and GATEWAY_RELAY_ROUTE_KEYS — the only
thing that writes guild route rows — is a self-hosted, static field never
stamped for managed agents. So their guild has no route row, the
connector's guild-route lookup misses, and with no user_id on the frame
there's nothing to fall back to → every guild reply is declined
"discord egress declined: target not routed to an onboarded tenant"
even though INBOUND resolved the same guild fine (via the author-first
SharedSocketRouter.targets() fallback).

Fix: capture the authentic author user_id for EVERY inbound (DM and
scoped alike) and re-attach it on the outbound reply alongside scope_id.
The connector consults it only on a route/scope miss, so carrying both
never overrides routing-table resolution. This is the gateway half of the
paired gateway-gateway change (makeDiscordTenantOf guild-route-miss
author-binding fallback); together they make guild replies resolve the
same observed-author way inbound already does.

Tests (tests/gateway/relay/test_relay_adapter.py): a guild reply now
carries both scope_id AND user_id; a scoped inbound with no author still
yields scope_id only (never invents one). Verified fail-without /
pass-with.
2026-07-21 17:04:34 +10:00
hermes-seaeye[bot]
0155c09374
fmt(js): npm run fix on merge (#68462)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-21 06:57:41 +00:00
Siddharth Balyan
94c944363c
feat(tui): show the plan catalog in /subscription on Free (#68357)
* feat(tui): show the plan catalog in /subscription on Free

The server returns the tier list even with no subscription, but the
overlay hid the picker behind can_change_plan && !isFree, so a Free
account got only "Start a subscription" with no idea what the plans
cost. Now:

- Overview on Free offers "Choose a plan" whenever the catalog has
  enabled paid tiers.
- The picker on Free lists each plan as name · price · monthly credits
  (no upgrade/downgrade hints — there is nothing to move from), and
  picking one opens the portal, where starting a subscription actually
  happens (card capture + checkout live there; the upgrade RPC requires
  an existing subscription).
- Paid-plan behavior (preview → confirm → apply) is unchanged.

* refactor(tui): compute the picker row suffix once

Review feedback: the isFree fork duplicated the label template and run
handler; only the suffix differs.

* fix(tui): arm the busy guard before the Free portal handoff

Adversarial review: the Free branch returned before setting busyRef, so
a double-Enter could open the portal twice; and the picker narrated a
handoff that openManageLink already narrates (duplicate on success,
contradictory on failure). Guard first, let the helper do the talking.

* fix(tui): monthly credits are dollars — label them as such

The Free picker showed "1000 credits/mo" for what is $1,000 of monthly
credit — render "$1,000 credits/mo" (grouped, dollar-signed).

* feat(tui): render the Free-plan catalog inline in the /subscription overview

Sid ruling: the upsell belongs where the user already is — no
intermediate "Choose a plan" hop. On Free the overview lists each paid
plan (name · $/mo · $credits/mo) as a pickable row; picking opens the
portal (openManageLink narrates). The generic "Start a subscription"
row survives only when the catalog is empty. The picker reverts to its
original change-only form (Free never reaches it).

* feat(desktop): tier catalog chips on the Subscription row

Desktop parity with the TUI inline catalog (Sid ruling): accounts that
can act see the plans where they already are — Free gets the upsell
list (every chip opens the portal), a subscriber sees all tiers with
the current one marked inert. Members and team contexts see no chips.
Chips learn an optional url (portal handoff) in the shared row model.

* chore(tui): fixture harness mirrors the live tier catalog

The dev screenshot fixtures showed invented plans ($50 Super / $99
Ultra, "1,000 credits"); align with the real catalog ($20/$100/$200
with $22/$110/$220 monthly credits) so fixture renders cannot be
mistaken for product truth. The overlay itself always reads tiers from
the subscription API.

* chore: trim narration comments
2026-07-21 12:20:59 +05:30
Siddharth Balyan
b0da653ac8
fix(billing): rename user-facing "terminal billing" copy to Remote Spending (#68355)
* fix(billing): rename user-facing "terminal billing" copy to Remote Spending

The capability was renamed Remote Spending on the portal (consent CTA:
"Allow Remote Spending"; per-terminal states Granted/Stopped), but the
terminal, desktop, and docs still said "terminal billing" everywhere.

- Feature name: Remote Spending in titles/labels, lowercase mid-sentence.
- Step-up action verb is now "allow", matching the portal consent CTA.
- Kill-switch-off recovery copy points at the actual control ("a billing
  admin can turn it on from the portal's Hermes Agent page") instead of
  the dead-end "manage it on the portal".
- Per-terminal revoke copy uses the portal vocabulary ("stopped").
- Wire identifiers (cli_billing_enabled, cli_billing_disabled, ...) are
  unchanged; copy, comments, docs, and test expectations only.

* fix(billing): correct the post-step-up denial diagnosis + finish the desktop rename

Adversarial review findings: (1) a repeated insufficient_scope after a
successful step-up is a per-terminal authorization failure, but the copy
blamed the org kill-switch and pointed at the wrong recovery control —
now: "Remote Spending still isn't active for this terminal — the
authorization didn't take. Retry, or make this change on the portal."
(2) the desktop step-up flow started in Remote Spending vocabulary but
finished in "billing management access" — renamed both end states.
(3) prettier formatting on the touched files (matches the post-merge
fmt bot).
2026-07-21 12:20:25 +05:30
PRATHAMESH75
7a8852ddcb fix(tests): make the live-system-guard canary fail closed
tests/test_live_system_guard_self_test.py executes real kill primitives
(os.kill(-1, SIGTERM), os.killpg, pkill -f python) and depends entirely on
the autouse _live_system_guard fixture in tests/conftest.py to intercept
them. That makes the canary fail-OPEN: in any collection context where the
file is present but its home conftest is not — a published sdist that ships
tests/ but not tests/conftest.py, a tree assembled by copying test*.py (that
glob does not match conftest.py), pytest --noconftest, or a foreign rootdir —
the primitives fire for real, and os.kill(-1, SIGTERM) SIGTERMs every process
the invoking user owns (a full desktop-session kill was reported in the field).

Add an autouse fixture that refuses to run any canary test unless the guard is
provably active. The one thing the canary can detect about its own safety is
that the guard monkeypatches os.kill with a plain Python function, whereas the
unguarded primitive is a C builtin — so the probe keys off that. Tests marked
@pytest.mark.live_system_guard_bypass still opt out, matching the guard's own
bypass contract (e.g. test_bypass_marker_disables_guard). With the guard loaded
every canary test behaves exactly as before; without it each test refuses at
setup with zero side effects.

Fixes #68311
2026-07-21 12:15:46 +05:30
kshitijk4poor
087732c8c6 fix(telegram): widen fatal handoff to heartbeat watchdog path
The wedged-recovery heartbeat watchdog (line 2526) calls
_notify_fatal_error() directly from the heartbeat task. disconnect()
cancels _polling_heartbeat_task unconditionally (no current_task guard,
unlike _polling_error_task). Same bug class as #68406: the child
disconnect cancels the heartbeat parent before the runner can queue
reconnect.

Widen _handoff_polling_fatal_error() to also clear
_polling_heartbeat_task when it is the current task, and route the
heartbeat watchdog call site through the handoff helper.

Co-authored-by: Imgaojp <6065749+Imgaojp@users.noreply.github.com>
2026-07-21 12:11:52 +05:30
Imgaojp
1ba0e873ff fix(telegram): preserve fatal recovery handoff
Release the current polling-recovery task's ownership before invoking
the fatal-error handler. The runner bounds adapter cleanup in a child
task; disconnect() cancels the tracked polling-recovery task, so
retaining the current notifier in _polling_error_task would cancel the
fatal callback before the runner can finish its reconnect-queue or
shutdown decision.

The new _handoff_polling_fatal_error() helper clears
_polling_error_task only when it is the current notifier. Other
recovery tasks remain tracked and are still cancelled and awaited
during teardown.

Covers both network retry exhaustion and polling-conflict exhaustion.
Replaces the misleading "Restarting gateway" message with "Escalating
to gateway recovery".

Fixes #68406.
2026-07-21 12:11:52 +05:30
x7peeps
3a9b9d65d5 fix(agent): circuit-break AttributeError from commit-splice and detect code skew
Fix #68178

The git-install auto-updater rewrites source while the desktop backend
is live. Because agent/conversation_loop.py is imported lazily on the
first API call, a process can end up running two different commits
spliced together — one commit's AIAgent against another commit's
conversation_loop. When the interface differs, every turn fails
permanently with an AttributeError, and the loop retries indefinitely,
burning provider API calls (576 failures, 149 wasted API calls observed).

Three-prong fix:

1. Circuit-break AttributeError on agent objects: the outer-loop error
   classifier now detects AttributeError targeting agent/run_agent
   modules and breaks immediately instead of continuing the retry loop.

2. Code skew detection for desktop/serve backend: run_agent.py now
   snapshots the checkout revision at import time and exposes a cheap
   per-iteration check that the conversation loop uses to refuse new
   work with a clear 'restart required' message before the lazy import
   can crash.

3. Informative error message: when code skew is detected, the user
   gets a clear explanation of the mismatch (boot revision vs current
   revision) and actionable guidance to restart the application.
2026-07-21 12:11:46 +05:30
x7peeps
6c28558161 fix(desktop): prevent contentEditable composer input from visually collapsing to near-zero height
Fix #68095

The composer input box (contentEditable div) randomly shrank to a tiny/pixelated
size when typing character-by-character (paste worked fine). Root cause: during
per-keystroke input, the normalizeComposerEditorDom cleanup could briefly leave
the contentEditable with zero child nodes, and without intrinsic content the
browser collapsed it visually despite the CSS min-height.

Two-pronged fix:
1. Add min-h-[1.625rem] bracket syntax alongside the CSS variable min-height
   to ensure the minimum height is enforced even if the CSS variable resolution
   is delayed or overridden by browser defaults.
2. In normalizeComposerEditorDom, ensure the contentEditable always has at
   least one <br> child when empty, giving it intrinsic height that the browser
   cannot collapse. This is a belt-and-suspenders approach with the CSS min-height.

Closes #68095
2026-07-21 12:11:46 +05:30