Commit Graph

43461 Commits

Author SHA1 Message Date
ethernet
df1b647b42 fix(security): don't warn while tirith's first download runs
The first CLI launch after a PM install starts tirith's download in the
background, so ensure_installed() returns None and the CLI printed
"tirith security scanner enabled but not available". The scanner was on
its way, not missing; the same warning also fired when lazy installs are
disabled by the operator's own policy.

missing_is_expected() tells those by-design states apart. The CLI logs
them and keeps the visible warning for a missing explicit tirith_path or
a finished download that failed.
2026-09-24 14:23:47 -04:00
ethernet
cfeffe851d fix(windows): enable VT processing so console output renders colours
`hermes setup` launched by install.ps1 on PowerShell 5.1/conhost printed
`[35m`/`[0m` literally. hermes_cli.colors and the skins emit raw SGR codes
whenever stdout is a TTY, but no Hermes entry point ever set
ENABLE_VIRTUAL_TERMINAL_PROCESSING (origin/main didn't either; only
pm/cli.py did, for its own progress line), and shells hand native children
a console with VT off.

hermes_bootstrap now opts the stdout/stderr console in at import, before
anything prints and before the venv relaunch (the mode lives on the console
buffer, so the child inherits it). Non-console handles are left alone; a
console that refuses VT gets NO_COLOR so it shows plain text instead of
escape garbage. ctypes only: colorama is merely transitive.
2026-09-24 14:13:26 -04:00
ethernet
dec282c658 style(desktop): eslint --fix on the split local-models and remote-setup files 2026-09-24 14:12:16 -04:00
ethernet
de89d9c95b fix: update ui double string 2026-09-24 14:11:17 -04:00
ethernet
e78e3a77be refactor(release): move the author map out of release.py
release.py was 2,804 lines, 2,076 of them the frozen legacy author dict.
The map and its resolver now live in scripts/releases/: authors.py holds
the directory loader, the merged AUTHOR_MAP and resolve_author, and
authors_legacy.py holds only the frozen LEGACY_AUTHOR_MAP literal (same
1,895 entries, same order). release.py drops to 707 lines.

The contributor-check job and audit_pr_attribution.py grep the legacy
file for quoted emails, so both now read authors_legacy.py. The
importers (contributor_audit.py, add_contributor.py), contributors/README
and the tests read the defining modules. No behaviour change.
2026-09-24 14:08:22 -04:00
ethernet
d3f2872258 docs(i18n): note KittenTTS is unavailable on managed Python 3.14
The English TTS page already explains that the kittentts extra is gated
off on the managed 3.14 interpreter (misaki requires <3.13); the zh-Hans
page still presented KittenTTS as installable.
2026-09-24 14:08:22 -04:00
ethernet
25a22628ea docs(docker): document the full Chromium in every image
The Dockerfile now stages only PM's pinned full Chromium (no headless
shell) for both the slim and -desktop variants. docker.md still listed a
headless shell, and bot-screen.md claimed the official image had no
headed browser and that the -desktop build added Playwright's headed
Chromium. Correct both, note the size cost of the full build on slim
images, and describe the browser pick order browser.executable() uses
(managed first, system first only for non-root under restricted userns).
2026-09-24 14:08:22 -04:00
ethernet
1f264c7205 test(ci): evaluate stable-release and build-cache gates instead of spelling them
test_stable_release_graph and test_desktop_build_cache asserted gate text:
`== "always()"`, `== "false"`, `"conclusion != 'success'" in`,
`"!cancelled()" in`. Both files now evaluate the gate with the shared
evaluator, which also resolves `steps.*` now.

- The stable gates (acceptance, publication, complete) must run when every
  ancestor failed.
- Deferred desktop e2e never runs.
- The publication reconciler runs after a failed, dispatched Stable Release
  of this repository and on manual dispatch. It refuses a successful run, a
  push-triggered run, and a fork's run.
- No desktop-build-cache step runs during cancellation. The service-failure
  report runs when restore or save failed and stays quiet otherwise.
