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).
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.
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.
_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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
`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.
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.
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>
`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>
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.
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.
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.
- 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.
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.
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.
Salvage follow-up for #79314 / #82774: one sentence splitter for both the
client-direct streaming session and the sync (edge) fallback queue. Moves
cutSentences into the pure speech-text module (importers and its tests
now import it from speech-text directly; no re-export shim) and makes
IncrementalSpeechSentenceBuffer a thin think-block-aware wrapper over it. Also passes ownerRef scope to per-sentence
playback (grafted during conflict resolution).
Co-authored-by: XtremXpert <5651439+XtremXpert@users.noreply.github.com>
Both interim predicates now compare one (visible, streamed) pair from
_interim_visible_and_streamed so their normalization cannot drift; the two
#88954 codex tests collapse into one parametrized test.
With streaming enabled on Telegram, the streamed commentary is routinely
cut mid-text when the tool_calls finish arrives; the partial stream is a
prefix of the full commentary. The interim-message path used the
prefix-based _interim_content_was_streamed verdict, so the gateway's
_interim_assistant_cb called on_segment_break() — finalizing the
truncated bubble — and the tail ("...pick it u" vs "...pick it up") was
permanently lost (#88954).
That prefix semantics stays correct for the conversation-loop
"previewed" marks (the streamed prefix IS on the user's screen there,
and the contract test pins it per the #65919 review). The gateway
decision needs the stricter test: add _interim_content_fully_streamed
(exact normalized equality) and use it for the interim-message verdict.
Only an exact match may skip the full-text resend; a partial prefix
falls through to on_commentary() and re-delivers the complete text —
a benign duplicate, never lost text.
(cherry picked from commit 0b06a6660a1a4d47b4974f21ae42d7aeb1cbea15)
Add 思考/反思/推理/推敲 to THINK_TAG_NAMES so the streaming scrubber, CLI and
gateway stream filters and the final-response stripper all hide them, and
derive the auxiliary-client reasoning strip from the same list instead of a
hard-coded copy. Bare bracketless markers (unverified) are not covered.
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
The event bridge listed the text-delta methods twice (display handlers and
the #118410 liveness set) and duplicated _fire_delta's text extraction.
Derive both from _CODEX_TEXT_DELTA_METHODS, share _delta_text(), and hoist
the progress item-type set to a module constant (no per-event frozenset).
#86444 widened the large-context stale floor, the 1500s hard ceiling and the
TTFB scale-up/cap gate from OpenAI-Codex to every codex_responses route. That
made the #92302 local-endpoint TTFB branch unreachable (local TTFB fell back to
120s instead of agent.local_stream_stale_timeout) and silently clamped a local
server's configured stale timeout to 1500s. Evaluate is_local_endpoint once and
exclude local endpoints from the hosted clamps; xAI keeps the #86444 behaviour.
Note: xAI large requests now get the raised stale floor but keep first-event
idle semantics (progress gating stays OpenAI-Codex only).
The desktop/TUI reasoning pane is driven by reasoning.delta via
reasoning_callback; the scrubber-side collector only filled the final
reasoning_content, which extract_reasoning already recovers from the raw
content, so the pane stayed dead.
- Drop the scrubber reasoning collector (_reasoning_parts, reasoning(),
clear_reasoning(), \x00 sentinel, _THINK_TAG_RE) and its per-request
reset hook.
- StreamingThinkScrubber.feed() exposes the text it stripped from inside
think blocks as last_hidden; _fire_stream_delta forwards it through
_fire_reasoning_delta(inline=True) while no native reasoning delta has
arrived for this model response (reset per request) — no double
reasoning. CLI gating is unchanged: its reasoning_callback is None
unless show_reasoning/verbose.
- _finish_chat_stream fills reasoning_content from the raw content via
the existing extract_reasoning when no reasoning delta arrived.
- Replace the collector tests with two guards (live forwarding + native
suppression; _finish_chat_stream fallback), both red on origin/main.
Co-authored-by: SayHell0W0rld <852938468@qq.com>
Rejoin streamed pieces of one <think> block verbatim (the per-delta newline
join split sentences on every token) and clear collected reasoning in the
per-request _reset_stream_delivery_tracking so a tool-loop's earlier API call
does not bleed into the next call's reasoning_content (the scrubber is only
reset per turn). Follow-up to #90417 (#89647).
Providers that inline reasoning (MiniMax-M3 streams <think>…</think> in
content) never send a reasoning delta, so the desktop reasoning pane stays
dead even though the reasoning happened (#89647).
StreamingThinkScrubber (#17924) already strips inline blocks from streamed
content but discarded the text. It now collects the stripped text
(reasoning() accessor, tag markup removed), and the stream finisher
populates reasoning_content from the scrubber when the provider returned
no reasoning delta.
Hand-grafted from 759b65a8a4 (PR #90417) onto main's refactored scrubber and
_StreamingCall._finish_chat_stream; credential_pool.py commit dropped (out of scope).