Commit Graph

214 Commits

Author SHA1 Message Date
Brooklyn Nicholson
330d9d6df9 fix(desktop): wrap wide markdown and fences instead of scrolling the transcript (#70451)
Wide markdown content forced horizontal panning: fenced code in tool
bodies, user bubbles and the bot group room grew sideways scrollbars (the
group room clipped with no scroll affordance at all), and huge plain-text
fallbacks never wrapped.

- markdown-text: min-w-0 on the container so the flex chain can't grow
  from its content; HugeTextFallback chunks soft-wrap
- compact-markdown (tool bodies): fenced code soft-wraps
- user-message-text: fences soft-wrap inside the bubble
- group-chat-view: room bodies soft-wrap pre instead of overflow-x-auto
  (the clip with no scroll affordance, #91706 surface)
- tables keep content-sized auto layout, so cells wrap at word
  boundaries; an unbreakable token scrolls inside the table card, like
  code cards do, instead of squeezing every column letter by letter

Co-authored-by: yuexiong <xiongyue_hnu@163.com>
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: networthexplained <networthexplained@users.noreply.github.com>
2026-10-01 02:20:13 -05:00
Xipong
7df7b2776f perf(desktop): bound messages-below scroll measurements 2026-09-30 23:07:15 -05:00
Xipong
9be397bdfc test(desktop): use workspace Electron resolver and clean timeline lint 2026-09-30 23:07:15 -05:00
Xipong
e6499b5a68 test(desktop): measure timeline geometry cost in native Chromium 2026-09-30 23:07:15 -05:00
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
666f313d1d Desktop opens new installs at a screen-sized window and 110% zoom (#127448)
* feat(desktop): open new installs at a display-sized window and 110% zoom

A fresh install opened at a fixed 1220x800 and 90% zoom, so body text sat
below the Apple and Windows minimums and users pressed Cmd+ repeatedly.

The first window now takes 75% x 80% of the primary display's work area,
floored at 1220x800, capped at 1600x1000, and never larger than the work
area. A saved window-state.json still wins. The default zoom is the 110%
UI Scale preset; Cmd+0 / Actual Size resets to it. Existing installs keep
their saved zoom (first launch has always written the default to
zoom-state.json).

* chore: retrigger CI (zero-job dispatch failure, auto-heal)

* test(desktop-e2e): let a full-window overlay cover the viewport within 1 px

waitForInteractive treats a fixed element as blocking only when its rect
reaches innerWidth/innerHeight. innerWidth is rounded: at 110% zoom a
1280x860 window is 1163.6 CSS px wide and innerWidth reads 1164, so the boot
overlay (inset-0, 1163.6 px) no longer counted as covering the window and the
update E2E declared the app interactive while it was still booting.
2026-09-29 06:55:56 +00:00
alt-glitch
2d9d4abbef fix(desktop): desktop.log stamps lines in local time, like the Python logs
desktop.log stamped each line with an ISO-8601 UTC time in brackets
(`[2026-09-28T07:48:19.466Z]`), while agent.log, gui.log and errors.log use
Python's local-time asctime (`2026-09-28 13:18:46,062`). Reading one
incident across logs/ meant converting clocks by hand, and
`hermes logs desktop --since 1h` returned every desktop line, because its
timestamp regex expects the Python shape at the start of the line and
lines it cannot parse pass the filter.

formatLogStamp() now emits the Python shape in local time, and
formatDesktopLogLine() puts it first: `2026-09-28 13:18:46,062 [hermes] ...`.
The #84405 comment already said the stamp matched the Python convention;
now it does.
2026-09-28 19:02:20 +05:30
teknium1
989798cd5e fix(desktop): test runs stop leaking temp dirs into TMPDIR
Hermes points TMPDIR at ~/.hermes/cache/scratch, so every Desktop test
run's leftovers pile up there until the 24h idle pruner catches them:
one day on a dev host left 688 playwright-tracing-*, 152
playwright-artifacts-*, 75 hermes-e2e-mock-* sandboxes and ~600 small
vitest mkdtemp dirs.

- Playwright (main + core configs): e2e/run-tmp.ts gives the run one
  temp root that workers, Electron and the backend inherit via TMPDIR,
  and removes it when the runner exits. Covers the stack-trace dirs the
  Electron tracing patch orphans (2 per launch, even on green runs),
  sandboxes a failed/timed-out setup never cleans, and Playwright's own
  artifact dirs. A SIGKILLed run now leaves one pw-* dir for the pruner.
  The update suite keeps os.tmpdir(): its seeded install is reused
  across runs and bakes its path into AF_UNIX sockets.
- vitest (apps/desktop): globalSetup does the same for the run; one
  `--project electron` run left 54 dirs from tests that never remove
  what they mkdtemp.
2026-09-27 23:10:22 -07:00
BlackishGreen33
55e27e5dbe fix(desktop): explain missing profile SOUL.md files
The backend already reports {content, exists} from GET
/api/profiles/{name}/soul, but both SOUL.md editors read only content —
a profile whose personality lives in config.yaml
(agent.personalities.<name>) rendered a blank CodeEditor with no hint
that anything was missing or where the persona actually lives (#89436).

Both surfaces (Profiles view editor and the sidebar Edit SOUL.md
dialog) now show a localized notice when exists is false: no SOUL.md
exists yet, instructions below will create one on save, and config.yaml
personality presets are managed separately. The notice clears on a
successful save; an existing empty file or a read error is never
misreported as missing.

i18n: soulMissing added to every full locale (de/es/fr are override
locales and fall back to English). Component tests cover the
missing/empty/read-error distinction and cross-profile leakage; an e2e
spec drives both editors end to end.

The config.yaml read-back fallback and the dashboard source indicator
stay deferred to the personality-precedence work (#82801).

Salvaged from PR #105201 (author preserved).

Fixes #89436
2026-09-27 06:54:56 -05:00
teknium1
5b27d6c2c2 test(e2e/desktop): let the build-fail updater finish its launch acceptance before asserting it exited
On the error path posix.sh writes the result, relaunches the app, and then
sleeps 1.5 s to confirm the launch took. A relaunched app that boots fast logs
`detached update FAILED` inside that window, and the spec checked for a live
posix.sh right away. On #124878 the app logged at 08:34:06.565, about 0.5 s
after it launched, the check ran at 08:34:07.16, and posix.sh was still in
`sleep 1.5`. The spec now polls for up to 30 s.
2026-09-27 02:56:08 -07:00
teknium1
7b4211eee2 test(e2e/desktop): stop the installer's nested lazy-fetch chain on CI
Trail from CI: 78+ nested 'git fetch origin --filter=blob:none --stdin'
lazy fetches, each with its own upload-pack on the seeded origin, until
the runner is OOM-killed. The origin serves full clones, so nothing is
legitimately missing; GIT_NO_LAZY_FETCH=1 for the installer run only.
2026-09-27 00:42:43 -07:00
teknium1
464e1fe473 test(e2e/desktop): seed from an origin that serves full clones
CI trail: the installer's --filter=tree:0 clone against the local
file:// origin fanned out into 1318 concurrent upload-packs and the
runner was OOM-killed ~90 s into seeding (local git never advertised
filter there, so local seeds were already full clones).
2026-09-27 00:42:43 -07:00
teknium1
0cd1070bf8 test(e2e/desktop): poll desktop.log for the refusal line
desktop.log is flushed on a timer, so the refusal line can trail the
dialog by a flush interval (seen once in five local runs).
2026-09-27 00:42:43 -07:00
teknium1
db2e88cc0a style(e2e/desktop): eslint --fix on the update suite 2026-09-27 00:42:43 -07:00
teknium1
e7d71ca2c2 test(e2e/desktop): first run without the bootstrap marker; chat before the About check
first-run: launch 2 drops .hermes-bootstrap-complete, so a usable install
must be recognised off the filesystem (the marker-gated resolver is the
sabotage that turns it red), and the chooser assertion runs before the
backend count so a red names the chooser.
update-concurrency: the About panel replaces the chat view, so the post-
update chat turn runs before the panel is opened.
2026-09-27 00:42:43 -07:00
teknium1
ae32798893 test(e2e/desktop): reuse a seeded install only once its snapshot is complete
seed.py writes install.json last and global-setup keys REUSE on it, so a
seed interrupted mid-copy is re-seeded instead of reused half-built.
2026-09-27 00:42:43 -07:00
Teknium
183e0d4adc test(e2e/desktop): PR-time Desktop install/update suite on a real install
Drive the packaged app a real scripts/install.sh + hermes desktop install
built, against a local bare origin, a local api.github.com stand-in and a
scripted model: first run, Update now hand-off/relaunch/chat, a failed
Desktop build inside that update, and a second update while one runs.
Open bugs are message-gated (gated on #122991).
2026-09-27 00:42:43 -07:00
liuhao1024
00c8e5ca48 test(desktop): cover the one-entry batch clarify shape end to end
Since the questions[]-only schema (#95907) every single question is a
one-entry batch on both the tool args and the gateway wire, yet no test
exercised that shape (called out in #98645). Lock the behavior down at
both layers:

- unit: a one-entry batch renders the batch card (not a blank/spinner
  single card) both with the wire already parked and when the request
  lands after the tool row, and answers with the qid-keyed lock
- e2e: a SINGLE_BATCH trigger drives the real chain (composer -> gateway
  -> agent -> clarify tool -> clarify.request -> renderer) through mount,
  pick, confirm, and settle for questions.length === 1

The e2e mock's trigger routing also learns to scope its has-tool-result
guard to the answering turn's own question text: the existing any-tool-
result check would false-positive once a second scripted clarify shares
the conversation history.

Adapted to main: the mock server now lives in tests-js/scripts, and a batch
confirm answers with clarify.lock.
2026-09-26 18:00:36 -05:00
Hermes Agent
9d09662993 docs(desktop): describe what --ignore-existing skips 2026-09-26 17:15:10 -05:00
teknium1
4cdc58ad4c fix(desktop): Stop shows the same partial reply the session saves
Stop seals the live bubble at the click and mutateStream drops every later
message.delta (queued-but-unflushed ones too), but the agent keeps streaming
until it honours the interrupt and persists every delta it delivered. The
interrupted message.complete carrying that persisted partial was ignored, so
state.db (and the next turn's context) held words the user never saw.

The interrupted completion now extends the stopped bubble to the persisted
partial, or adds a bubble when nothing had been painted. Extend-only: a
shorter or different text never replaces what was shown.

Refs #121594
2026-09-26 11:12:42 -07:00
ethernet
8d8edf4cda test(desktop-e2e): drop the packaged smoke's now-unused repo-root import 2026-09-24 11:12:27 -04:00
ethernet
890e9b73d4 test(desktop-e2e): run the core Desktop E2E on the PM toolchain
Upstream's e2e-desktop-core job provisioned setup-node + uv sync and pointed the
packaged smoke at a checkout .venv. Provision through setup-pm, hand its selected
interpreter to Desktop via HERMES_DESKTOP_PYTHON, and read the packager contract
from electron-builder.config.cjs, where the build config now lives.
2026-09-24 11:12:27 -04:00
ethernet
427d0883be Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 09:33:58 -04:00
teknium1
deb4bd2c08 test(desktop): packaged boot fails fast (60s) with main log tail; README lists the new specs and KNOWN policy 2026-09-24 06:06:00 -07:00
teknium1
e0615053dc test(desktop): #120730 Bot-Mode shape (local primary + remote secondary) and a backend that really cannot see client paths
remote-secondary: a bot on a remote secondary connection (connections.json)
with the local backend as primary; a client-only image must reach the remote
as image.attach_bytes. startRemoteBackend({hide}) covers client folders with
an empty tmpfs in a private mount namespace (unprivileged userns; annotated
'fidelity' when unavailable), used by both remote specs. remote-topology:
the post-restart state.db poll (which passed before the new backend opened
the DB) is replaced by a new-pid check + the restarted backend serving the
title after reload.
2026-09-24 06:06:00 -07:00
teknium1
552704e26d test(desktop): real compaction lineages — rotated rows (#121148) and in-turn compaction handoff (#121088) as merge-order-safe KNOWN cells
lineage-sidebar's #121148/#121088 steps never compacted (/compress was
refused as would_grow on a tiny transcript), so they could not fail.
lineage-rotation drives real auto-compaction with compression.in_place:
false (state.db-verified sealed parent + continuation) and asserts one
sidebar row per lineage after rotation, reload and cold relaunch; the live
nested-branch shape is KNOWN #121148. lineage-compaction-prompt compacts
inside an acknowledged redirect turn so the refresh passes through
preserveLocalPendingTurnMessages; the duplicate prompt is KNOWN #121088.
known.ts: expectNoSymptom marks a test expected-failing at run time only on
the bug's own assertion (a fix merging first stays green). lineage-sidebar
keeps the branch/switch scenario, drops the #121096 claim and now checks the
in-page duplicate sampler.
2026-09-24 06:06:00 -07:00
teknium1
6157155c62 test(desktop): format + drop empty KNOWN scaffolding in lineage spec 2026-09-24 06:06:00 -07:00
teknium1
4e489e9ee6 test(desktop): lineage/sidebar integrity after compaction and branching (#121148 #121088 #121062 #121096) 2026-09-24 06:06:00 -07:00
teknium1
7f401ed5e0 test(desktop): remote-backend topology — image bytes not client paths (#120730), rename across backend restart (#121192) 2026-09-24 06:06:00 -07:00
teknium1
65b24e9600 test(desktop): packaged-app smoke — asarUnpack contract + packaged binary boots to a first chat (#121097) 2026-09-24 06:06:00 -07:00
ethernet
66d54ebf51 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/docker.yml
#	Dockerfile
#	apps/desktop/src/app/settings/about-settings.tsx
#	docker/stage2-hook.sh
2026-09-24 02:23:44 -04:00
BearHuddleston
994074c789 fix(desktop): retain provider setup connection and profile ownership 2026-09-23 22:41:26 -07:00
ethernet
d75d3fe15c Merge origin/main into ethie/pm-clean
Conflict resolutions and semantic fixups:

- tools/environments/base.py: main's hard-exit kill fence (kill a spawn the
  fence missed, deregister from _live_foreground in a finally) wrapped around
  pm-clean's output collector.
- pyproject.toml: pm-clean's marker list plus main's new `live` marker.
- hermes_cli/main.py: pm-clean runs startup recovery from hermes_bootstrap, so
  the old early-recovery block stays gone; main's interrupted-pull restore
  (auto-merged above it) runs right after bootstrap, as on main.
- hermes_cli/update_cmd.py: main's interrupted-pull marker now guards
  pm-clean's first tree mutation (release-tag detach, ff-only, or reconcile)
  and is cleared once git is done. The marker's target is the ref git actually
  moves to (a release tag, not always origin/<branch>), since the restore
  compares against it.
- hermes_cli/_early_recovery.py: restore `import subprocess`, which pm-clean
  had dropped and main's auto-merged restore needs (NameError on the first
  launch after a killed update; test_update_interrupted_pull red -> green).
- apps/desktop/src/i18n/{de,es,fr}.ts: main's new locales carry the full
  settings.about block; trim it to `updates` as pm-clean's type and the other
  overlays do (tsc: 27 errors -> 0).
- main's new e2e tests: `import yaml` -> hermes_yaml; wake-word import table
  names pyopen_wakeword (pm-clean's wake-openwakeword extra); the anthropic
  key-leak switch leg needs the SDK, and the api_server two-tenant test needs
  aiohttp, both PM runtime extras the test env does not carry.
2026-09-23 21:55:59 -04:00
teknium1
1d574487eb test(desktop-core): 180 s per-test cap, keep traces on a cancelled lane, honest onboarding header
A stalled stream burned the 600 s per-test timeout until the 30-min job
timeout cancelled the lane, and the failure()-only upload then saved no
traces. The CLI --reporter flag also dropped the config's html report.
The onboarding spec is a smoke test; #120005 needs a non-default profile.
2026-09-23 17:41:12 -07:00
teknium1
f14ee90e52 test(desktop-core): no lazy promisor fetch from a partial-clone checkout
Loop run 7 at 22e8184 went red on "no sandbox process survives quit #1":
a `git fetch origin --filter=blob:none --stdin` (+ its index-pack) was
still running 60 s after quit. That is git's lazy promisor fetch: the dev
checkout is a blob:none partial clone, and a git read by the backend
needed a missing blob, so git went to the network, and the fetch outlived
the backend (reported as a finding; same class as the gh probe). CI
checkouts are not partial clones, so the spawned app gets
GIT_NO_LAZY_FETCH=1: dev runs stay offline and deterministic like CI.
2026-09-23 17:41:12 -07:00
teknium1
3ed7faa88d test(desktop-core): keep the external tirith scanner out of the sandbox
With no tirith on PATH (a CI runner) the backend installs it from GitHub
releases synchronously on the first terminal command, which puts a network
download inside a required lane. With one on PATH (a dev box) it fetched a
12 MB threat DB into the sandbox HOME that was still being written after
quit and cleanup (the stability loop left a recreated sandbox dir behind).
The approval prompts the suite asserts come from Hermes's own dangerous-
command detector, so the sandbox config turns tirith off, the same way the
suite fakes gh.
2026-09-23 17:41:12 -07:00
teknium1
c7cdc91fe8 style(desktop-core): eslint padding in the core specs 2026-09-23 17:41:12 -07:00
teknium1
bd736f3b42 test(desktop-core): onboarding (custom endpoint) then first chat (C2)
Fresh home, no provider: the real first-run onboarding, custom endpoint
pointed at the scripted provider, then the first chat, a second turn and
a reload, each under the transcript oracle (exactly once, DOM ==
persisted, streamed == provider at every sampled frame). Also asserts
the persisted assignment is the entered endpoint, the model is called
with the endpoint's advertised model, and one live socket remains after
onboarding: the path where #120005 (every word painted twice) shipped.
Red when onboarding persists the URL as typed instead of the probed /v1
base (#65488 class).
2026-09-23 17:41:12 -07:00
teknium1
20300f0d47 test(desktop-core): a pre-exec child of the backend is not a second backend
The boot census flagged "backend pids seen during boot: 2" in about 1 of
20 boots. Diagnosis (tight /proc watcher beside 20 repeats): every
`hermes serve` forks ~40 probes per boot (git, ps, ldconfig/gcc for
find_library, pip, docker version), and between fork and exec each child
still shows the backend's argv and HERMES_HOME; ~9 per boot were caught
in that window at 2 ms sampling, and the 100 ms census caught one every
few boots (the reproduced red: two extra pids with ppid = the backend,
one sample each). No double spawn by the supervisor was ever observed.

backendProcesses() now drops a serve process whose parent is itself a
serve process. A real second spawn has Electron main as parent, and a
pre-exec child orphaned by a dead backend is reparented and still counted.
The census also records ppid, argv and lifetime per pid so any future
extra pid explains itself in the failure message.
2026-09-23 17:41:12 -07:00
teknium1
3d8dbad45e test(desktop-core): wait for message.complete before judging the DOM; explain stuck boots
Under heavy load message.complete can trail the persisted row, so the wire
check is now a deadline poll run first (duplicate frames never heal, so the
poll cannot mask them) and the DOM oracle then covers whatever the renderer
does on completion. A boot that never becomes interactive now fails with the
blocking element chain, route, body text and the main-process log tail.
2026-09-23 17:41:12 -07:00
teknium1
12b770c357 test(desktop-core): README lists the C20 and switch-back specs 2026-09-23 17:41:12 -07:00
teknium1
0015e4038b test(desktop-core): clarify and approval round trips (C20)
Clarify: one card, and the clicked choice is exactly what the model receives in
the tool result. Approval: the gated command has not run before the click and
has run after Run once; Deny never runs it and the turn still completes.
2026-09-23 17:41:12 -07:00
teknium1
c62fe9e39f test(desktop-core): switch-back hydrate race, both orders forced (C2)
Parks the REST page read in main and the provider stream on gates so
message.complete lands before / after the switch-back hydrate
deterministically. complete-before-hydrate was 4/4 red on base (reply rendered
twice) and is green with the renderer fix.
2026-09-23 17:41:12 -07:00
teknium1
2bfa29b697 test(desktop-core): one live socket per backend; sampler names the duplicate bubbles
The non-default-profile step now asserts the renderer holds exactly one live
socket to the host backend (red when 0bb539b472, #120006, is reverted); the
earlier per-turn check alone missed that regression. Transient-duplicate
violations now report which bubble ids carry the copies.
2026-09-23 17:41:12 -07:00
teknium1
75908c1d23 test(desktop-core): boot handshake, supervised respawn, zero orphans (C5)
Class: Desktop boot / backend lifecycle - stuck boots, crash-loop or double
respawn, orphaned hermes serve / tool processes piling up after quit.

Real app + real backend: interactive composer and first turn with exactly
one backend; kill -9 the backend -> exactly one supervised replacement
(background /proc census catches transient extra spawns) and a working
turn; quit mid-turn with a running terminal-tool child -> zero processes
carrying the sandbox HERMES_HOME (orphans reparented to init included);
relaunch the same home 3x -> one backend per boot, zero after each quit,
transcript cold-hydrates exactly once.
2026-09-23 17:41:12 -07:00
teknium1
8fb196b3b6 test(desktop-core): transcript oracle across every chat transition (C2)
Class: duplicate / vanishing / reordered messages in the Desktop (#120005
and its dupes #119131 #119566 #119540 #118934; completed-reply refresh,
warm-resume, steer and reconnect regressions).

One real Electron app + one real hermes serve; only the LLM is faked by a
lane-private scripted provider keyed by each turn's own marker (no shared
trigger soup / global counters). After every transition - stream, tool-call
turn, reasoning turn, steer, queued follow-up, session switch mid-stream,
warm resume, reload, WebSocket drop+reconnect mid-stream (loopback proxy
the test controls), a non-default profile's chat, a forced second socket to
the same backend, final reload - the oracle asserts: persisted rows render
exactly once, in order, nothing unpersisted renders, no marker ever renders
twice even transiently, and the backend's concatenated deltas equal what
the provider streamed.

Why: the Desktop E2E job is disabled, so the 67 legacy specs run nowhere;
this is the small deterministic lane meant to be required (retries 0,
no fixed sleeps, one worker).
2026-09-23 17:41:12 -07:00
ethernet
9a8cdf96bf Merge origin/main into ethie/pm-clean
- tools/browser_tool_install.py: keep pm-clean's frozen old-updater stub; main's
  UTF-8 decode fix touched only the npx prefetch body it replaces.
- tests/hermes_cli/test_update_scoped_reconciliation.py: keep pm-clean's test
  subset (catch-up rides the PM completion owner) and take main's gateway-less
  host evidence (#120740): the updated seed that holds the host at a running
  gateway, and the two gateway-less matrices for the source change that merged
  cleanly into update_cmd_fleet.py.
2026-09-23 20:04:27 -04:00
teknium1
9d6e4e72a4 fix(desktop): a member that fails after pre-tool text is reported, not posted as its reply
Independent-review follow-up for the group room failed-turn fix.

- Group room poll: a retained error newer than the pre-submit snapshot
  (turn start replaces it with a fresh started_at) is this turn's failure
  and wins over transcript text. The core closer writes no failed-turn row
  behind a tool row, so "said X, called a tool, provider 401" left X as the
  newest assistant row and the room posted it, dropped the 401 and re-drove
  the member to the round cap (live repro on the PR head).
- Both pickers end the turn at a failed_turn row instead of scanning past
  it; with the retained error gone (backend restart) the row's notice is
  reported through the failed path instead of a silent pass / dropped
  stranded marker. The stranded harvest treats a retained error as the
  stranded turn's own the same way.
- REST cold-load/paging (/api/sessions/{id}/messages, /messages/around)
  type legacy untyped notice rows like session.resume does, via one
  read-side helper in agent/turn_failure_copy.py.
- Gateway closer: the fresh-session closure test now asserts the row's
  display_kind through a real SessionDB round-trip (red without the
  run_turn.py stamp).
- E2E: mock trigger that says text + calls a tool, then 401s; spec asserts
  the room reports the error and never posts that text.
2026-09-23 16:58:39 -07:00
teknium1
502031a4a3 test(desktop-e2e): restore the group approval-click and member-failure specs
Both were deleted by #120071 as failing on main. Triage: group-member-backend-failure
was a real regression (fixed in the previous commit); group-approval-click-submits
only failed under a leaked HERMES_YOLO_MODE (harness fixed two commits up) and passes
on a clean env. Restored verbatim from d9ca819cc4.
2026-09-23 16:58:39 -07:00
teknium1
c3d4007c00 test(desktop-e2e): the sandboxed app never inherits the caller's Hermes runtime env
A spec run from inside an agent terminal passed HERMES_YOLO_MODE,
HERMES_INTERACTIVE, HERMES_SESSION_ID... straight into the sandboxed backend.
HERMES_YOLO_MODE alone made group-approval-click-submits fail (the gated
rm -rf ran without a prompt), which read as an approval regression; CI never
has these vars. buildAppEnv now drops inherited HERMES_* except the
harness's own HERMES_DESKTOP_* / HERMES_E2E_* knobs.
2026-09-23 16:58:39 -07:00