2026-09-24 14:08:22 -04:00
ethernet
3689d8c435 test(js): evaluate setup-pm and desktop cache gates, find legs by role
setup-pm-cache.test.mjs compared `if:` expressions as literal strings and
indexed `build-${platform}-${arch}-{release,commit}` by id. So the per-arch
split, or rewording a gate, broke it while the cache contract still held.

- A small evaluator for the Actions expression subset. Gates are asserted by
  how they behave:
  - The exact-key wheel save runs only for callers with extras or a test
    environment, and never together with the rolling restore.
  - The prune runs after a failure but not on cancel.
  - The upload needs a successful prune.
- Desktop build legs are the jobs that use the desktop-build-cache action,
  keyed by matrix target and cache-mode. A new check requires a write leg and
  a read leg for every native target. The write leg runs only for tag builds;
  the read leg runs for commit and channel builds.
- Candidate saves are evaluated: they run after a successful prepare of a
  trusted dispatch (a tag build, or a main push for the payload). They do not
  run on a failed prepare, a cancel, commit or channel builds, pull requests,
  other branches, or a foreign ref.
- Selection gates are found by SELECTED_BUILD_SUCCEEDED. Each needs validate
  plus one write and one read leg of a single target, and refuses a failed
  admission. The selection logic and the "only gates consume legs" invariant
  are covered in tests/ci/test_desktop_bundle_smoke.py.
2026-09-24 14:08:22 -04:00
ethernet
992433bde6 test(ci): drop hardcoded desktop job ids from summary, archive and canary tests
These tests still named desktop jobs by id, or compared `if:` expressions as
literal strings: build-win32-x64, build-win32-x64-release,
assemble-win32-bundle, publish-canary, builds-table, termux-deb.

- tag build summary: found as the table the canary publisher waits on. Its
  gate is evaluated to run over failed builds and to refuse a rejected tag,
  commit builds, dry runs, stable phases and channel builds. Failed needs are
  the selection gates, the termux builder and the Windows updater publisher,
  all found by role.
- termux/input archive: the validate archive step, the setup-pm archive step,
  the canary prune R2 steps and the Termux native gate are evaluated, not
  compared. evaluate() now resolves `env.*` for the R2-credential gates. "No
  build needs a separate archive job" now covers every job, not three ids.
- store eligibility and protected canary: jobs come from the assembler, native
  leg and canary publisher roles. Stage scripts come from the receipt the
  step stages.
2026-09-24 14:08:22 -04:00
ethernet
8f7da10406 test(ci): resolve commit and channel staging jobs by role
test_commit_build_staging and test_channel_build_publication ran staging
scripts from jobs named by id and step name, and checked literal `if:`
substrings. The per-arch build split broke them for that reason alone.

- Staging scripts come from the native leg for a target and trust branch, the
  universal assembler, or the termux builder. Within the job, the step is
  found by the receipt it stages (--commit-build or --channel-request). The
  summary and channel publisher are found by the table they render.
- Summary needs come from the summary job's own needs, not a copied list.
- "Selection env mentions needs.<id>-commit.result" becomes "no native leg is
  fail-fast". That is what the commit summary relies on. The selection logic
  itself is evaluated in test_desktop_bundle_smoke.
- Channel exclusion is evaluated: on every native leg the tag/commit stage
  stands down for a pinned channel build and the pinned-request stage runs.
  The commit-only status page is evaluated to run for a commit build and to
  stand down for channel and disposable-channel dispatches.
2026-09-24 14:08:22 -04:00
ethernet
37d3e32bbe test(ci): look up desktop release jobs by role, not by id
The desktop release tests indexed jobs by id and compared `if:` expressions
as literal strings, so splitting one build job into per-arch legs broke them
while every property they guard still held.

tests/ci/desktop_release_roles.py finds each job by what it does: the build
cache action a native leg uses, the receipt a job stages, the smoke workflow a
job calls, the jobs it needs. A lookup that matches zero or several jobs fails.
It also owns the expression evaluator, now with success()/failure() and step
semantics, so gates are asserted by evaluating them, not by substring.

- tag admission: every signing job checks out the admitted SHA or the run's
  own controller, and builds from the admitted SHA outside channel builds.
  This is checked by resolving each checkout per dispatch shape. Every native
  leg (not one hardcoded pair) checks credentials before it builds, and that
  check runs for each downloadable dispatch and not for a dry build.
