Commit Graph

46147 Commits

Author SHA1 Message Date
unsupportedpastels
b846c17fe0 test(checkpoints): cover a gitlink below an ordinary directory
A one-component gitlink cannot catch separator or component-splitting
mistakes in the scoped restore guard. Pin refusal for the exact gitlink,
its ancestors and descendants in every spelling (including Windows
separators on Windows), and unrelated recovery beside it.

Co-authored-by: FroRaut <79215537+Froraut@users.noreply.github.com>
2026-09-29 14:12:41 -04:00
unsupportedpastels
713d09970f fix(checkpoints): select nested repo guard scope by recorded path, not symlink target
The scoped restore guard resolved the requested path through the live
filesystem, so a captured file later replaced by a symlink into a nested
repository was refused even though the checkpoint holds its bytes.
Normalize the selection lexically; resolved-path confinement stays in
_validate_file_path. A recorded gitlink replaced by an unrelated symlink
is still refused.
2026-09-29 14:12:41 -04:00
unsupportedpastels
e700e5ffdb fix(checkpoints): honor Git pathspecs in nested repository guard
Resolve selective restores against a disposable index before any checkpoint mutation, preserving unrelated-file recovery and refusing uncaptured gitlinks. Keep NUL-delimited filenames lossless, including surrogateescaped POSIX bytes. Add real-Git regression coverage and document the capture limitation.

Fixes #127073
Salvages #127077

Co-authored-by: Yuan Li <dskwelmcy@163.com>
2026-09-29 14:12:41 -04:00
Yuan Li
19714b1d93 fix(checkpoints): scope nested repo restore refusal
(cherry picked from commit e590192111781391ab3dc2255222aea6627992aa)
2026-09-29 14:12:41 -04:00
Yuan Li
45daba473f test(checkpoints): cover nested repository rollback refusal
(cherry picked from commit a28a71652eb6736b7402dbb5f6e288da4b10772d)
2026-09-29 14:12:41 -04:00
Yuan Li
be6c55685b fix(checkpoints): refuse rollback of uncaptured nested repos
(cherry picked from commit 574816570f0c72fef12b836ea09e5c8233049ca6)
2026-09-29 14:12:41 -04:00
finn763
ebf1be6527 fix(desktop): keep a settled unpersisted live-tail reply across a stale refresh graft
Post-turn rehydrates can race the gateway commit: the refreshed page shares the anchor but ends before the just-settled reply. preserveLocalPendingTurnMessages only protected stream-id rows, so a reply that settled onto a non-stream id (interim id, appended assistant-<ts> bubble) was dropped after it already painted. Extend the #70209 only-copy rule to the single newest settled, non-interim, unpersisted assistant row when the refreshed page carries no identical text.

That row is skipped when the refreshed page brings its own assistant row for the same reply ordinal: a rotated or compacted page supersedes it, so keeping the cached copy would render the pre-compression answer next to its replacement.

