Repository navigation
fix(title): T3 Code handoff and instruction wrappers no longer name sessions - #3
Closed
IEatCodeDaily wants to merge 10000 commits into
Closed
IEatCodeDaily wants to merge 10000 commits into
IEatCodeDaily wants to merge 10000 commits into
Conversation
…ce lint The rule could not tell a bundled library's own classes from patching a renderer global: it kept failing legitimate bundles (three.js once minified to $-names) and still missed a bare $ receiver. Prototype writes in a Desktop plugin are now read by the catalog reviewer instead; the policy in the catalog docs stays, with a note that review checks it by hand. eval/new Function, non-SDK imports, script injection and app-DOM reach are still linted.
…hing
A plugin whose pyproject.toml declares `dependencies = []` reached the
Python-dependency consent question with nothing to list. Non-interactive
`hermes plugins install --enable` refused it ("dependency install skipped
(non-interactive)"), and in a terminal a default answer left it installed
but disabled, so official plugins had to drop their pyproject.toml.
The gate keyed on workspace membership, which every pyproject has. Ask
only when admission would put packages in the shared environment
(`PythonDeclaration.installs_packages`): applicable dependencies, a
buildable plugin package, extras, or dependency groups (uv sync
--all-packages installs a member's default groups). The install gate,
the active-replacement veto at publication and the update re-consent
all read it. PM membership and requires-python handling are unchanged.
Consent-test fixtures that used an empty pyproject to stand for "declares
Python dependencies" now declare one; their assertions are unchanged.
…ousResearch#142565) * fix(desktop): sync the approval-mode chip when the gateway opens The statusbar approval chip seeds an optimistic 'smart' default and fetches approvals.mode from a mount effect. The shell mounts before the gateway WebSocket is open, so at every cold start the request rejects with "Hermes gateway unavailable"; the swallow (.catch(() => undefined)) hides it and the effect deps ([profile, requestGateway]) never change when the socket opens — the chip sits on 'smart' for the whole session even when the configured mode is 'off', correcting itself only when a later session event reconciles the shared store. Gate the sync on gatewayState === 'open' and include it in the effect deps so it runs when the socket actually opens (and again after a reconnect) — the same pattern useStatusSnapshot uses for its boot legs. Test: with gatewayState="connecting" no request is issued and the chip shows the pre-sync default; rerendering as "open" runs the sync and the chip settles on the configured mode. Fails on base, passes with the fix. * fix(desktop): only the active gateway's runtime sets the approval-mode chip applyRuntimeInfo takes the owner scope of the runtime info, and the chip is updated only when that owner is the active gateway (same profile, same connection, a missing connection id counting as local). A bare profile owner counts as active only while the primary or a connection-less socket is active. All five callers pass their owner. Salvaged from NousResearch#125174 onto current main, where the runtime side effects now live in reportRuntimeInfoAppState; the owner is threaded through it. Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> * fix(desktop): address review nits on the approval-mode chip fixes Reword the runtime-owner comment to name how it differs from the session.info rule, and add the blank line eslint wants in the boot-sync test. --------- Co-authored-by: Muntasim Ul Haque <106990977+muntasimulhaque@users.noreply.github.com> Co-authored-by: John Paul Soliva <soliva.johnpaul@icloud.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
b1aa63b folded the pinned-input archive step into the `validate` ("Pre-build setup") job, which kept its 5-minute cap from when it only ran admission. The step re-downloads every pinned input from R2 to verify its sha256 (several GB: Chromium, llama.cpp CUDA). That takes ~20 s normally (v0.22.3 rc.5), but v0.22.4 rc.1 hit slow R2 reads: both Windows candidate chains were cancelled at 5:09 mid-verify, and every downstream job failed "Release blocked: validate=cancelled". The standalone archive job it replaced had 60 minutes; give the merged job 30.
…c7w-9gw2) certifi 2026.10.19 drops the emSign roots whose intermediate CA keys were compromised. It was published 2026-10-20, inside the 3-day `exclude-newer` lock quarantine, and holding a CA revocation for the quarantine keeps the compromised root trusted for those days. CA trust stores therefore get the one per-package exemption (`exclude-newer-package = { certifi = false }`). The packaging contract test now allows exemptions only for CA-trust packages and only as a full opt-out, so any other package or a dated exemption still fails. The pm/AGENTS.md and package-management docs state the exemption.
…computer_use and browser follow into the Space; no core overrides)
…-2026.10.19 fix(deps): bump certifi 2026.5.20 → 2026.10.19 (critical, GHSA-6q48-6c7w-9gw2)
…587h-7wmj-h5pm) Also fixes GHSA-r825-w8mm-5pxw and GHSA-698v-4r3x-3vcv. Transitive via webpack-dev-server ^2.14.1.
…3-v3hh) The website override pinned js-yaml to 4.3.1; it moves to 4.3.2, which matches the root override.
…62r-hmrv) Matches the root lock's 2.10.0.
) Also fixes GHSA-4vpr-x523-8j87. Transitive via @svgr/plugin-svgo and postcss-svgo.
vite 8.2.4 bundles launch-editor fixes for GHSA-587h-7wmj-h5pm, GHSA-r825-w8mm-5pxw and GHSA-698v-4r3x-3vcv. Bumped in all three workspaces that pin it (web, apps/desktop, apps/bootstrap-installer), leaving one hoisted copy. Forced by vite 8.2.4's ranges: - rolldown ~1.2.4: 1.2.1 → 1.2.13 (@oxc-project/types =0.153.0) - postcss ^8.5.26: root override 8.5.23 → 8.5.29; website lock moved to the same 8.5.29 The .npmrc age-gate excludes for vite/rolldown/@rolldown/*/@oxc-project were marked "remove once vite 8.2.0 is > 2wks old" (it is 82 days) and let a same-day rolldown 1.2.15 into the lock, so they are removed; every version above is past the 14-day gate. The website postcss exclude ("remove when 8.5.23 is > 2wks old") is removed for the same reason.
…6-jgq8) Also fixes GHSA-8r9p-f939-6h3c. Transitive (axios in the root lock, http-proxy in website); bumped in both locks.
Also fixes GHSA-73wf-gq98-2v4g. Website 4.28.6 → 4.29.3, root 4.29.0 → 4.29.3, so both locks match. Forced by 4.29.3's ranges, with the same versions in both locks: baseline-browser-mapping 2.11.27 (also fixes website GHSA-w5vr-8v7q-w6rv), caniuse-lite 1.0.30001815, electron-to-chromium 1.5.450, node-releases 2.0.57 and update-browserslist-db 1.3.3.
…-gh2g) Also fixes GHSA-x5fp-wj9c-mxmx. express and body-parser pinned qs ~6.15.1, so they move to the first releases on ~6.16.0: express 4.22.2 → 4.22.3, body-parser 1.20.6 → 1.20.8.
Fixes GHSA-c7q8-3ch8-vqpv, GHSA-27p8-2357-5qqv, GHSA-3px3-54cx-rmw9, GHSA-8344-3jmq-59r6, GHSA-vr34-hp96-76pp, GHSA-4w3w-2rp5-g8jm, GHSA-w2rr-34g9-rvrj, GHSA-93r5-fhx6-vmg9, GHSA-6h8r-xr42-gp59 and GHSA-6gmq-8vp8-gcm6. Both copies are transitive via plist, within its ^0.8.8 / ^0.9.10 ranges.
3.14.4 fixes GHSA-9xw5-w5jj-pvcg, GHSA-2g87-pp6c-29x5, GHSA-5wr8-gcqf-gqf3, GHSA-f9xw-jh8m-3mwq, GHSA-pqxq-w44r-73xw, GHSA-6cjc-53gc-5rqm, GHSA-gx76-c99x-2c96 and GHSA-494j-54xm-rwp3. 3.14.5 is the latest patch release and is past the 3-day lock quarantine. All five exact pins (messaging, slack, matrix, sms and teams extras) move together with their advisory comments. aiohttp 3.14.5 requires yarl>=1.25.1, so yarl 1.24.5 → 1.25.1 in the lock.
…_task fake The drain test patches asyncio.create_task process-wide to delay the /v1/runs task. aiohttp 3.14.4+ schedules its own client and connector tasks through asyncio.create_task(..., eager_start=True), so the fake failed with "unexpected keyword argument 'eager_start'". Accepting **kwargs alone is not enough: the fake would then hold aiohttp's connect task behind the same gate and client.post would never return. The fake now delays only the run coroutine and passes every other task (kwargs included) straight to the real create_task.
…-3.14.5 fix(deps): bump aiohttp 3.14.3 → 3.14.5 (high, GHSA-9xw5-w5jj-pvcg)
…editor-2.14.2 fix(deps): bump launch-editor 2.14.1 → 2.14.2 in website (high, GHSA-587h-7wmj-h5pm)
…-4.3.2 fix(deps): bump js-yaml 4.3.1 → 4.3.2 in website (high, GHSA-2883-xcg3-v3hh)
…2.10.0 fix(deps): bump colord 2.9.3 → 2.10.0 in website (medium, GHSA-2wm5-q62r-hmrv)
fix(deps): bump svgo 3.3.4 → 3.3.5 in website (high, GHSA-w27v-7q3p-w38r)
fix(deps): bump vite 8.2.0 → 8.2.4 (high, GHSA-587h-7wmj-h5pm)
…redirects-1.16.1 fix(deps): bump follow-redirects 1.16.0 → 1.16.1 (high, GHSA-q3jh-wrc6-jgq8)
…slist-4.29.3 fix(deps): bump browserslist → 4.29.3 (high, GHSA-c83g-rgw3-j3cx)
fix(deps): bump qs 6.15.3 → 6.16.0 in website (medium, GHSA-4mjr-xmp4-gh2g)
…0.8.15-0.9.12 fix(deps): bump @xmldom/xmldom 0.8.13 → 0.8.15, 0.9.10 → 0.9.12 (high)
MCP servers previously saw the python SDK's default clientInfo
("mcp"/"0.1.0") instead of who is actually connecting. Send
Implementation(name="hermes-agent", version=<shipped version>) in
ClientSession kwargs so server logs, policy engines, and dashboards
attribute sessions correctly — the same identity fix GitHub Copilot
CLI shipped in v1.0.84-2 ("MCP servers now see the same client
identity ... carrying the shipped CLI version instead of 0.0.0").
Salvaged from NousResearch#70074 (yulunz); relocated from the pre-decomposition
tools/mcp_tool.py onto the transport sibling's _session_kwargs(),
the single seam all three transports (stdio, streamable HTTP, SSE)
build their ClientSession from, and gated on the SDK actually
accepting client_info.
…lemetry-1.45.1 chore(deps): bump opentelemetry to 1.45.1 and mistralai to 2.10.1
NousResearch#139099) The source check offered v0.22.2 as "ready to install" to installs whose session stores already use the gateway runtime, and `hermes update` then refused it as a downgrade every time. The rollback test only caught a release that is an ancestor of HEAD; a release cut beside the checkout's history (sene1337's report on NousResearch#139099) slipped through as a plain update. The check now asks the updater's own refusal, read-only and without lazy fetches (update_downgrade_guard.would_refuse, sharing downgrade_refusal's profile scan and target test). When the update would refuse, the status says updateAvailable=False with held="downgrade": - Desktop names the hold ("Hermes stays on this build until a newer stable release ships") instead of an Update now button that always fails. - `hermes update --check` prints "Holding at this build" instead of "Selected release available". channelProtocol moves to 2 so cached statuses computed without the hold are re-checked. The refusal in `hermes update` itself is unchanged.
…untime A stable-channel install on a post-cutover main commit could never update: the channel's release predates the gateway runtime its state.db already uses, so every `hermes update` hit the downgrade refusal and exited 1, though the user never asked to go back (NousResearch#140729). When the channel (not an explicit --branch) picked that release, keep the install on HEAD, say it is ahead of the release and how to follow main, and finish like an up-to-date run. An explicit --branch to a pre-runtime commit is still refused. (cherry picked from commit 6127cdc)
…shapes The salvaged hold is pinned on a preview channel. These cases pin it on the shapes NousResearch#140729 and Desktop actually reach, through the real _cmd_update_impl against real git and a state.db with runtime ledger rows: - an unconfigured install whose default channel is stable, on a main commit beside the release (neither contains the other): plain `hermes update --yes`, and the argv scripts/desktop-update/posix.sh runs (`--yes --keep-stash --channel stable`); - an install whose channel is recorded as stable (Desktop's Settings selector, `--set-channel stable`) on a checkout that contains the release, where forward-only never applies. Each holds HEAD, prints the ahead-of-release notice instead of the refusal, and completes like an up-to-date run; all four fail on the base with SystemExit(1) at the downgrade refusal. --branch to the same commit stays refused (covered by the salvaged test). Refs NousResearch#134889 (GW-19) Refs NousResearch#140762 Refs NousResearch#140729
…s held Review follow-up on NousResearch#140741, carried onto NousResearch#140778: the release can sit beside HEAD's line rather than behind it, so "ahead of" claimed more than the guard proves; use "newer than". The run no longer closes with "Already up to date!" under the hold notice: _finish_already_up_to_date takes a held message. Drop the redundant expected_sha pin (the up-to-date path recaptures HEAD). _assert_held_at_head now checks the new wording and the held closing line. (cherry picked from commit 0dc55e8)
target_predates_runtime greps a release tree for _RUNTIME_SCHEMA_MARKER. If the DDL in hermes_state*.py drifts from that literal, every later forward release reads as pre-runtime; with the channel hold that becomes a silent pin instead of a loud refusal, so fail CI on the drift instead. (cherry picked from commit 0241a85)
…target update_cmd.py sat at ~2000 lines after the hold landed. Move _hold_channel_release_behind_runtime unchanged into update_cmd_check as hold_channel_release_behind_runtime, beside the channel target it acts on. Facade helpers (_m, _capture_head_sha) are read through _uc() at call time like the rest of that module, and downgrade_refusal is still imported per call, so no import cycle and existing patches stay effective. No behaviour change. Refs NousResearch#134889 (GW-19) Refs NousResearch#140762 Refs NousResearch#140729
… not Review follow-up for the consolidated hold (NousResearch#140215 + NousResearch#140778), closing the three open findings on NousResearch#140215: - `hermes update --check` asked the rollback question before the refusal, so a release this checkout already contains still printed "`hermes update` moves it back to that release" while the update itself holds. The refusal now comes first; the copy no longer says "move back", since the update holds rather than refuses. - source_check cached its verdict for 24h with the hold baked in, so an offer cached before the first runtime turn stayed on offer after it. The hold is now re-asked on cached channel results too (a local git + sqlite probe, no network). - The dashboard /update/check endpoint turned the hold's behind=0 into "You're on the latest version." and dropped it, and Desktop's backend card returned its generic latest line before looking at `held`. The endpoint now carries `held` + the release version, mapBackendCheck keeps them, and the held line wins over the backend fallback.
The hold asked the release tree, via local git, whether it defines the gateway runtime. An install that tracks only `main` never has a release cut beside it until `hermes update` fetches it, so `cat-file -e` failed, the guard's "a git failure proves nothing" rule answered "offer", and Desktop showed v0.22.2 as ready to install for exactly the installs NousResearch#139099 is about (DavidMetcalfe's review on NousResearch#140215). target_predates_runtime now takes an optional `remote` answer for a target that is not local. The check surfaces (source_check, `hermes update --check`) pass one that lists the release's root tree from the GitHub API and reads its hermes_state*.py from raw.githubusercontent.com (no API quota). Both backends share the marker and the module pattern (STATE_MODULES, defines_runtime), so they cannot drift apart. The guard stays the single place that decides. - Only consulted when a store has runtime history and the commit is absent: everyone else makes no extra request. - A definite verdict is cached per sha for good (a commit's tree never changes) in source-checks/release-runtime.json. - A truncated tree, an unreadable module, or any network failure answers None and keeps the old behaviour (offered). - `hermes update` itself is unchanged: it fetches the release first and stays git-only. Co-authored-by: DavidMetcalfe <80915+DavidMetcalfe@users.noreply.github.com>
Check that a release's schema objects are present before trusting a local no-match, and use the remote reader when the local probe cannot finish. Cache release facts before applying the live store-dependent hold, so removing the only ledgered profile makes an allowed release visible again. Read independent schema blobs four at a time with one lookup deadline. Daemon workers cannot hold a version command open after that deadline; unknown verdicts expire instead of retrying GitHub on every cache hit. Check marker drift against imported SCHEMA_SQL rather than source text, and keep dashboard hold and rollback copy readable without a version.
Keep version-only release commits in the path-limited completeness probe with --sparse. Git 2.53 otherwise simplifies them away, then mistakes an unreadable schema blob for an absent runtime marker. Cover inherited pre-runtime and runtime schemas in real blobless clones. Clear rollback when projecting a live hold so --version cannot promise a move the updater will not make. Extend the store/cache regression to in-history releases and exercise the real version renderer; removing the only ledgered store still restores the cached rollback offer. Verified the ten affected test files on Git 2.53.0 and 2.55.0: each run passed 218 tests with one platform skip. Both regressions failed before their respective production fixes.
On Windows without distlib in the runtime environment, a repin of PM's Python republishes the source launcher as a .cmd and must retire the old hermes.exe so it cannot shadow it. stage_launcher deleted it, but the exe it has to retire is usually the one running `hermes update` (or the launch retrying the owed step), and Windows refuses to delete a mapped image. The publication failed, the update owed 'launchers', so the install stamp and bootstrap receipt were withheld at the old commit and the old exe kept booting the old interpreter. Every retry ran from that same exe and failed the same way. Windows does allow renaming a running image, so retire it by renaming it aside when the delete fails (distlib's .deleteme move), and collect the aside copy on a later publication once its process has exited. Seen in the v0.22.5 rc.1 install E2E: installer-script -> hermes-update (v0.22.4 -> HEAD) on Windows, where pm/lock.json moved Python 3.14.7 -> 3.14.9 (e31fa4c): "Update follow-up 'launchers' did not finish: source launcher publication failed", then verify-stamp "pinnedCommit 4d04303 != installed HEAD 7c0905a".
Review follow-up: the sweep of `hermes.exe.<pid>.deleteme` copies ran only when the publication fell back to a .cmd launcher, so a later publication with distlib available (an .exe) left the aside copy behind forever. The sweep now runs on every successful publication. The running-launcher test bounds its readiness wait, keeps the child's whole lifetime inside the try/finally (kill + reap on a stuck shutdown), and republishes with distlib restored to pin the collection.
…usResearch#143234) * feat(auth): hand the free tier's apps to the account that replaces it When a Nous account is saved over a free-tier identity, Hermes now moves that identity's connected apps into the account with one call to the Nous Portal claim endpoint, authorized by the account's own token. Nothing asks first; the result (account email, what moved, what needs reconnecting) is shown afterwards in `hermes auth status nous` and exposed for the desktop. Sign-in code does not know about it. A hook at the two lowest write seams (`_store_provider_state`, `_write_shared_nous_state`) writes a pending record under the existing locks and does no network. The claim runs outside every lock: after a shared-store write, when a `deferred()` block exits (the desktop poller and `run_sign_in` save under their own lock), after a free-tier reconcile, and on each Nous keepalive tick, which resumes retries from an earlier process. Records sit beside the shared Nous store, keyed by a hash of the free-tier credential and bound to the destination account, so profiles sharing one free-tier identity share one record and one claim. A claim lock spans read, claim and write. A Claimed record is never reopened; a lost success followed by an unknown-credential answer reads as "probably moved". The account token is refreshed after the move only when the cohort carried. The same path runs `settle_after_upgrade` once per replacement, so every replacing writer settles the config. The legacy promotion sign-in already moves the apps on the server, so it records the handoff as done and sends no claim. * fix(auth): the free-tier handoff only ever claims into its bound account Review round 1 on the guest handoff. P1, Bearer not proven to be the destination: the runner checked the profile's account against the record's destination, then resolved a Bearer separately. A sign-in landing between the two could move the apps into that other account and mark the record Claimed. The Bearer is now resolved first and its JWT subject is compared to the record's destination inside the reservation, so only a token proven to belong to the destination is ever sent. P1, stale reservation: the destination check and the attempt increment were separate record reads, so a sibling could rebind the destination in between. `_reserve` now does one locked read-modify-write: still active, due (`next_attempt_at`, so a waiting runner honours a Retry-After another runner saved), under `MAX_ATTEMPTS` (a crash after the last send now closes the record as Failed instead of sending again), and bound to the Bearer's subject. A record without a destination subject is never written. P2, config completion in the runner: `drain` no longer runs `settle_after_upgrade`. Settling stays with the sign-in that saved the account (run_sign_in, the desktop poller), as before this PR. This deletes the thread-local queue and the batch's settle fallback. P2, keepalive: a handoff failure is logged and token renewal carries on. An unparseable records file is moved aside to .corrupt on the next write instead of blocking every Nous account save. P2, budget: the remaining sync budget is checked before each record, token resolve and request, and is passed as their timeout, so a refusal at the edge starts no refresh or second claim. P2, status: an active move bound to an account the profile no longer holds reads as paused, with copy that says so. * fix(auth): free-tier handoff retries reach every profile and every long-lived process Review round 2 on the guest handoff. P1, queued moves had no worker: retries rode the token keepalive, which does not start in a process that boots with a guest (a Desktop backend before sign-in). A move left by a CLI sign-in during an outage then waited for a restart. Retries now run on a dedicated worker that `start_nous_auth_keepalive` starts in every long-lived Nous process, guest or not; the keepalive tick no longer drains. P1, named profiles never retried: the keepalive drained only the launch profile's records. Each worker tick now runs every due record under its owning profile's home and secret scope (the way the cron ticker binds a profile), so a named profile's move retries from a process launched as another profile. The bound-account check and the Bearer proof in the reservation still decide whether anything is sent. P2, cached guest token: the resolver's 5 s memo could still hold the guest's token from a connector call just before the save; the reservation then refused its subject and the move waited for a later run. The handoff now drops that memo before reading the account token. P2, budget through locks: the token read waited the resolver's own 15 s floors for the auth and shared locks. The handoff now takes both locks itself within the remaining budget (same order), which makes the resolver's acquisitions reentrant, and re-checks the budget before reserving or sending. * fix(auth): free-tier handoff never gives up on a transient failure before the reap Compatibility audit, items B1, B4 and B5. B1: a handoff that reached Failed was never retried, so the free tier's apps were lost when the idle free-tier identity was reaped after 14 days. Transient failures (5xx, the 503 breaker with no Retry-After, timeouts, busy, unknown codes) became final after 8 attempts, about 81 minutes. Now only codes no later attempt can change are final (client_not_allowed, account_locked, invalid_request). Every other outcome retries, the slowest rung (1 h) repeating, until 13 days after the record was made. The attempt reservation then closes it as Failed `retry_window_ended`. One predicate (`_retryable`) picks the work for every run, so a record an older build failed on a transient code re-arms on the next sign-in, `hermes auth status nous`, `hermes auth upgrade`, or retry-worker tick, with no new state. The attempt cap is deleted. B4: failure copy printed raw server codes. Final codes map to a plain sentence, and anything else shows only the generic sentence. B5: a claim answer without a `connectors` field (an older service) read as "There were no free-tier apps to move", and so did Gone on a first attempt, which follows a released ticket that already moved the apps. The first now reads as unknown with neutral copy, and the second as "Nothing left to move from the free tier." * fix(auth): a free tier with no apps reads as nothing moved after sign-in A free tier that only read its connectors gets a vendor id row on the service, so the claim answers kind `moved` (or `swapped`) with an empty `moved_toolkits`. The status copy keyed on the kind and printed "Your apps moved to <email>." for an account that received nothing. One rule: an empty moved list reads as nothing moved, for moved and swapped alike. The service now refuses a move it could not read, so an empty list is the truth. The promotion sign-in's record, which reports no list at all, now carries kind `unknown` and keeps its neutral copy instead of falling under the rule.
…h#143235) * feat(auth): every Nous sign-in is the plain device-code login Every Nous sign-in (desktop, CLI and TUI /login, gateway /login, `hermes auth upgrade`) now runs one plain device-code login, `anon_sign_in.run_sign_in`: device code, a cancel-aware token poll, save. The free tier the account replaces hands its apps over in the existing guest handoff step after the save, and every surface prints what moved and what to reconnect right after "Signed in as ...". The promotion intent/status calls, the consent page link and the `cancel_wins_after_promotion` rule are gone, with the Declined, Superseded, Retired and Unavailable states. A cancel is now one `Cancelled` state and always wins: nothing is saved. - The token poll takes a `cancelled` hook and checks it on a <= 1 s tick, so a desktop cancel or a newer /login stops it at once. - An expired code maps to `TimedOut` (`reason: timeout` on the desktop). - The account email is read from the access token's `email` claim. - TLS goes through `_resolve_verify`, so the CA bundle settings apply. - /login works with no free tier on disk. - The desktop start route always uses the flow (`replace_account=True` keeps "connect another account" working) and the poll route returns the handoff lines. - `hermes model` signs a connector-only free tier in instead of refusing; `_login_nous` prints the handoff result too. - Every sign-in that saves an account settles the config itself once its locks are released (`run_sign_in`, `_login_nous`, `hermes auth add nous`, the billing step-up, the tool-setup login); the handoff runner only moves apps. * fix(auth): CI round 1 on the plain sign-in - The docs page wrote "<= 1 s", which MDX parses as a JSX tag; say "at least once a second" instead. - The shared Retry-After parser test still imported the promotion status poller's helper, which this PR deletes; drop that leg. * fix(auth): Greptile round 1 on the plain sign-in - Read the Portal URL inside the profile scope: with no Nous state on disk, a multi-profile backend read the override after the scope had exited and signed in against the default Portal. - An expired_token answer from the token endpoint is TimedOut (the desktop's timeout screen), the same as running out the clock. * fix(auth): Greptile round 2 on the plain sign-in `hermes auth add nous` and the tool-setup Nous login now capture the free tier's handoff around their save and print what moved and what to reconnect, the same as the terminal login does. * fix(auth): Greptile round 3 on the plain sign-in - The Portal is resolved through the same rule every other Nous call uses: the operator's override wins over a URL saved in the auth store, so a staging override no longer signs a desktop "connect another account" into production. - Gateway /login holds its registry lock across the final cancel check and the save, so a replacing /login cannot land between them and the replaced attempt never saves. * fix(auth): Greptile round 4 on the plain sign-in Gateway /login no longer holds its registry lock across the save. The final cancel check claims the save under the lock, then the save runs outside it: the event loop takes that lock, and a save can wait on the auth store. A replacing /login while the save runs is told a sign-in is finishing instead of cancelling it.
…sResearch#143236) * feat(desktop): sign-in dialog shows who signed in and what moved The free-tier sign-in dialog now reads "Finish in your browser" with the device code shown small ("Do not share it.") and an "Open page again" button. Once the browser approves, the result screen sets the account email large under "Signed in as", then says what the replaced free tier's apps did, in the same lines every other sign-in surface prints. A Reconnect button opens the app that has to be reconnected; a move still retrying is a small note that updates itself. - The sign-in poll is a long-poll (`?wait=`): the backend answers once the session leaves pending, capped at 25 s. The desktop never asks again sooner than 2 s, so an older backend that answers at once is not hammered. The onboarding picker uses the same loop, and a Nous sign-in there that moved apps opens the same result screen. - The poll reports `handoff_status`: the handoff record as fields plus its copy (`guest_handoff.wire_status`). - New `GET /api/providers/oauth/nous/handoff?since=&wait=` (token-protected, profile-scoped) long-polls the handoff record so a retrying move is followed until it settles or the dialog closes. - The copy and failure screens for the retired transfer outcomes (declined, superseded, retired) are gone. * test(auth): sign-in copy check builds Completed with its handoff_status The every-state copy check builds each SignInState from a placeholder per dataclass field; Completed gained handoff_status in the parent commit, so give it a real wire_status value. * fix(desktop): Greptile round 1 on the sign-in result screen - Onboarding paints above every dialog, so a result from the onboarding provider list now waits until onboarding leaves the screen. - The result keeps the profile the sign-in ran for, and Reconnect names that profile (and connection) in the Connectors link, like a chat's connector links do. - A sign-in whose own run attempted no move (another process held the claim, or the budget ran out) reports the replaced free tier's saved record instead of no handoff (`guest_handoff.record_status`). - A failed handoff read retries with backoff (4, 8, 16, 32 s) before the result stops following the move. - de, es, fr and zh get the new dialog strings.
…sResearch#143248) * feat(connectors): list hosted apps without creating a guest identity Opening the desktop Connectors page used to create a Nous guest identity: connectors.list, connectors.catalog, connectors.accounts and connectors.policy.get each created one before answering, because the Portal's catalog route needs a token. A user who only looked at the page got an identity anyway. While there is no identity yet (and one may still be created), those four reads now answer without one: the catalog from a snapshot that ships with Hermes, an empty connector list, no accounts and no rules. Connecting an app still creates the identity, as before. scripts/refresh_connector_catalog.py rewrites the snapshot from a signed-in install. It leaves logos out: no client draws them. * feat(free-tier): sign-in offer for guests who connected apps Every free-tier sign-in offer keyed on the welcome trial, so a user on their own model key who connected apps through a guest identity never saw one. The offer record now tracks two offers on the same schedule. `trial` is unchanged. `connector` is for a guest without the trial that has connected an app: each app that finishes connecting sets `has_connected_app` on the guest state (so it dies with the guest) and counts as a finished task. free_tier.status (still a pure local read) adds connector_nudge_due_in, and free_tier.claim_nudge takes a `kind`. The desktop reads the connector offer whether or not the trial is available, re-reads the status after a turn or a page connect, and shows connector copy instead of the free-allowance copy. After a page connect settles it also re-reads the catalog and rules, so the bundled catalog from before the identity existed is replaced by the account's own. * fix(connectors): point the sign-in hint at `hermes auth upgrade` `hermes connectors` told users to run `hermes login`, which no longer exists. Both hints now say "Run `hermes auth upgrade` to sign in." * feat(connectors): read the public catalog with no token; one guest rule per entry point The bundled catalog snapshot listed apps a new guest cannot connect and went stale whenever the catalog changed. With no token, the Connectors page now reads the Nous Portal's public catalog route (GET /api/v1/connectors/catalog/public), sent with no Authorization and parsed with the existing catalog model. Once a token exists the page reads the signed-in route, as before. Whether an entry point may create the guest identity is now declared once per entry point instead of checked inside handlers: - managed_client(creates_guest=...) is required: connect (page, agent, card retry) and the explicit lists (CLI `hermes connectors list`, agent manage_connections status/display) pass True, unchanged. - account RPCs declare creates_guest on _account_method: catalog, accounts and policy.get never create one; tools and the account writes still do. - the page's connectors.list never creates one; connect does. With no token the page reads give true answers: public catalog, empty connected list, no accounts. The invented "unrestricted" policy is gone: the desktop asks for rules only once the list has rows, which needs a token, and policy.get without one refuses with sign-in-required. Deleted: the bundled snapshot, its refresh script, and the no-identity stubs. * fix(connectors): CI and review round: older backends, outside connects, link connects, guest opt-out - CI: test_legacy_fallback_ticket_profile_verbs faked managed_client with no arguments and gave the secondary profile no token, so the page list (now token-gated, creates_guest=False) never reached it. The fake takes the new keyword and the profile holds a token. - Older backends: a trial claim sends free_tier.claim_nudge with no params again. An older backend rejects the unknown `kind` key and would have dropped the trial offer; the new backend defaults to the trial. Only the connector claim sends `kind`. - Outside connects: while the page's list is empty (no identity), every finished turn or connect re-reads the list and catalog. An agent connect in a chat then moves the page to the signed-in routes and its rules. - Link connects: an agent status read that sees a connected app on a guest without the flag arms the connector offer, so messaging and one-shot connects (no operation watcher) count too. - Opt-out: the connector offer requires nous.guest to be allowed. * fix(connectors): the Connectors page list arms the connector offer too A guest whose apps were connected by an older Hermes has no has_connected_app flag, and only the operation watcher or the agent's status read set it. The page's connectors.list now passes its rows to note_connected_apps_seen, which owns the any-connected check for both list reads. * fix(connectors): review round 3: accounts after an outside connect, retryable offer record - An identity created outside the page (an agent connect in a chat) now also re-reads the accounts cached while there was none, so the new account row, rename and remove show up. - note_app_connected records the connector task before it sets has_connected_app. The flag ends list-read sightings, so a failed schedule write no longer leaves the offer unarmed for good.
With a cold desktop-inputs cache, Windows desktop preparation builds sdists that have no cp314 Windows wheel (obstore on win32-x64, dependency-injector on win32-arm64). uv builds each one in <cache>\python\runtime\<toolchain id>\sdists-v10\pypi\<pkg>\<ver>\<id>\src, about 200 characters below the deep .cache\desktop-inputs root, so MSVC link.exe outputs passed MAX_PATH and the build failed with LNK1104. On Windows, prepare_tools now hands uv a short job-local junction (<work>\uv) that points at the same transported cache directory. uv keeps the spelling it is given, so builds run under the short path while the bytes are still restored and saved by the desktop-inputs cache. The native toolchain id is read through the junction, and prepare_native keeps the absolute spelling instead of resolving the junction back to the long path. CARGO_TARGET_DIR is also owned by the preparation environment and points at <work>\cargo, so Rust sdists link under the work root on every target even if a tool resolves the junction.
Review follow-up: short_alias now creates the junction's parent directory, so preparation never depends on the caller having made the work dir first (the existing prepare_tools test failed on both Windows lanes with WinError 3). That test now checks the invariant the Windows path keeps: UV_CACHE_DIR reaches the cache directory, through the short work-local junction on Windows.
07ee800 relocked with newest uv, which writes `revision = 5` and a `default-groups` attribute on the root package. The pinned uv2nix (5a836d3, July) rejects that attribute, so `nix flake check` fails at evaluation: "function 'anonymous lambda' called with unexpected argument 'default-groups'" (v0.22.5 rc.2 release run 39880455146). uv2nix accepted the attribute upstream in e3dc0e4 ("lock1: accept default-groups attribute introduced in uv 0.12.22"); pin to its follow-up 7aba73a (the regression test for it). Only the uv2nix lock node moves.
test_malformed_secondary_cannot_evict_recorded_member failed in CI with `uv sync` "Hash mismatch for member-dep @ file://.../member_dep-1.0-py3-none-any.whl". Mechanism: installing_member() rebuilds member_dep-1.0 into the same wheels/ dir for the second plugin. _wheel used ZipFile.writestr(name, ...), which stamps the wall clock (2-second zip resolution), so a rebuild that crosses a tick has different bytes. The final sync seeds from home A's recorded lock (pm/packages.py: seed = prior resolved_lock), and uv 0.13.5 keeps that lock's sha256 for an unchanged path-wheel version (uv 0.12.13 re-hashed it), so sync then verifies the new file against the old hash. The test runs in ~2 s alone, so it passed whenever both builds fell in one tick; CI load pushed it across. Not caused by 07ee800's relock: 7c0905a (rc.1) and 07ee800 fail identically under the injection below. Repro: a 2.1 s sleep before the second installing_member() call, with uv 0.13.5 first on PATH, fails base on every run (3/3) and passes with this fixture (3/3); uv 0.12.13 passes on base too. Fix: fixed ZipInfo date_time and mode, so a same name/version wheel is one artifact, as on an index. test_fixture_wheels pins it (clock moved an hour between builds -> identical bytes; red on base).
…nd reports why a launch dies Every Windows x64 MSIX chat smoke since the Blacksmith migration (91fae5f) failed: first on the GitHub-hosted guard, and since that guard was removed (272adc7) with Playwright's "Process failed to launch!", on Electron 40 and 44 alike. The same builds' arm64 MSIX, on a GitHub-hosted runner, chats; the last green x64 smoke (Oct 14) ran on windows-latest-32-core. Move the x64 leg back there, as f1b290e did for the install/update GUI legs. The failure itself carried no evidence: when the app exits before its debugger line, Playwright's Electron.launch leaves sibling stderr-watcher rejections unhandled, Node dies on them, and the smoke's catch never writes its receipt or Playwright's call log. The driver now logs those rejections (still failing the run), so the receipt records the app's exit code and stderr, and the Windows smoke saves Application-log errors from the run on failure.
On Windows a launch activates only the dependency environment, which has no distlib (a test extra) and drops the store interpreter's site-packages. So the `hermes update` that repins PM's Python found no ScriptMaker, published bin\hermes.cmd and -- since NousResearch#143291 taught it to rename a running image aside -- retired bin\hermes.exe. The install was left with no hermes.exe, which Desktop's backend sweep (taskkill /IM hermes.exe), users' shortcuts and terminal profiles, and the install E2E all name. The installer mints the first hermes.exe through python-build-standalone's bundled pip (pip._vendor.distlib). Every store Python ships that pip, so when the running process cannot import distlib, mint_launcher now loads the vendored distlib from beside the interpreter it is binding, under a private module name so the store's pip never answers `import pip` here. The .cmd fallback remains for an interpreter with no distlib at all. The retired-copy sweep also collects distlib's own `hermes.exe.deleteme`, which its in-use replacement leaves when the update's own exe is the one being replaced. Seen in the v0.22.5 rc.2 install E2E (v0.22.0 -> a4072ce, Python 3.14.7 -> 3.14.9): test_upgrade_over_old_gateway.py "installer published no ...\hermes\bin\hermes.exe".
…essions T3 Code delivers a thread handoff as 'Context handoff (...)' ... 'User message:' and wraps requests in <t3_code_instructions>/<user_request>. Both were titled verbatim, and gateway_server mirrors the title onto the T3 thread. Model: custom:9router:opus via Hermes (acpRegistry) in T3 Code
commented
Nov 14, 2026
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem. T3 Code threads driven by Hermes get titles like
Context handoff (full_thread_summary):. T3 Code sends a delegated or handed-off thread's first prompt asContext handoff (...)+ coverage +\n\nUser message:\n+ the user's text, and wraps requests in<t3_code_instructions>…</t3_code_instructions><user_request>…</user_request>.agent/title_generator.pytitled that verbatim, andacp_adapter/gateway_server.py(_rename_t3_thread) then mirrors the session title onto the T3 thread.Fix.
_summarize_user_message(the one input both the instant title and the model title use) now:<user_request>body when one is present, andContext handoff (header, keeping the text after the lastUser message:marker.The test is in the existing
tests/agent/test_title_generator.pyand covers: wrapper only, handoff only, and handoff inside a wrapper.The companion T3 Code change strips the same header on the T3 side: IEatCodeDaily/t3code#1
Base. Branched from
bf0203a794, the upstreammaincommit the desktop install runs. Forkmainis months behind upstream, so this PR also shows those upstream commits; the change itself is the single last commit (3786a91302, 2 files).Gates. Upstream CI needs Blacksmith runners, which the fork doesn't have, so the full test suite and
ruff check .run on GitHub-hosted runners fromci/title-skip-t3-handoff(this head + one workflow file). Alongside them runs a baseline onbf0203a794, so failures that already exist on the base can be told apart from ones this change causes.Results (run 43136060375):
tests/agent/test_title_generator.pypasses 99/99, including the 3 new cases.tests/plugins/image_gen/test_openrouter_compat_provider.py::...test_catalog_models_are_offered_only_by_openrouter. The baseline run onbf0203a794fails the same way, so it is not caused by this change.ruff check .: one FURB192 finding intests/gateway/test_native_http_auth.py:231. This PR does not touch that file.Model: custom:9router:opus via Hermes (acpRegistry) in T3 Code