- bundle smoke: native consumers come from the graph (direct needers of a
  selection gate or smoke that are not always() observers). Each must fail
  closed in every dispatch that admits it. A new invariant: only a selection
  gate may need a native build leg. The canary publisher must refuse every
  failed need, not a hand-picked list.
2026-09-24 14:08:22 -04:00
ethernet
07b39f1e8a test: split tests/conftest.py into topic modules under tests/_fixtures
tests/conftest.py had grown to 2,343 lines, past the 2,000-line gate. Move
three self-contained topics out, unchanged:

- env_filter.py: the credential / behavioral env-var name tables the
  hermetic fixture blanks;
- live_system_guard.py: the autouse live-system guard fixture, its marks
  and the protected checkout roots;
- platform_gating.py: the platforms() marker evaluation used by the
  collection hook.

conftest imports them instead of listing them in pytest_plugins: it is not
the rootdir conftest, and pytest fails a run that loads a non-root conftest
carrying pytest_plugins after startup. Imported fixtures register on the
conftest module under their old names, so autouse order is unchanged.
Tests that reached into the moved symbols now import the new modules.
2026-09-24 14:08:22 -04:00
ethernet
907b06fb28 test(git-safety): parse the git config key instead of substring-matching argv
The checkout guard decided whether `git config` writes a URL rewrite by
substring-matching every argument, so `url.<base>.pushInsteadOf` and the
Git 2.46 `git config set <key>` form slipped through, while a value that
merely contained "url." could trip it. Find the key positionally (skipping
option values and the subcommand) and match insteadOf/pushInsteadOf on it.
2026-09-24 14:08:22 -04:00
ethernet
29f8012beb build(docker): pin the debian:13.4 base by digest
uv and node already come from pm's sha-verified lock; the base image was
the last input pinned by tag only. Pin both FROMs to the multi-arch index
digest (sha256:e2d08da6f42ef4b09b165d55528a12727aeed8240dc9edf888e3ec07e10ef9da) so every arch resolves the same reviewed base.
2026-09-24 14:08:22 -04:00
ethernet
5956061fca fix(rehearsal): record the raised VSS cap before raising it
pre raised the shadow-storage cap to 128 GB and wrote the originals only
after the whole loop, so a pre that died mid-way left a raised cap with no
record for post to restore from. The cap must stay raised until post (it
is what keeps the snapshot alive during the update), so an exit trap is
wrong; instead record each original before raising it, and have status
list every recorded cap with the vssadmin command that puts it back when
post is never run.

The gateway stop the review flagged (`pkill -f "hermes gateway"`) is
already scoped: the .sh post runs `hermes gateway stop` under this
HERMES_HOME.
2026-09-24 14:08:22 -04:00
ethernet
2ba873b93a fix(install.ps1): resolve the bootstrap Python once per process
The full ladder runs every stage in one PowerShell process, and venv,
python-deps, source completion and launcher publication each called
Get-BootstrapPython, repeating the uv probe and `uv python find`.
Memoize the resolved interpreter in script scope. The existing
find-before-install order is unchanged.
2026-09-24 14:08:22 -04:00
ethernet
427ab1d58d test(desktop): require HERMES_PYTHON for mac-sign digest tests
The fixture imports pm and scripts.bundles, which only the prepared
runtime provides. Falling back to py/python3 on PATH failed later with
an unrelated ImportError; fail up front with a clear message instead.
2026-09-24 14:08:22 -04:00
ethernet
787c739a2a refactor(desktop): split the local models pane into scoped sections
ScopedLocalModelsSettings was one ~760-line component that threaded
`owner` into every child. The pane now provides its owner through
LocalModelsOwnerProvider (still keyed by owner, so each connection/profile
remounts), and renders sibling components that read it from context: the
quickstart hero, runtime, hardware, models (catalog and sideloaded rows)
and browse sections. Request actions live in local-models-actions.ts and
keep the current-owner toast fence.