Closes #121613
2026-09-29 12:32:11 -05:00
Brooklyn Nicholson
4bc847741d fix(desktop): keep a pending streamed reply over a divergent same-turn store partial
Fixes #80151
2026-09-29 12:30:35 -05:00
Siddharth Balyan
45079e6330 fix(security): the first tirith scan no longer waits on its download (#128150)
* fix(security): the first tirith scan no longer waits on its download

A process that never ran the CLI startup install (desktop serve, the
gateway, a newly routed profile) claimed the install on its first scan and
ran pm.ensure("tirith") inline. The first terminal call then waited on the
PM worker, the runtime toolchain and the tirith download before it ran.

The cold scan now starts the same background install as startup and scans
with the default path, so the call fails open at once like any scan during
a startup download.

* fix(security): a scan during the tirith download never trips the breaker

The previous commit let a cold scan run while the background install was
still downloading. That scan spawned the bare default "tirith", got
FileNotFoundError and counted it as a crash. Three terminal calls during
the download opened the circuit breaker, and every scan then returned
allow unscanned for 300 s, even after the download finished. With
tirith_fail_open: false the first three commands were blocked and the
breaker then let commands run unscanned.

A scan that finds the default binary missing while this home's install
thread is alive no longer spawns or counts a crash. Fail-open allows the
command at once ("tirith installing"). Fail-closed waits for the install,
as the inline install did before, and then scans with the real binary.
So only the fail-open case skips the scan during a download; the earlier
commit's "fails open at once" holds only for fail_open.

The PM consumer test now joins the background install before it reads
the recorded download requests.
2026-09-29 17:29:05 +00:00
David Metcalfe
d939624916 fix(desktop): remove 9px override from pane tab label, inherit parent size
The layout-tree renderer (63a9bde77) introduced PaneTabLabel with a
text-[9px] override that contradicts the parent PaneTab's baseline of
text-[0.6875rem] (11px). The old ChatHeader/TitleMenuTrigger used 0.75rem
(12px), making 9px a clear regression.

Drop the explicit size class from the label span so it inherits 0.6875rem
from the parent — consistent typographic hierarchy, still a meaningful
improvement over 9px.
2026-09-29 12:27:20 -05:00
Brooklyn Nicholson
6ae7b79b51 fix(tools): word the read_preview hint so the Windows footgun lint doesn't read it as open() 2026-09-29 12:25:29 -05:00
Brooklyn Nicholson
0242946697 fix(tools): split the merged preview/terminal bridge timeout errors into the two cases they conflate
An empty bridge answer produced one string that blamed a closed tab
('The action timed out, or no GUI window answered. Open a page with
open_preview first.') even when the pane was open on an app older than
this backend — two cases with different remedies. drive_preview /
annotate_preview now lead with 'no preview tab is open' and name the
older-app possibility; read_preview / read_terminal name both the
nothing-open and bridge-timeout cases and the update-the-app remedy.

Refs #94272
Refs #95104
Refs #119333

Co-authored-by: lesseradmin <veeman961@gmail.com>
2026-09-29 12:25:29 -05:00
Brooklyn Nicholson
d3d47ce98a fix(tui-gateway): probe the preview.act bridge so an absent renderer fails fast
drive_preview / annotate_preview blocked the full 45s deadline on every
call when nothing could answer preview.act — an app build older than the
tools (the renderer ships in the desktop bundle, the backend offers the
tool), or a session no attached window hosts — and the tool then blamed a
closed tab. Port PR 94300's fail-fast ladder onto the JSON-RPC
server-requests path, mirroring _tour_request: first action per session
gets a 10s probe deadline; unanswered → bridge marked unavailable for
that session with an actionable update-the-app error, and later actions
short-circuit instead of stacking waits. Unlike tour support, a preview
pane can open later in the session, so an unanswered probe only condemns
the bridge for a cooldown window, with a single in-flight reprobe per
session; concurrent callers fail fast. Once answered, the full deadline
is back and a transient miss never disables the bridge. A window-owned
request that every window declines settles as a non-empty answer
(main e33f1a0faf): the refusal passes through unchanged.

Fixes #94272

Co-authored-by: lesseradmin <veeman961@gmail.com>
2026-09-29 12:25:29 -05:00
ethernet
36699ac97a Merge pull request #128156 from NousResearch/fix/desktop-flavored-icons-clean-checkout
Desktop release builds stop leaving their flavored icon render in the checkout
2026-09-29 13:08:09 -04:00
Hukla
73688014f7 fix(desktop): hydrate pending clarify args on re-arm 2026-09-29 11:45:04 -05:00
Brooklyn Nicholson
adb1cbde32 fix(desktop): guard the session-info running=false clarify clear too
A reconnect can replay a pre-clarify session.info snapshot with
running=false while the server bridge is still parked on the open
clarify request — the same wipe class as the message.complete/error
clears the previous commit guards, just arriving through the
session-info recovery edge instead of a turn signal.

Fixes the sibling named in #83319 review.
2026-09-29 11:44:20 -05:00
Ayush Nangia
cc931d239c fix(desktop): keep live clarify cards through turn signals 2026-09-29 11:44:20 -05:00
tk-pkm111
9bfbeb7cd0 fix(desktop): mark previous reply spoken before submitting the next voice turn
In the voice conversation loop, the normal (silence-detected) submit path set
`awaitingSpokenResponseRef` and submitted without consuming the reply that
had just finished playing. The turn-drive effect then treated that reply as
unspoken, opened speech for it, fell back to whole-text playback and spoke
the OLD answer while the model was still thinking — until the new reply
arrived and barged in. The barge-in submit path already calls
`consumePendingResponse()` before `onSubmit`; give the normal path the same
treatment. A submitted turn makes any existing reply stale by definition.

Adds a regression test that fails on main (`expected [ 'submit' ] to deeply
equal [ 'consume', 'submit' ]`) and passes with the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-29 11:43:57 -05:00
finn763
7083a524a7 fix(agent): stamp compaction replay rows at compaction time, not the original task time
Closes #121064
2026-09-29 11:43:34 -05:00
Brooklyn Nicholson
7c47cb4c54 style(desktop): blank line before repaint try in webgl context-loss handlers 2026-09-29 11:42:46 -05:00
Brooklyn Nicholson
ebfae3eb73 fix(desktop): repaint through the DOM renderer after WebGL context loss
Fold #107808 into the fan-out architecture: on webglcontextlost the addon
is disposed but the viewport could stay black while the buffer is intact —
now a fit + full-row repaint is forced on both the user terminal and the
agent mirror, and every remaining clearTextureAtlas path (fan-out clear,
live font change) is guarded so a half-dead GL context cannot abort the
repaint path. Also rebuild every registered terminal when the main process
broadcasts powerMonitor resume/unlock-screen: macOS can evict glyph
textures on wake without firing webglcontextlost (#76102).
2026-09-29 11:42:46 -05:00
Soheil Fakour
1dc3edf627 fix(desktop): two-phase atlas fan-out — clear all before refreshing any
GottZ triage (PR #76463) correctly identified that the sequential
clear+refresh per terminal could let a later clear invalidate an earlier
terminal's rebuilt glyph model. Split into two phases: all atlas clears
run first, then all terminal refreshes, guaranteeing every terminal
rebuilds against a clean shared atlas.

Added behavior-oriented regression test proving clear-before-refresh
ordering across multiple terminals sharing one atlas.
2026-09-29 11:42:46 -05:00
Soheil Fakour
46d7989202 fix(desktop): refresh sibling terminals when the shared WebGL atlas is cleared
xterm WebGL terminals that share one glyph atlas (same font/theme/DPR)
keep per-terminal render models over a SHARED texture. clearTextureAtlas()
on one terminal wipes the shared texture pages and xterm 0.19 never tells
the sibling renderers to rebuild their models, so their stale cells draw
whatever glyphs land in the freed atlas rows - garbled text until a
resize forces a full refresh (#76102). Upstream fixed this in
xtermjs/xterm.js#6055; we stay pinned on 0.19.0.

Every atlas-mutating call site (theme switch, tab activation) now fans
out through redrawAllTerminals(), and each xterm registers its own
redraw+atlas-rebuild on mount, unregistering on dispose. Covered by a
unit test asserting the registry invariant.
2026-09-29 11:42:46 -05:00
ethernet
dd962a804f fix(desktop): route Ctrl+R to the terminal instead of reloading the app
Main claims Ctrl/Cmd+R on every window (installPreviewShortcut's
before-input-event + preventDefault) so Chromium's default host
reload cannot fire, and sends preview-nav reload to the renderer.
The renderer's routing only knew browser panes, so a terminal-
focused chord fell through to window.location.reload() — and the
preventDefault meant xterm never saw the keystroke either, killing
readline reverse-i-search (#96482).

Route it the way ⌘W already routes: main keeps claiming blindly,
and the renderer adds the missing terminal rung. The terminal
registry keyed its handles by the inner xterm host while both
resolvers walk up to the [data-terminal] scope, so no handle was
ever found in production (only tests registered on the scope div
itself) — the registry now keys by that scope. The user terminal's
handle re-delivers the ^R byte to its PTY; the read-only agent
mirror swallows the chord (no PTY, and focus there must still stop
the app-level reload). The app-reload escape hatch elsewhere in
the app is unchanged.
2026-09-29 11:42:19 -05:00
Brooklyn Nicholson
b11f3ebe3b test(preview): match the holder contract in the #98048 regression tests
The suite's tests came from #98780 written against an invented holder shape
(handles/node); the engine resolves through holder.book/holder.url after an
elements inventory. Address elements the way the agent does (firstRef), stub
document.execCommand (undefined in jsdom), and add the execCommand-fails
fallback case.

Co-authored-by: salch-cred <salch-cred@users.noreply.github.com>
2026-09-29 11:41:51 -05:00
salch-cred
64878d4311 fix(preview): use proper input events for React/contenteditable/sumbit in drive_preview type
drive_preview type/press/submit silently failed on React controlled inputs,
contenteditable rich-text editors, and Enter-to-submit forms — the tool
reported success but the text never landed and no DOM events fired (#98048).

Three fixes in the injected type handler:

1. contenteditable: use document.execCommand('insertText', false, text)
   instead of el.textContent = text.  execCommand fires beforeinput/
   insertText composition events that Lexical/ProseMirror/Draft editors
   (DeepSeek, ChatZhipu, Kimi) subscribe to.  Fallback to textContent
   + InputEvent for sandboxed pages where execCommand fails.

2. INPUT/TEXTAREA: replace new Event('input') with new InputEvent('input',
   {data: text, inputType: 'insertText', bubbles: true}).  React 16+
   controlled-input trackers compare lastTrackedValue against the event's
   .data property — a plain Event has no .data, so the value-change is
   deduplicated.

3. submit: add keyCode: 13 and which: 13 to the KeyboardEvent init dict.
   Modern React forms check both event.key AND event.keyCode for Enter;
   constructed KeyboardEvents read keyCode as 0 (read-only), so the
   event was silently swallowed.

Regression tests added in act-in-page.test.ts:
- contenteditable execCommand path
- InputEvent data property set correctly
- submit KeyboardEvent keyCode/which === 13

Fixes #98048
2026-09-29 11:41:51 -05:00
Brooklyn Nicholson
f91451ba93 fix(desktop): boot a peer window onto the fresh draft, not the primary's remembered session
The cold-start navigation restore in use-desktop-integrations only excluded HUD
and browser windows, so Ctrl+Shift+N's peer window (?peer=1) restored the
profile's remembered last-session route and opened the same chat as Window 1.
Add isPeerInstanceWindow() to the early-return gate, mirroring the source
restoration gate in connections.ts.

Fixes #74948
2026-09-29 11:41:39 -05:00
Brooklyn Nicholson
4b2682e9fa fix(desktop): prefer the live backlog over the last snapshot fence when appending a catch-up delta
syncAgentTerminalSnapshot checked the stale lastSnapshots prefix-delta before
the authoritative backlog (body) prefix-delta. Live agent.terminal.output
chunks advance the backlog but never lastSnapshots, so once any chunk has
streamed since the last snapshot, body is strictly longer than previous and
both are prefixes of a catch-up process.list snapshot. The fence-first branch
then emitted output.slice(previous.length), re-appending bytes the chunks had
already written and duplicating them on screen and in the replay backlog.

body is the authoritative on-screen state and is always >= previous, so its
delta is the correct, smaller one; check it first. Regression case added to
agent-terminal-stream.bound.test.ts (red without the swap).

Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
2026-09-29 11:41:14 -05:00
John Paul Soliva
63ee01c78f fix(desktop): a full-tail terminal reset refreshes recency and runs eviction
The snapshot reset path re-set the backlog in its original LRU slot and never
ran evictColdProcs, so a just-synced process could be the next one evicted and
resets alone could hold past the character ceiling. The delta paths now update
the lastSnapshots fence before the write, so that write's eviction pass counts
it and never re-creates a fence for a process it just forgot.
2026-09-29 11:41:14 -05:00
John Paul Soliva
0b9287c8cf fix(desktop): a finished agent terminal stops pinning its backlog for the life of the renderer
Every `terminal(background=true)` mints a new process id, and the agent-terminal
stream module never forgot one. `writers` was unregistered when a tab unmounted,
but `backlog`, `lastSnapshots`, `commandHeaders` and `seededCommands` are keyed
by process id and had no eviction at all, so every command an agent ever ran kept
its output until the window closed.

Replaying each process back through `registerAgentTerminalWriter` measures the
retention directly, without depending on GC: 256K chars held per finished process
(~512 KB, JS strings being UTF-16), growing linearly — 100 processes 25.6M chars,
200 51.2M, 400 102.4M (~195 MB). With this change it is flat at 1.79M chars
(~3.4 MB) for every one of those counts.

Bounded the way the other renderer caches already are (`lib/lru-cache`,
`chat/shiki-highlight-cache`): an entry ceiling and a total-character ceiling,
because one busy process can hold as much as twenty quiet ones. `backlog`
insertion order is the LRU clock, so a chunk write re-inserts to move its process
to the tail — a plain re-set keeps the original slot and would let the
oldest-first sweep evict a process that is still streaming.

A process whose terminal is MOUNTED is never evicted: that is on screen, not a
cache. Everything dropped is regenerable — the next `syncAgentTerminalSnapshot`
re-seeds the tab from the registry's rolling tail. The four maps are evicted
together because `lastSnapshots` is the delta fence for `backlog`; dropping one
without the other makes the next snapshot diff against a tail that is gone and
paint only the delta.

Each guarantee is pinned by a regression that fails when that guarantee alone is
reverted: no eviction, evicting a mounted process, losing the recency refresh,
and keeping a stale delta fence.
2026-09-29 11:41:14 -05:00
BrunoBza
5f23cac568 fix(desktop): warm local STT on mic-open so cold model load stops timing out transcription (#105955) 2026-09-29 11:40:36 -05:00
Siddharth Balyan
a2840f954b Inline agent tools get the shared argument repair (clarify, todo, memory, session_search) (#127781)
* fix(tools): inline agent tools get the shared argument coercion

Inline agent tools (todo_list, clarify, memory, session_search, and the
rest of INLINE_TOOL_EXECUTORS) never went through handle_function_call.
They did not get coerce_tool_args, so a JSON-encoded string for an array
parameter failed unless the tool parsed it by hand.

Every entry of INLINE_TOOL_EXECUTORS now runs coerce_tool_args first.
The sequential path and the concurrent invoke_tool path both read that
table, so one wrapper covers both.

todo_tool no longer parses a JSON string by hand. The todo_list schema
declares todos as an array, so coercion covers it. The existing test now
calls the dispatch table, not todo_tool directly.

* chore: retrigger CI (zero-job dispatch failure, auto-heal)
2026-09-29 22:07:30 +05:30
ethernet8023
4bc38bceec fix(desktop): hand the admitted checkout back after flavored icon staging
electron-builder resolves every piece of packaging artwork from the workspace
— the app icon, the icon.ico extraResource, the MSIX logo set before-build
stages into build/appx, and the exe identity stamp — so a flavored canary or
commit build copies its rendered icon set over apps/desktop/assets before
packaging.

Those files stopped being gitignored in 4b7229d612, which turned a write that
was invisible into 15 dirty paths. The source custody check then refuses the
checkout twice: inside the same build, at the check that used to sit after the
copy, and on every later build from the first check on a now permanently dirty
tree.

Hold the render at the workspace path for the packaging window only and
restore the admitted files afterwards, whether packaging finished or failed.
The final custody check now asserts the build handed the checkout back.
2026-09-29 12:29:47 -04:00
Mark S.
ca705dbf7e fix(ci): card-tool contract test reads the as-const list and runs on desktop edits (#128010)
#127828 changed CARD_TOOL_NAMES in apps/desktop/src/lib/tool-render-class.ts
from `new Set([...])` to `[...] as const`. The pytest that pins that list
against the gateway's _TOOL_LIFECYCLE_UI_TOOLS located it with a regex that
only matched `new Set([`, so it has failed on main since with
"CARD_TOOL_NAMES not found in tool-render-class.ts". The gateway set still
covers every desktop card name; only the test's parser was stale.

The PR merged green because it touched only apps/, which skips the Python
lane, so the one test that reads that file never ran on it.

- The parser accepts both declaration forms and fails on an empty list
  instead of passing a vacuous subset check.
- tool-render-class.ts joins _PY_RELEVANT_CONTRACT_FILES so an edit to it
  runs the Python lane, with a classifier case for the entry.
2026-09-29 12:27:26 -04:00
Jeffrey Quesnelle
5000e29936 Merge pull request #127012 from RyanUnderhill/fix/n1x-pci-device-identification
fix(local-models): identify N1X by PCI vendor/device ID
2026-09-29 11:55:48 -04:00
ethernet
5bb2be179d Merge pull request #127959 from NousResearch/fix/desktop-native-quad-epoch
fix(desktop): use admitted release epoch for Windows native version
2026-09-29 11:42:16 -04:00
kshitijk4poor
16c59d0e78 perf(cli): scan platform manifests only for names the cheap checks cannot place
saved_toolset_resolver built the hermes-<platform> bundle set eagerly, so
every migrate_config run (hermes update, config migrate, doctor) and every
config check paid a full bundled+user manifest scan even with no saved
platform_toolsets. The scan is now cached and lazy, like the plugin lookup.
The stale-toolset test stubs the plugin lookups so host-installed plugins
cannot change its verdict.
2026-09-29 20:34:45 +05:30
kshitijk4poor
361dc5cf69 fix(cli): judge saved selections the way the runtime does in config check
Follow-up to the config check diagnostics from #127392.

- Saved platform_toolsets are validated by one resolver,
  toolset_validation.saved_toolset_resolver, shared with
  `hermes config migrate`. The two commands previously disagreed: migrate
  flagged MCP server names and plugin-platform bundles (even suggesting
  'hermes-teams' for 'hermes-teams'), while config check accepted them.
- A configured MCP server is known even when disabled (enabled: false);
  it is inactive, not a typo.
- known_plugin_toolsets is no longer treated as proof that a toolset
  exists. It records what a platform was offered at save time, so it hid
  toolsets of uninstalled plugins, the stale case this check is for.
- Plugin-provided names are consulted only for names nothing else
  explains, so a clean config no longer runs plugin discovery.
- A malformed mcp_servers (a list) no longer crashes the command.
- The top-level `toolsets` check is dropped: the CLI resolves its
  toolsets from --toolsets or platform_toolsets, and reads the top-level
  key only for the legacy kanban opt-in.
- Disabled platforms are read with the runtime's plugins.disabled reader,
  so a non-list value (which the runtime ignores) is no longer reported
  as disabling a platform. Required credentials come from
  _platform_manifest_env_entries, which now takes optional=False.
- Tests use a synthetic platform manifest instead of the real Telegram
  one, and assert config check and migrate report the same names.
2026-09-29 20:34:45 +05:30
Gille
85b01aac9b fix(cli): diagnose stale toolsets and disabled platforms
(cherry picked from commit 98ab863de43995a133b2ccacdc790fb3589128ec)
2026-09-29 20:34:45 +05:30
ethernet
74163fa18c fix(desktop): pass release epoch to native version subprocess 2026-09-29 10:55:18 -04:00
Siddharth Balyan
5eb1381f39 fix(desktop): consecutive manage_connections calls share one card for everyone (#127872)
The tool-group slot folded consecutive manage_connections calls into one
connection card only when guided onboarding was enabled, while MCP targets
were folded for every user. Connector calls now fold the same way for
everyone, and the onboarding check is gone.
2026-09-29 19:29:29 +05:30
ethernet
ee5f49b943 fix(ci): give the stable-release reusable workflows their environment secrets
stable-release.yml called desktop-bundled-release.yml, docker.yml and
termux-verify.yml without `secrets: inherit`, so every job inside them that
declares `environment: release-signing` (or container-publish) attached its
deployment and read the environment's `vars`, but resolved `secrets.*` to the
empty string. The signed-candidate legs died at "Archive every pinned input"
with `missing env CLOUDFLARE_R2_ACCESS_KEY_ID`; the docker publish legs and
the termux packaging checks would have failed the same way.

A called workflow receives no secrets by default, and a job-level
`environment:` inside it is required but not sufficient: a caller job that
`uses:` a reusable workflow cannot declare `environment:` at all, so
`secrets: inherit` is the only mechanism that can carry them
(actions/runner#1490).

`secrets: inherit` is added only where the callee reads a protected
environment's secrets. ci.yaml, nix, pm-bundle, windows-venv-e2e and the
install-e2e chain reference none, and ci.yaml must stay secret-free by its own
header (it runs PR-controlled code).
2026-09-29 09:40:09 -04:00
Siddharth Balyan
fc042f1d67 refactor(desktop): route tool cards and bridge events through lookup tables (#127828)
* refactor(desktop): route tool cards through a table keyed on the card-tool names

The name-keyed if ladder in ChainToolFallback becomes TOOL_CARDS, typed
Record<CardToolName, FC>. CardToolName comes from the same list isCardTool
reads, so a card added to one list and not the other fails the build.
Behaviour is unchanged.

* refactor(desktop): dispatch desktop bridge events through a handler table

The six-branch event.type ladder becomes DESKTOP_BRIDGE_HANDLERS, one row
per event with its body and comments unchanged. Behaviour is unchanged.

* chore: retrigger CI (zero-job dispatch failure, auto-heal)
2026-09-29 18:37:11 +05:30
Siddharth Balyan
5eea87882a Clarify: one question shape and one result shape on every surface (skip, cancel, timeout and undelivered are told apart) (#127760)
* refactor(desktop): split clarify-tool.tsx into a clarify/ folder

Pure moves, no behaviour change. Parsing, the question-card core
(shell, choice rows, question block), the delivery watchdog, and each
pending/settled card get their own file so the core can be reused.

* feat(clarify): one questions[] shape with per-question status and one outcome

The clarify tool now takes only questions=[{question, choices?, multi_select?}].
A wrong shape is a tool error that names the right one, so a model that
sends the old top-level question/choices corrects itself on the next call.

Every result has the same shape on every surface: each response carries
status (answered, skipped, unanswered) with user_response null unless
answered, and the result carries one outcome (submitted, cancelled,
timed_out, undelivered) plus an optional surface-written notice. The
timeout and cancel sentences that used to pose as the user's answer are
gone, and the compressor reads status instead of matching their prefixes.

The callback contract is callback(questions) -> {answers, outcome, notice?}
(None = skipped, missing qid = unanswered). tui_gateway settles a batch the
same way for the last lock, a cancel, an interrupt and the deadline, and
clarify.lock keeps a null answer as a skip. A multi-select answer that is
not a JSON array counts as one typed ("Other") answer. The tool-row preview
reads the first question.

* feat(clarify): every surface asks through the one question card

CLI: the batch panel is the only panel; Enter on an empty field skips a
question, Ctrl+C cancels and keeps the locked answers, the deadline returns
timed_out. -q and -z return undelivered with a no-user notice.

Ink TUI: the single-question prompt is gone; the card supports multi-select
(Space or a digit toggles, Enter locks, typed Other text joins the picks),
an empty submit skips, Esc cancels, and "Batch" names are dropped.

Desktop: the single-question card is deleted and its keyboard handling moved
into the one card (arrows, letters and digits, auto-advance, Enter picks then
confirms). Confirm enables at one answer; blank questions lock null; Skip and
a composer message cancel; the settled card shows No answer for unanswered
questions. The bots room card follows the same contract.

Messaging: one card per question with a "Reply skip to skip" line in every
locale; timeouts and delivery failures map to timed_out and undelivered.

* fix(clarify): cancelled for a released messaging card, labels win over the skip word

A prose reply to a messaging choice card, /new and session end released
the wait with "", which read as timed_out. They now resolve with a
CANCELLED marker and the tool gets outcome cancelled.

A typed reply that matches a choice label ("Skip") now resolves to that
choice; the skip word applies only when no choice matches.

The bots room card sends the picked labels as they are; the tool already
strips the recommendation label. Unused OUTCOMES and a new docstring and
comment are removed.

* fix(tui): re-editing a multi-select answer keeps its picks

The Ink card stored a multi-select answer as display text ("A, B"), so
revisiting the question put the whole string into Other and re-locked it
as one item. The answers map now keeps the raw JSON array, the card
restores the picks on revisit, and only the display lines format it.

Comments that earlier edits reworded are restored to their original
words, minus the phrases the change made wrong. The compute-host clarify
lock accepts None for a skipped question.

* test(clarify): align three checks with the one-answer confirm and the regenerated keys

The desktop Cmd+Enter test now expects a send with one answer (the blank
question locks null), the compressor test expects the single-answer summary
the code gives, and locales/_keys.desktop.json is regenerated for the
removed and added clarify strings.

* test(tui): the one-question card header is singular
2026-09-29 18:33:44 +05:30
Siddharth Balyan
bc7f58f3b0 fix(gateway): first-contact note only in DMs, offer flag saved in the routed profile (#127818) 2026-09-29 12:59:23 +00:00
FroRaut
b8a8be185d fix(debug): mask URL tokens and passwords in debug share uploads
hermes debug share, gateway /debug, the dashboard share and the desktop
diagnostics upload sent credential query parameters (?token=, ?api_key=,
?access_token=) and user:pass@ userinfo from logged URLs unchanged, to
the paste service or to Nous storage. Log lines keep these values
because log-time redaction leaves web URLs alone, and Firecrawl, for
example, logs the full URL it scrapes at INFO.

The cause is hermes_cli/debug.py:_redact_log_text, the one redactor on
every share path. It called redact_sensitive_text(force=True) without
redact_url_credentials=True, so the strict URL pass that compaction,
memory context, cron errors and the kanban notifier already use never
ran on the upload copy.

Pass redact_url_credentials=True there. Parameter names, hosts and
paths stay readable; values of credential-named parameters and URL
userinfo become ***.

(cherry picked from commit f522171b836bfd842354f5a34c8c2bdd46b39467)
2026-09-29 07:51:44 -04:00
kshitijk4poor
5ed979bc30 test(gateway): kill live lock holders by captured identity, tolerate AccessDenied
Review follow-ups on the live strict-identity tests:

- keep the holder as the psutil.Process captured at spawn, so a PID
  recycled before class teardown is refused instead of killed;
- suppress psutil.Error (AccessDenied on an exiting Windows process),
  not only NoSuchProcess, so one failed kill cannot leave the rest of
  the tree alive and wedge communicate();
- bound the handshake at 20s (a normal start takes ~1s);
- garble the lock in binary mode like the restore does, and say why
  overwriting without truncating is safe;
- drop spawns_gateway_lookalike from the non-gateway holder test (its
  argv carries no `gateway run`) and note that the class holder spawns
  before the per-test guard is armed.
2026-09-29 17:15:05 +05:30
kshitijk4poor
e4e16a54ae test(gateway): bound and reap the live strict-identity lock holders
The live lock-holder tests could hang or leak:

- the handshake readline had no deadline and stderr went to DEVNULL,
  so a child stuck on import blocked the worker with no diagnostics;
- the holder slept 120s, outliving a killed pytest while holding
  gateway.lock;
- `assert acquire_gateway_runtime_lock()` vanishes under -O;
- kill() hit only the Windows venv launcher, never the self-reported
  interpreter, and pipes were never closed (ResourceWarning per test);
- the discovery test relied on list_profiles() resolving to the temp
  HERMES_HOME and compared unresolved paths (/var vs /private/var on
  macOS, 8.3 names on Windows).

The holder now blocks on stdin, reports failure via exit + captured
stderr, and teardown kills the launcher tree plus the reported PID and
reaps via communicate(). The four class tests share one class-scoped
holder with per-test restore of gateway.pid / gateway.lock (5 spawns ->
2), and discovery pins the profile roots like the named-pipe live test.

Also read gateway.pid with utf-8-sig (windows-footguns lane).
2026-09-29 17:15:05 +05:30
emozilla
5d1ab29b03 fix(gateway): identify a live gateway from its lock when gateway.pid is gone
hermes update on Windows maps running gateways to profiles through
get_running_pid_identity_strict, which raised "active gateway lock has no
PID metadata" whenever the lock was held but gateway.pid was missing. The
whole update aborted before touching anything.

That state is reachable on Windows: status cleanup unlinks gateway.pid and
gateway.lock together, and the lock unlink fails silently while the live
gateway holds the file open, so only gateway.pid disappears (#96582).

The gateway writes the same identity record into the lock when it acquires
it. The strict probe now takes the PID from the lock record, requires a
PID file that exists to agree with it, and runs every existing liveness,
start-time and command-line check against what is present. A garbled lock,
a disagreeing PID file, or a lock holder that is not a gateway still aborts.

(cherry picked from commit 1d6bfbbf94605fcc4c84cb52636887e1e8ae36c1)
2026-09-29 17:15:05 +05:30
kshitijk4poor
d9f6a844c1 refactor(update): print the owed-tail notice once, in the process that boots
A supervised start that also relaunches onto the store interpreter printed the
notice in both processes; it now prints only after the relaunch decision.
The _supervised_child docstring claimed the neighbouring truthy set, which it
does not match, and prepare_launch said a supervised child "never" retries the
tail although stale dependencies still do; both reworded. The supervised test
also pins a clean stdout and a single notice.
2026-09-29 17:05:00 +05:30