Commit Graph

20877 Commits

Author SHA1 Message Date
witcheer abaa43ed7c docs: add 'Which File Does What?' - one-page map of SOUL/USER/MEMORY/AGENTS
The four-file map is currently split across the memory, personality and
context-files pages; 'which file is my agent's brain' is one of the most
frequent support questions (e.g. #20245, #29476). One master table, the
frozen-snapshot rule surfaced with a link, and the two canonical mix-ups
answered directly. Content is drawn from the existing three pages.
2026-08-05 21:33:44 +05:30
witcheer b4312f92c6 docs: state the /goal vs Kanban boundary on both pages
The goals page never mentions Kanban and the kanban page references /goal
only inside the goal-mode-cards section, so users assume /goal hands work
to the board (see #26116 - /goal is single-session continuation only).
Adds a decision section to goals.md and the inverse note to kanban.md.
2026-08-05 21:33:44 +05:30
ethernet acb590fc4a fix(nix): fix electron headers sha 2026-08-05 11:47:05 -04:00
ethernet b27cdc3824 feat(nix): desktop app icon 2026-08-05 11:47:05 -04:00
ethernet eea6044098 feat(desktop): register a Linux launcher entry for `hermes desktop`
On Linux a freshly-built desktop app had no presence in the application
launcher: no Hermes in the KDE/GNOME menu, no icon, nothing to pin. Users
had to hand-write ~/.local/share/applications/hermes.desktop and remember
to reindex the menu caches themselves.

`hermes desktop` now writes that entry itself (best-effort, idempotent,
never blocking a launch), and `hermes uninstall --gui` removes it again.

Both fields that matter are absolute:

- Exec — the launcher runs with a minimal environment and no shell PATH
  customizations, so a bare `hermes desktop` silently fails for anyone
  whose hermes lives in ~/.local/bin or a venv. We resolve the real binary
  via relaunch.resolve_hermes_bin(), falling back to an absolute
  interpreter + `-m hermes_cli.main`.
- Icon — an unqualified name only resolves against an indexed icon theme,
  which we are not in. The spec allows an absolute path, so we point at
  apps/desktop/assets/icon.png in the checkout. No copy is installed: Exec
  already depends on that same tree, so a second copy would add bytes and
  an uninstall step without surviving anything Exec wouldn't.

Menu-cache refresh is tool-gated — update-desktop-database, then
kbuildsycoca6 or kbuildsycoca5 — each only when the binary is actually on
PATH, because most desktops ship none of them and a missing one is not an
error. The entry is only rewritten when its contents change, so a launch
doesn't churn the caches every run.

Verified on NixOS: the generated entry passes desktop-file-validate, a
real kbuildsycoca6 on PATH is invoked with --noincremental, a real
update-desktop-database writes mimeinfo.cache, absent tools are skipped
cleanly, and removal leaves the checkout's icon untouched.
2026-08-05 11:47:05 -04:00
ethernet b879df27fb fix(desktop): worktree dialog names the project, not the branch 2026-08-05 11:43:09 -04:00
ethernet cb7f594be8 feat(desktop): let convert-a-branch reach remote branches too 2026-08-05 11:43:09 -04:00
ethernet b818c427c8 fix(desktop): mount one worktree dialog instead of one per composer
Every CodingStatusRow mounted its own WorktreeDialog and subscribed to the
same global `$newWorktreeRequest` token, so a single ⌘⇧B with two composers on
screen opened two stacked dialogs — dismissing the front one revealed an
identical empty dialog behind it, which read as the dialog "staying open" after
creating a worktree.

Mount it exactly once in the sidebar (beside ProjectDialog) and drive it from a
`$worktreeDialog` atom, mirroring how the project dialog already works. One
mount cannot double-open. Every entry point (⌘⇧B, the rail's kebab, the
sidebar's + button) now publishes intent instead of rendering its own copy; the
rail and the button pin their own repo so a tile's kebab still targets that
tile's worktree.

The target is resolved at open time by `resolveWorktreeRepoPath`, which walks
the focused surface's cwd then the entered project's root, validating each
candidate against the repo-status probe cache — a project's root folder is not
necessarily a git repo, so existence alone isn't proof. That makes the resolver
the sole authority, so the hotkey no longer pre-gates on `$repoStatus` and now
works from a detached session that sits inside a project. When nothing in reach
is a repo it is a silent no-op: a worktree only exists inside a repo, so there
is nothing to report.

Also adds a project picker to the dialog so the repo can be retargeted before
naming the branch.

E2E: extends worktree-branch-status.spec.ts with a 10-branch repo, visual
snapshots of the base-branch picker and the convert-branch view, a geometry
assertion that the picker isn't clipped by the dialog (fails headlessly on
regression rather than waiting for a human to compare diff images), and a
two-composer test asserting one keypress opens exactly one dialog. Tests 1 and
4 fail against the previous code and pass now.
2026-08-05 11:43:09 -04:00
ethernet b846f0c002 fix(desktop): stop dialogs clipping popovers opened inside them
DialogContent published itself as the portal container for popovers opened
inside a dialog (so focus stays in the dialog and dismissal doesn't close it),
but that same element carried `overflow-y-auto`. Every Select/Popover/
DropdownMenu in a dialog was therefore born inside a scroll box and got
cropped at the dialog's edge — most visibly the worktree dialog's base-branch
combobox, where the branch list was cut off entirely and only the search field
showed.

Split the box in two: the shell keeps position/size/skin and no longer clips
(it stays the portal container), while a new inner body div owns layout and
scrolling. Popovers remain DOM descendants of the dialog, so focus and
dismissal behave exactly as before, but they can now paint past the dialog's
bounds. The banner variant had the same `overflow-hidden` on its shell; its
clip moves to the banner itself, which keeps the rounded bottom edge.

Callers that passed layout/scroll classes (grid, gap-*, p-*, overflow-*) now
pass them via the new `bodyClassName`; `className` keeps sizing and skin.
2026-08-05 11:43:09 -04:00
ethernet ee7c614eef fix(ci): follow artifact download redirect without auth
The artifact download URL returns a 302 redirect to a signed blob URL.
urllib sent the Authorization header to the blob, and the blob rejected it
with a 401 error. The download now has two hops. The first hop authenticates
to the API. The second hop follows the redirect without the auth header.

The query runs?event=workflow_call returns nothing for this repository.
GitHub flattens reusable-workflow jobs and their artifacts into the caller
run. The fetch now lists the artifacts on the orchestrator run only. The
dead sub-run enumeration is gone. Two API calls per cycle are gone with it.

The 'artifact statuses updated' reason never appeared. The code updated the
count before the comparison. Now the code compares first and updates after.

The code rejects zip members that contain '..' or start with '/'.

tests/ci/test_live_comment.py is deleted. This repository does not keep
tests for CI infrastructure.
2026-08-05 11:16:18 -04:00
ethernet 1d7d0e41af ci: poll review statuses from artifacts every cycle
The live comment poller got its review statuses from two sources. The first
was the REVIEW_STATUSES environment variable, fixed at the start of the
comment-live job. The second was one ci-timings artifact, downloaded at the
end of the run. Status details (error messages, action_required items)
appeared only after all jobs finished. The job pass/fail results were visible
as each job completed.

Now every status-producing workflow_call uploads a small review-status
artifact when it completes. The poller lists all review-status-* artifacts
from the orchestrator run and its workflow_call runs every cycle. It
downloads each artifact and merges the statuses into the comment. A status
appears as soon as its job finishes.

Changes:
- live_comment.py: _fetch_artifact_statuses became fetch_all_review_statuses.
  The new function lists the artifacts via the API, downloads each one, and
  parses it. Removed the review_statuses_json parameter, the
  --review-statuses-file argument, and the subprocess import.
- ci.yml: removed the REVIEW_STATUSES environment variable, the inline Python
  merger, and the --review-statuses-file argument. Renamed the
  ci-timings-review-status artifact to review-status-ci-timings.
- Eight workflow_call files: added a step that writes review-status.json and
  uploads it as an artifact after each review_status output.
- test_live_comment.py: added tests for _parse_status_file and
  _merge_statuses.
2026-08-05 11:16:18 -04:00
ethernet 949babd083 ci: add detailed logging to live comment poller
The poller logs transitions between polls. It reports newly completed jobs
(with their results), newly appeared jobs, and jobs that left the pending
list. Each comment update shows the reason for the change. For example:
'1 new completion(s); artifact statuses updated'. When nothing changed, the
poller lists the jobs that are still pending. The status line shows the raw
job count from the API and the number of infra jobs that the filter removed.
2026-08-05 11:16:18 -04:00
PRATHAMESH75 49d8a155c4 fix(terminal): skip binary content on the referenced-script remote-read fallback (#77703)
The gateway terminal guard crashed with 'ValueError: embedded null byte'
(command never ran, exit_code -1) when a command invoked an ELF binary by
full path. _read_referenced_script correctly rejects the binary locally
(NUL in first chunk), but the read_remote_script fallback
(_read_script_in_env) then re-read the SAME file's bytes without a NUL
guard, decoded them, and fed machine code back into the scanner, which
re-tokenized it into a bogus NUL-bearing path and crashed at os.open.

- _read_script_in_env: skip content containing a NUL byte on both the
  local-read and remote-cat branches (mirrors _read_referenced_script:
  a binary is nothing to scan), so binary never re-enters the guard.
- _read_referenced_script: tolerate ValueError from os.open on a
  NUL-in-path, alongside the existing OSError guard, so the guard can
  never crash the terminal tool regardless of input.

Extends the #76762 NUL-safety fix (local path only) to the gateway's
remote-read fallback path.
2026-08-05 20:34:59 +05:30
brooklyn! ae6c2e57e1
Merge pull request #78682 from NousResearch/bb/win-8dot3-profile-paths
fix(install): Windows install completes on profiles with a space, dot, or accent in the username
2026-08-05 08:21:23 -06:00
kshitij 1be70d6354 fix: join heartbeat thread in finally + add error-path test
Add activity_hb.join(timeout=2.0) after activity_hb_stop.set() in
direct_api_call's finally block so the heartbeat thread is deterministically
stopped before client teardown. Add test verifying no stray _touch_activity
fires after direct_api_call raises an exception.

Follow-up to PR #78548 by @xxxigm.
2026-08-05 14:00:25 +05:30
xxxigm 62800ddadb docs(delegation): note in-flight model waits count as progress
Clarify that activity-timestamp ticks during a provider wait keep the
staleness monitor from treating a slow completion as a wedged child.
2026-08-05 14:00:25 +05:30
xxxigm d55bc063f1 fix(delegation): keep subagents alive during slow model waits
Top-level delegate_task runs in the background, and the 450s progress-stall
monitor only sees api_call_count / tool / last_activity_ts. Subagents use
non-streaming direct_api_call, which previously touched activity once and then
went silent — so a healthy local GGUF / long-prefill wait looked frozen and
was interrupted around ~450s as "Operation interrupted: waiting for model
response", even when child_timeout_seconds was raised. Refresh activity while
the inline request is open, and treat last_activity_ts advances as sync
heartbeat progress too.
2026-08-05 14:00:25 +05:30
Ryder Freeman 84e93ffefb fix(state): stop delegate/tool children corrupting compression lineage
get_compression_lineage's forward walk accepted any non-branch child as
the compression continuation. Delegate subagent rows (_delegate_from)
and tool-tagged rows (source=tool) created before the real continuation
were picked as the lineage successor, so the lineage — and session .md
export built on it — followed a subagent's transcript instead of the
actual conversation continuation.

Rename _is_branch_child_row to _is_explicit_fork_child_row, treat
_delegate_from and source=tool rows as explicit forks alongside
_branched_from, and require _is_compression_child_row in the forward
walk instead of merely excluding branches.

Sliced from PR #79024 by @RyderFreeman4Logos (the cache-scope portion
of that PR is tracked separately in #79017).
2026-08-05 13:50:26 +05:30
kshitij 82c6acae6f chore: add contributor mapping for burak33bb 2026-08-05 13:00:47 +05:30
burak33bb c4f3d5a313 fix(agent): prevent historical steer replay 2026-08-05 13:00:47 +05:30
joaomarcos 34c3f06f91 fix(cache): scope prompt_cache_key by session to stop cross-session bucket sharing
Cherry-picked from PR #78959 by @JoaoMarcos44 with authorship preserved.
Follow-up: hoist _cache_scope_from_session_id(session_id) to a local in
build_kwargs so it's computed once instead of 4 times per call.

Closes #78941. Closes #79012. Closes #79013. Closes #79014. Closes #79015.

Co-authored-by: JoaoMarcos44 <joaomarcosdias444@gmail.com>
2026-08-05 12:42:46 +05:30
hermes-seaeye[bot] 2221010717
fmt(js): `npm run fix` on merge (#79155)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-05 07:06:05 +00:00
Tranquil-Flow 530d8148aa fix(desktop): scope restored navigation by profile (#67709)
Scope remembered desktop route and session keys by the encoded active
profile. Discard ambiguous legacy global navigation keys instead of
assigning them to an arbitrary profile. Delay cold-start restoration
until the primary profile and session list have reached renderer.ready.
Preserve explicit deep-link and hidden-window destinations. Restore
and persist session routes only when a direct ID or lineage root is
explicitly owned by the active profile. Clear both profile-scoped route
and session state after resume exhaustion.

Closes #67709
Co-authored-by: Tranquil-Flow <tranquilflow@users.noreply.github.com>
2026-08-05 12:27:17 +05:30
kshitij a6e1e270b1 refactor: trim verbose comments + drop redundant default_flow_style kwarg
simplify-code follow-up: collapse 9-12 line inline comments to 3 lines
(keeping the loss-chain WHY + issue refs), and remove redundant
default_flow_style=False (atomic_yaml_write already defaults to False).
2026-08-05 11:59:33 +05:30
briandevans 63c0bb694e fix(cli): correct the skin_cmd fallback comment to match the actual read path
_skin_set has no try/except around yaml.safe_load, so invalid YAML raises
and aborts the command. The {} fallback comes only from safe_load()
returning None on a zero-length file — which is exactly the state a torn,
unsynced write leaves behind, so the data-loss chain is unchanged.
2026-08-05 11:59:33 +05:30
briandevans 649ce1f811 fix(cli): make profile.yaml and skin writes atomic to stop silent field loss
`write_profile_meta` and `hermes skin set` are both read-modify-write
helpers that rewrite a user-visible YAML file with a bare truncating
write, bypassing `utils.atomic_yaml_write` — the shared helper whose
docstring states that "every destructive file rewrite in the codebase
shares one implementation".

Both read halves swallow a parse error and fall back to `{}`, so a
truncated file is not transient corruption. The next call reads `{}` and
silently, permanently drops every field the caller did not explicitly
pass:

* `write_profile_meta` promises "unspecified fields preserve existing
  values". After an interrupted write, a follow-up call that only sets
  `description_auto` erases the profile's `description` — it vanishes
  from `hermes profile list` and never comes back.
* `_skin_set` exists so that "changing one token never disturbs the rest
  of the look". `path.write_text(...)` neither fsyncs nor swaps
  atomically, so a crash or power loss can leave `<skin>.yaml`
  zero-length; the next tweak then rewrites from empty and the whole
  palette is gone. The gateway's skin watcher repaints live surfaces
  from this file within ~1s, so a half-written file is observable.

Routing both through `atomic_yaml_write` gives temp file + fsync +
`atomic_replace`, which also preserves a symlinked target (GitHub
#16743) and restores owner/mode, and emits emoji descriptions as real
UTF-8 instead of `\UXXXXXXXX` escapes (GitHub #51356).

Supersedes #51808, which fixed the unicode-escaping symptom alone by
adding `allow_unicode=True` to the same `yaml.safe_dump` call.
2026-08-05 11:59:33 +05:30
golldyck 652ebc5899 fix(console): handle string SystemExit code in _capture_output
A dispatched console handler that calls sys.exit("message") or
raise SystemExit("message") sets exc.code to a string. int(exc.code or 0)
then raises ValueError, which is not a ConsoleCommandError, so it escapes
execute()'s handler and crashes the local REPL on an ordinary user mistake
(e.g. removing a credential that does not exist). Treat a string exit code
as a status-1 failure carrying that message.
2026-08-05 11:48:03 +05:30
Teknium aec331899e chore: suppress windows-footgun false positive on gated killpg 2026-08-04 17:33:36 -07:00
Teknium 42e92c9c09 fix(git): kill the whole probe process tree on timeout (port of openai/codex#36793)
Timing out a bounded git probe must not leave helper descendants
(credential helpers, git-remote-https, hook children) running after the
probe fails open. bounded_git_probe now spawns the child in its own
process group on POSIX (process_group=0), and _kill_git_process_tree
signals the whole group with os.killpg — gated on the child actually
leading its own group (pgid == pid), so a shared-group spawn can never
take down unrelated processes. Windows keeps the existing taskkill /T /F
tree kill.

Proven live on main: a fake git that forks a 300s descendant left the
descendant running after the probe timeout; with the fix the descendant
dies with the launcher. Fast path and fail-open contract unchanged.

Port of openai/codex#36793 (Terminate timed-out Git process trees).
2026-08-04 17:33:36 -07:00
Alex Fournier 535a59c5c0 docs(observability): clarify active profile identity
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-04 15:13:34 -07:00
Alex Fournier 806c2b1fdc Merge updated client resource metrics into active-install metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-04 15:10:41 -07:00
Alex Fournier e7eaae2bd3 Merge latest skill metrics into client resource metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-04 15:07:34 -07:00
Alex Fournier 451a078a50 Merge latest origin/main into skill metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-04 15:06:36 -07:00
Alex Fournier 0680841797 Merge updated skill metrics into client resource metrics
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	tests/run_agent/test_run_agent.py
2026-08-04 15:05:58 -07:00
Alex Fournier 334c02b77e test(observability): exercise the active worktree in metrics smoke
Signed-off-by: Alex Fournier <afournier@nvidia.com>
2026-08-04 15:03:47 -07:00
ethernet 36cb5ae553 ci: test updating from sampled release tags, on tag + every 12h
Wires tests/install/install-update-e2e.sh into CI as a reusable workflow plus a
caller that fans out over real releases, because that is the question users care
about: can someone on a version they actually installed get to this commit?

install-e2e-run.yml takes `route` and `install-ref`, so the combinations that
matter are expressible without duplicating runner setup. Each leg is independent
-- its own runner, its own sandbox, its own install, nothing shared or rewound.

The starting versions are chosen at runtime by scripts/sandbox/pick-release-tags.sh:
newest, oldest, and an evenly spaced spread between (5 by default). Choosing at
runtime rather than hardcoding keeps the matrix honest -- a pinned list stops
covering the newest release the day after it ships, and pins an "oldest" long
after anyone still runs it. Newest catches "did the last release break
updating?", oldest is the longest upgrade jump still possible, and the spread
samples the migrations in between (config-schema bumps, venv layout changes,
dependency floors). Tags are read from the checkout with `git tag --list`, not
`git ls-remote`: the job has the repository already, so this needs no network,
works offline and on a fork, and takes 8ms. The repo is derived from the
script's own resolved path rather than $PWD, so a copy cannot silently report a
different checkout's tags. The pick-releases job takes the checkout that suits
it -- blob:none filter, sparse-checkout of just that script, and fetch-tags,
since tags are the entire input and the default shallow checkout has none.

Triggers match the shape of the work:

  * every 12 hours, so upstream drift (a new uv, a Node bump, a PyPI change)
    surfaces on a schedule instead of in someone's review cycle;
  * on release tags, the moment the set of versions users can update FROM
    changes and the moment a broken updater would strand them;
  * manually, with the route and the sample size as inputs.

Not on pull_request: a leg is ~9 minutes of real toolchain installation and the
matrix multiplies it. fail-fast is off so one broken release does not mask the
others, and max-parallel caps the fan-out so a run does not hammer the runners
or PyPI. The tag list is resolved once and shared by both route matrices, so the
two routes cover the same versions.

Artifact names include the sanitized install-ref, since a matrix runs the
reusable workflow several times per route and same-named artifacts collide; that
name is built in a step because Actions expressions have no string-replace
function. The name step runs with `if: always()`, since a failing leg is exactly
when its logs are wanted.

.gitignore covers .hermes-sandbox-e2e*/ rather than the bare directory: the
per-route sandbox trees (-update, -installer) fell outside it, so the sandbox
made the worktree dirty and dev-sandbox reacted by snapshotting the working copy
into a fresh fake-main commit on every invocation.
2026-08-04 17:36:26 -04:00
ethernet 3d9ec4d62e test(install): prove updating from a release reaches this commit
Nothing covered the update path, which is the worst thing to break: a broken
updater strands users on the version that cannot fix itself. `hermes update`
alone is ~2000 lines (hermes_cli/update_cmd.py) and had no end-to-end test.

tests/install/install-update-e2e.sh installs a genuine earlier Hermes through
the real one-liner (curl -fsSL https://…/install.sh | bash, served by
dev-sandbox's MITM proxy at the canonical URL, cloning "github.com" through the
upload-pack shim), which really installs uv, a managed Python, Node and the
venv. It then applies ONE update route and requires the checkout to land on this
commit with `hermes --version` still working -- so a pass means the venv and
entry point survived, not merely that git moved.

One route per run, each on a sandbox built from scratch. Sharing one install
across routes -- or rewinding with `git reset --hard` between them -- leaves the
second route running against a tree the first already updated (same venv, same
console script, same __pycache__), which is not the state any real user is in: a
route could pass only because its predecessor did the work, and a failure in the
first left the second exercising something undefined.

--install-ref chooses what to install first, so this covers "update from an
older release", not just from the tip. Installer flags are probed against the
target rather than assumed, because releases from months back predate flags
current Hermes takes for granted: --skip-browser is read out of that ref's own
install.sh, and `--yes` is asked of the installed `hermes update --help` (the
update subcommand has lived in main.py, subcommands/update.py and update_cmd.py
across the tags we sample, so a static parse rots silently -- and did). Without
those probes, old releases die on "Unknown option: --skip-browser" and
"unrecognized arguments: --yes" before doing any work.

Installer output is streamed through tee rather than captured: a real install of
uv, Python, Node and the venv IS the substance of this test, so it belongs in
the job log, not only in an artifact. pipefail keeps the installer's exit status
rather than tee's, so a failed install cannot look like a pass. The sandbox's
own proxy log is printed in full on failure, since a rejected TLS handshake
explains a failure that otherwise reads as a bare `curl: (35)`.

Deliberately reuses dev-sandbox rather than adding a second harness. An earlier
draft rewrote install.sh's hardcoded URLs with insteadOf and ran it against the
host; that tested the installer LESS faithfully (bash install.sh instead of the
real one-liner, host libs instead of a clean machine, ssh disabled to keep a
failed rewrite from reaching real GitHub) while duplicating a fake Internet we
already have.

Shell, not pytest, so scripts/run_tests.sh and run_tests_parallel.py stay
untouched: a pytest version needed an entry in the former's `env -i` credential
allowlist and a _SKIP_PARTS exclusion in the latter, and every meaningful line
was a command run inside the sandbox anyway.

Two guards, both earned during bring-up. It prefers the `sandbox` wrapper and
falls back to the raw script only when bwrap is on PATH (under Nix the wrapper
supplies the PATH and DEV_SANDBOX_* vars, so the bare script exits 127). And it
refuses to run on a dirty worktree: every dev-sandbox invocation re-derives fake
main from the working copy, so uncommitted changes move the update target
between the call that installs and the call that verifies -- a failure that
looks like a broken updater but is a moving reference.
2026-08-04 17:36:26 -04:00
ethernet 84874c58a5 feat(dev-sandbox): support fake installer / fake main / git clones
allow you to simulate the whole official curl | bash installer,
and subsequent hermes updates.

Run development commands in a bubblewrap filesystem and network sandbox
with a local HTTPS MITM fixture server and a fake github
git-upload-pack transport.
Package the sandbox command and expose it from the nix devShell.

Stage the local installer at its canonical fake HTTPS URL and add a
persistent installation/update test path. Route root installs through
sandbox-owned filesystem locations and snapshot dirty source worktrees
into temporary fake commits so update tests can fast-forward without
changing the real checkout.

Includes a --install-ref sandbox installer mode that fetches any commit
(--from-main is a nice shorthand for local development) outside the
sealed sandbox, installs from that snapshot, and then promotes the fake
remote to the current worktree so update flows can be exercised with FF.

Notes on non-root sandboxes:
Giving a non-root sandbox a network is tricky.
slirp4netns joins the target userns and setuids to root before configuring the
netns, so the userns must map a uid 0; bwrap's --unshare-user maps exactly ONE
uid, so --uid 1000 leaves no root to become and slirp diedswith
`setns(CLONE_NEWNET): Operation not permitted`. Stage 1 builds the user+net
namespaces with `unshare` and two one-id ranges:

    inner 0    -> a subuid, unused by the payload, present only so slirp can
                  become root
    inner 1000 -> our real host uid

Mapping the payload to the *host* uid (not a second subuid) keeps everything the
sandbox writes owned by us, so `rm -rf` on a persistent sandbox still needs no
privileges. Stage 2 execs bwrap WITHOUT --unshare-user -- it only adds mount/pid
-- sidestepping bwrap's refusal to accept --uid outside a userns it created.
Costs a /etc/subuid range for the invoking user (we error with the exact line to
add) and util-linux `unshare`; `--root` needs neither.
2026-08-04 17:36:26 -04:00
Brooklyn Nicholson 34833303f5 ci(install): actually run the PowerShell installer tests
scripts/tests/ has held three PowerShell suites that no workflow ever
invoked -- there is no Windows runner in CI, so they have been inert since
they landed. A regression test nothing executes is worse than none: it
reads as coverage.

Adds a windows-latest job, gated on a new `installer` lane so it only fires
for PRs touching install.ps1 or its tests. The 8.3 suite runs under both
pwsh 7 and Windows PowerShell 5.1, since install.ps1 arrives via `irm | iex`
into whichever shell the user already has and 5.1 is what ships with Windows.

Only the 8.3 suite is wired up. The other two fail on main today for
unrelated reasons; they can join once they are fixed.
2026-08-04 15:34:32 -06:00
Sahil-SS9 dae7e5477e test(install): exercise 8.3 normalization by running install.ps1, not by parsing it
The previous suite pulled ConvertTo-LongPath out of install.ps1 via the AST
and dot-sourced the extracted text. AGENTS.md bans source-reading tests, and
this one showed why: it never executed the script-level Add-Type the kernel32
resolver depends on, so the resolver that does the actual work was untestable
by construction.

Each case now spawns install.ps1 as a real subprocess with a crafted
environment. -ProtocolVersion is a side-effect-free early exit below the
normalization block, so the whole block runs exactly as it does mid-install
and the assertions read what it reports back.

Verified RED against the pre-fix install.ps1 (10 of 24 assertions fail) and
GREEN after. The profile-root substitution is pure path arithmetic, so those
cases run on any host including non-Windows CI.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-08-04 15:34:32 -06:00
xxxigm 9621f90325 fix(install): resolve 8.3 profile aliases so a built desktop app stops reporting failure
Windows aliases a profile folder whose name has a space, a dot, or an
accented character (FIRST~1.LAS, STONE~1.ZEN, RUBN~1). PowerShell's
FileSystem provider then throws "does not exist" the moment such a path
reaches a provider cmdlet, which every Node/Electron stage hits through
Tee-Object and the desktop stage hits again probing the binary it just
built. The install fails on an artifact that is sitting on disk.

install.ps1 already tried to expand these, but only via COM and only for
TEMP/TMP. COM cannot expand an alias on a non-English locale, and it cannot
expand one at all when 8dot3 generation is disabled or the alias is stale
-- both return the short path unchanged. LOCALAPPDATA was never normalized
either, so InstallDir stayed short even when TEMP got fixed.

Three resolvers now run in order, each covering what the last one cannot:
kernel32!GetLongPathNameW (locale-independent), COM (P/Invoke blocked),
and profile-root substitution (nothing to resolve -- rebuild on a root we
can prove is long). All five profile-rooted variables are normalized, and
HermesHome/InstallDir are re-derived from them. An explicitly passed
-HermesHome/-InstallDir is normalized in place, never replaced.

Every resolver degrades to returning its input, so a host where none apply
behaves exactly as before. Rewrites are logged to stderr: this bug class
has only ever been reported as a bare "does not exist" with no hint that a
short alias was involved.

Co-authored-by: Sahil-SS9 <218421507+Sahil-SS9@users.noreply.github.com>
2026-08-04 15:34:32 -06:00
brooklyn! fdc342c082
fix(models): a model id missing its vendor prefix says so instead of 404ing (#78909)
Selecting an NVIDIA NIM model whose id reached config without the nvidia/
prefix produced a bare "HTTP 404: 404 page not found" — retried three times,
never naming the model. It reads exactly like an outage or an auth failure,
which is where the Discord thread spent its time before the id was spotted.

normalize_model_for_provider() had no branch for nvidia, so a bare id passed
straight through to the API. Repair it from the provider's curated catalogue:
a bare name that matches exactly one entry modulo the prefix gets it back.
That's a lookup, not a guess — build.nvidia.com also fronts local NIM
containers and third-party models, and anything absent from the catalogue is
left alone. Because the repair runs on every runtime setup, an already-broken
config self-heals on the next turn and prints what it changed.

If a bare id still reaches the wire, the 404 now explains itself. The
classifier consults the same catalogue: a prefix-less id the provider only
serves as vendor/model is a deterministic failure, so it classifies as
model_not_found instead of burning three retries on a retryable "unknown",
and the error trace names the id to use.

Fixes #78796
2026-08-04 21:05:53 +00:00
brooklyn! 43717123ca
fix(models): a model id missing its vendor prefix says so instead of 404ing (#78856)
Selecting an NVIDIA NIM model whose id reached config without the nvidia/
prefix produced a bare "HTTP 404: 404 page not found" — retried three times,
never naming the model. It reads exactly like an outage or an auth failure,
which is where the Discord thread spent its time before the id was spotted.

normalize_model_for_provider() had no branch for nvidia, so a bare id passed
straight through to the API. Repair it from the provider's curated catalogue:
a bare name that matches exactly one entry modulo the prefix gets it back.
That's a lookup, not a guess — build.nvidia.com also fronts local NIM
containers and third-party models, and anything absent from the catalogue is
left alone. Because the repair runs on every runtime setup, an already-broken
config self-heals on the next turn and prints what it changed.

If a bare id still reaches the wire, the 404 now explains itself. The
classifier consults the same catalogue: a prefix-less id the provider only
serves as vendor/model is a deterministic failure, so it classifies as
model_not_found instead of burning three retries on a retryable "unknown",
and the error trace names the id to use.

Fixes #78796
2026-08-04 19:35:57 +00:00
brooklyn! 0a48af25bb
Merge pull request #78812 from NousResearch/bb/profile-share
Share your whole setup: export/import profiles with theme, layout, and skills
2026-08-04 13:35:41 -06:00
brooklyn! ec9572f876
Merge pull request #78854 from NousResearch/bb/session-move-project
Right-click a session to move it into another project
2026-08-04 13:35:09 -06:00
brooklyn! 0106844c9b
Merge pull request #78849 from NousResearch/bb/multi-tab-drag
Select multiple tabs and drag them to a zone together
2026-08-04 13:26:30 -06:00
Brooklyn Nicholson 1d6606d2cf fix(profiles): exported archives open in Finder (GNU tar, not PAX)
shutil.make_archive writes PAX with fractional-mtime records, which macOS
Archive Utility rejects ("Error 94 - Bad message") on double-click. Write
the profile archive with tarfile in GNU format instead: integer mtimes,
longlink for deep paths, extracts under Finder, bsdtar, and gnutar alike.
Verified against /usr/bin/tar (bsdtar) with >100-char member paths.
2026-08-04 13:23:33 -06:00
Brooklyn Nicholson edae3eed10 feat(desktop): move a session to another project from its row menu
'Move to project' submenu in the session actions menu (kebab and right-click,
via the shared MenuKit) listing every project with a folder except the current
owner. Picking one calls session.workspace.move at the project root, mirrors
the new cwd/branch/root into the $sessions cache, and refreshes the tree so
the row hops immediately.
2026-08-04 13:23:00 -06:00
Brooklyn Nicholson 28b3b0dd1c feat(gateway): session.workspace.move — re-home a stored session's workspace
A session created in the wrong directory needs its cwd corrected after the
fact. session.cwd.set only reaches live runtime sessions, so cold rows were
stuck. The new RPC targets the persisted row by session_key, validates the
folder, and REPLACES the git branch/root identity (update_session_cwd grows a
replace_git_meta flag) so the project tree's grouping follows the move instead
of pinning the session under the project it left via a stale git_repo_root.
A live idle agent bound to the row is re-anchored through the runtime path;
a mid-turn session refuses with 'session busy'. Runs on the RPC pool — the
git probes are subprocesses.
2026-08-04 13:23:00 -06:00
brooklyn! d98287fe3c
Merge pull request #78831 from NousResearch/bb/session-read-state
Sessions track read/unread
2026-08-04 13:20:26 -06:00