Two intentional differences: the delete spinner is per row instead of one
shared id, and the runtime install/update buttons pass the pane's owner
and QueryClient explicitly (the unused handleInstallRuntime is gone).
2026-09-24 14:08:22 -04:00
ethernet
fd19d439cb refactor(desktop): split remote setup into probe, test and OAuth hooks
useRemoteSetup covered the credential form, the connection test and the
OAuth login in one hook. The test/feedback leg now lives in
useRemoteConnectionTest and the login/logout leg in useRemoteOAuth, both
fenced by the probe's target generation as before. useRemoteSetup keeps
the form state and composes the three legs; its public shape is unchanged.
2026-09-24 14:08:22 -04:00
ethernet
a36dd91ef1 refactor(update): split _marker_only_restart_obsolete below CC 30
The fleet-restart-pending discharge parsed the marker inventory and
probed a gateway-less host inline (CC 36). _marker_owed_gateways now
turns the inventory into the owed set (raising ValueError, which the
caller's existing handler already maps to "keep the marker"), and
_discharge_gatewayless_marker owns the #118742 host probe. Every
verdict is unchanged; the caller is at CC 21.
2026-09-24 14:08:22 -04:00
ethernet
a67891b0ee refactor(update): split hermes update --check into update_cmd_check
_cmd_update_check (CC 30, 224 lines) did debris cleanup, channel
resolution, the scoped upstream/origin fetch, shallow-graft repair and
two verdict printers inline. Those steps move to the topical sibling
hermes_cli/update_cmd_check.py; the facade function (a frozen updater
surface) stays in update_cmd and orchestrates them at CC 10.

Output, exit codes and git argv are unchanged. The sibling reads
_capture_head_sha / _no_prompt_git_kwargs through the facade and
imports gitlock, source_releases, source_check and config per call, so
existing monkeypatch seams keep reaching production.
2026-09-24 14:08:22 -04:00
ethernet
672a65e6bc refactor(recovery): split the two CC>30 early-recovery functions
recover_if_needed (CC 33) and _restore_holding_claim (CC 30) carried
their failure bookkeeping and git put-back steps inline. Extract
_missing_environment, _count_failed_attempt and _put_back_paths so
both land at CC 24-25 with the same ordering: a failed restore still
skips the rm, and the attempt bump stays best-effort per marker.
2026-09-24 14:08:22 -04:00
ethernet
beb88a13b1 refactor(update): table-drive the steward refusal rungs
The sealed-tree steward ladder in evaluate_update_admission was four
if-rungs that differed only in the install method whose command
remediates the tree. A steward -> method table plus one helper replaces
them; the refusal code, message and command are unchanged for docker,
nix, apt-termux, desktop-app and unknown future stewards.
2026-09-24 14:08:22 -04:00
ethernet
ed76cbf9bb test(pm): skip host-Python fixtures by what the prefix contains
The cold-runtime e2e and the uv dependency-leg test ship or digest the host
interpreter as if it were a standalone Python. The earlier skips matched one
file (sitecustomize.py) and one path (/usr), so /usr/local, Homebrew or a
distro stdlib with other absolute links still failed.

Skip instead when the shipped stdlib holds a link tarfile's data filter
refuses, or when the prefix has unreadable files or is far larger than a
single interpreter.
2026-09-24 14:08:21 -04:00
ethernet
6697d8b01c fix(update): collect superseded dependency generations after a sync
Only a manual `hermes pm gc` reclaimed old app and PM runtime generations
on native installs; the Docker boot already collects after its refresh.
A successful venv sync (hermes update and launch-time completion) now runs
the same collectors, which keep selected, leased and day-young generations
and skip when another install holds the lock. Cleanup errors are logged and
never fail the committed update.
2026-09-24 14:08:21 -04:00
ethernet
1991e100b5 fix(pm): lease the PM runtime generation before spawning its child
runtime_python() released .prepare.lock on return and the worker/CLI child
only leased its generation once its own code ran. A publish plus `pm gc` in
that window could remove the generation the child was starting from.

The resolving process now leases the generation it returns while still
holding .prepare.lock (which the collector also takes), once per generation
for the life of the process, so the child is covered until it holds its own.
2026-09-24 14:08:21 -04:00
ethernet
6dc1288dd1 refactor(pm): route .deb payloads through the one tar extractor
DebPackage kept its own 116-line member walker next to
pm.store.extract_tar. Both enforced the same containment rules, so the
.deb data.tar now goes through extract_tar (which accepts an open
stream) and FilterError maps to InstallError. Link fallbacks on hosts
without symlink support come from tarfile's own copy fallback, which
also resolves chained aliases. The Termux mode normalization stays in
DebPackage, since it is .deb policy and not extraction.

The per-member FilterError/OSError swallow in pm/packages.py was already
removed; Git's MSYS /proc link exemption is unchanged.
2026-09-24 14:08:21 -04:00
ethernet
477565073a refactor(pm): extract worker command and error mapping from _request
Choosing the worker command (including the cold-runtime lazy refusal and
its receipt) and mapping a worker error back to a caller exception move
out of pm.client._request. Behaviour is unchanged; CC drops from 40 to 26.
2026-09-24 14:08:21 -04:00
ethernet
3e256363a4 refactor(pm): split cmd_install and cmd_update into their phases
cmd_install: flag validation and the venv/test-environment phase move
to helpers (CC 40 -> 18). cmd_update: resolution, per-row report, pin
application, uv and npm refreshes move to helpers (CC 51 -> 22). The
messages, order and exit codes are unchanged; a dead `target` local in
cmd_update is dropped.
2026-09-24 14:08:21 -04:00
ethernet
581bb440fd refactor(pm): split _install into recovery, currency and copy helpers
Extract settling an interrupted publication, the facts/stage-marker
currency check, the verified bundled copy and the repair log out of
pm.install._install. Order of checks, locking and errors is unchanged;
CC drops from 40 to 25.
2026-09-24 14:08:21 -04:00
ethernet
9b4ed5f5ec refactor(pm): one plugin-input request for sync_venv and venv_is_current
sync_venv took four mutually exclusive plugin kwargs (plugin_dirs,
extra_plugin_dirs, selection, staged_plugin) and venv_is_current two, with
runtime ValueErrors guarding the combinations. Replace them with a single
`plugins=` argument typed as one of pm.plugin_inputs.Members, Candidates,
Selection or StagedUpdate, so a conflicting request cannot be expressed.
The module also owns the worker wire encoding that client.py and worker.py
each duplicated.

pm.install.sync_venv is split into cohesive helpers (feature policy,
install lock, publication snapshot, target selection, commit) with the
same ordering, receipts and recovery; its CC drops from 54 to 16.

All in-tree callers and tests move to the new argument.
2026-09-24 14:08:21 -04:00
ethernet
b728fdd627 refactor(pm): own the lock/atomic-write primitives in pm.filesystem
pm imported runtime_state's private helpers (_lock, _atomic_bytes, _bytes,
_digest) at a dozen sites while runtime_state imports pm.environments at
module top. The primitives are pm's: move them into the stdlib-only
pm.filesystem as lock_fd, durable_write_bytes, read_bytes_or_none and
file_digest, and repoint every pm caller.

runtime_state keeps the private names only as import aliases: it still
calls them through its own globals, and pre-PM updaters load them by these
names mid-swap (tests/compat/old_updater_surface.json).

Boot-subset test fixtures now copy pm/filesystem.py, since runtime_state
imports it at process boot; worker-injection tests patch the name
pm.publication now reads.
2026-09-24 14:08:03 -04:00
ethernet
0377e9d814 test(docker): the image allows opt-in installs now
72df5aa60e dropped HERMES_DISABLE_LAZY_INSTALLS=1 from the image: opt-in
extras install into PM generations under $HERMES_HOME, never the sealed
/opt/hermes tree. The hosted write-policy smoke test still required the
refusal, so the desktop/amd64 image job failed. Assert it is unset.
2026-09-24 13:50:17 -04:00
ethernet
f83a9e9a45 fix(build): announce npm ci and show npm's spinner
The workspace npm ci printed nothing until 'added N packages': builders
set CI=1, which turns npm's progress off, and the stage name only went
to the desktop UI's status file.

Print a line before npm ci, and pass --progress=true so a terminal gets
npm's spinner back (npm still shows it only on a TTY, so piped output
such as the desktop app's log stays clean). The flag stays out of the
receipt-keyed args, so existing installs are still reused.
2026-09-24 13:43:40 -04:00
ethernet
372979c3de Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 13:40:17 -04:00
ethernet
de2029fb2c fix(docker): drop the pipes from the tool smoke checks (DL4006)
The build only needs the binaries to run; the version banner piped through
head tripped hadolint and could mask a failing ffmpeg behind head exit 0.
2026-09-24 13:40:16 -04:00
ethernet
553d93502e fix(install.ps1): hide scary warning 2026-09-24 13:36:02 -04:00
ethernet
dc11e3b3bc feat(release): add --skip-bundles and --skip-tests to stable releases
`release.py release` gains two flags. They can be used together.

--skip-bundles ships only the claim, the GitHub release, the final tag
and the Docker image. No desktop, Termux or PM bundle job runs. The
final tag records candidateManifestSha256: null. Publication moves only
the Docker stable/latest aliases. The R2 stable head, feeds, APT, the
downloads page, the signed-package baseline and the Store stay on the
previous bundle release.

--skip-tests builds, signs and publishes every artifact and runs no
test job: source CI, Nix, PM bundle check, Termux, Windows live,
install/update E2E, bootstrap identity, native smokes, upgrade
acceptance, tests/docker and the in-build vitest step. The candidate
manifest records each smoke as skipped, never as passed.

The flags live in the claim message (skipBundles, skipTests), next to
autopublish. They are not workflow inputs, so a rerun cannot change
them. admit emits them, and every job condition and gate reads them.
stable.validate_claim and stable.validate_final are now the one shape
check for stable.py and the sequencer.

The gates stay strict. SKIPPED_BY in stable.py maps each job to the
flags that remove it. `gate` requires those jobs to report skipped and
every other gated job to report success. A job that ran although a flag
removes it blocks the release.

A release that skipped bundles never moves the R2 stable head. Two
readers depended on that head:

- The next version was derived from it, so the next cut would reuse the
  version. It now takes the newer of the R2 head and the newest
  published non-prerelease GitHub release with a vX.Y.Z tag. Bare v*
  tags do not count, because those refs are not protected yet.
- The sequencer used it to decide which published releases still need
  their publication pass, so a bundle-less release would re-advance
  every 15 minutes. The head is now the newer of the R2 head and the
  published release whose final tag binds the Docker stable alias
  digest.

`release` also refuses a cut when its next version already has a final
tag. That closes the window between the final tag and the public
release, where the published identity still names the old version.

Tests: 42 release test files, 546 passed. Three tests fail on this
Windows host, and they fail the same way on a clean HEAD worktree:

- test_stable_release_graph::test_docker_recovery_refuses_to_replace_a_divergent_version_tag
- test_release_artifacts::test_windows_metadata_is_read_from_package_and_stale_stamp_is_rejected
- test_tag_builds_summary::test_admitted_failure_publishes_tag_info_without_promoting_channel[True]

Not verified: no real Stable Release dispatch ran with either flag, and
actionlint is not installed on this host. The workflow changes are
checked by the graph tests and by running the phase-result step script.
2026-09-24 13:31:33 -04:00
samuelpatro
130b8f2c5d fix(desktop): run cloud agent silent sign-in at boot
A Hermes Cloud agent authenticates through the silent per-agent cascade
(cloudAgentSilentSignIn), which was only ever invoked from the settings
"Use gateway" button. Boot went straight to waitForHermes, so once the
agent's session cookie expired the ticket mint answered 401, the app
reported "not signed in" and latched reauth, even though the portal
session it needed to recover was still live. Every relaunch needed a
manual click.

Boot now runs the cascade once and retries once, only for remoteKind
cloud + authMode oauth on the terminal reauth error and only with a live
portal session. Everything else surfaces unchanged.
2026-09-24 13:23:37 -04:00
Austin Pickett
4dc7a23690 fix(desktop): adopt an unmarked copy of a package's own desktop half
Folders already installed by shipped builds have no marker, and reconcile
refused them on every pass, so the stuck "copying…" row never recovered.

Reconcile now stamps such a folder in place when its `plugin.js` is byte
for byte the package's own half — that can only be our pre-marker copy,
and adopting it writes no files. Anything that differs is a standalone
plugin the user installed and is still never touched (#112450).

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-09-24 13:17:12 -04:00
Austin Pickett
0d914a0652 fix(desktop): stamp the package marker when installing a unified plugin from git
`installDesktopPluginFromGit` published the desktop half with no
`.hermes-package.json`, unlike every other publisher. Without that marker
the Plugins page has no evidence the copy belongs to the agent package:
the agent row sat on "copying…" forever beside a second, standalone row,
and the half loaded default-enabled instead of opt-in.

A repo carrying both halves now lands under the AGENT package name and is
stamped as part of the same staged publication, so this path and
`reconcileUnifiedDesktopHalves` converge on one folder (#100412) instead
of racing to two. A desktop-only repo is unchanged: git-derived folder
name, no marker, standalone.

The Plugins-page test moves with it — pairing is the marker's job, so the
page keeps an unmarked copy as its own row rather than guessing from the
folder name.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-09-24 13:17:12 -04:00
xxxigm
b372ffa859 test(desktop): pin pairing for a marker-less desktop plugin copy
A catalog install can leave desktop-plugins/<name>/plugin.js with no
package marker. The Plugins page must show that copy on the agent row
instead of a second row stuck on copying.
2026-09-24 13:17:12 -04:00
ethernet
05a78671a7 Merge remote-tracking branch 'origin/main' into ethie/pm-clean 2026-09-24 13:13:35 -04:00
kshitijk4poor
d5785bb525 fix(desktop): stop the sentence-fallback poll on unmount and session drop (#79314)
The incremental Edge/non-streaming TTS fallback re-polls the pending reply every 150 ms until the response finishes. Nothing cancelled that timer when the hook unmounted or the speech session was dropped, so a tick could fire after teardown (CI: 'ReferenceError: window is not defined' at use-voice-conversation.ts poll, from the rearm test file). The poll's cancel is now held in a ref, cleared by dropSpeechSession() and by an unmount effect.
2026-09-24 22:42:17 +05:30
kshitijk4poor
dbc4780a08 test(desktop): poll for 'listening' after re-arm in voice rearm tests
status flips to 'listening' only after handle.start() resolves, so the
synchronous assertion right after waitFor(start x2) raced (2/9 flakes on
'speaks completed fallback sentences before the response finishes').
Wrap it in waitFor; same assertion, no longer timing-dependent.
2026-09-24 22:42:17 +05:30
kshitijk4poor
feb503d68f fix(desktop): edge-TTS fallback flushes sealed interims and skips stream re-probes
- poll: flush the sentence buffer when the interim bubble is sealed but a
  tool is still running (mirrors feedSpeechSession's session.flush), so its
  trimmed last sentence isn't held for the whole tool run.
- playSpeechText gains a syncOnly option; the per-sentence fallback uses it
  so each sentence no longer re-runs directTtsConfig/resolveSpeakStreamUrl
  and a speak-stream WebSocket that already answered fallback.
- single ownedSequence baseline per sentence.
2026-09-24 22:42:17 +05:30
kshitijk4poor
299fc88f6e fix(desktop): make the edge-TTS sentence buffer a pure cutSentences accumulator
Drop the ad-hoc <think> handling (narrower than markdown-preprocess's
REASONING_TAGS; prose mentioning '<thinking' held the rest of the reply
until finish) and the unused minSentenceChars parameter. The fallback is
fed text-parts-only reply text, and the streaming session's ingest does no
think stripping either, so this restores parity.
2026-09-24 22:42:17 +05:30
kshitijk4poor
e71cdf328f style(desktop): prettier on voice-playback import after re-export removal 2026-09-24 22:42:17 +05:30
kshitijk4poor
e84313f65b test(desktop): trim edge-TTS sentence-queue tests to two invariants
Keep: speaks completed sentences before the reply finishes; Stop halts the
queue without re-arming. Buffer unit tests dropped — the splitter is
cutSentences(), already covered in speech-text.test.ts.
2026-09-24 22:42:17 +05:30