Commit graph

9995 commits

Author SHA1 Message Date
ethernet
6ecd335aa8
Merge pull request #75037 from NousResearch/sec-fixes
fix(sec): patch vulnerable deps + add publication-age floors and npm script allow-list

Co-authored-by: Kingsley Wong <7207924+datanerdie@users.noreply.github.com>
Co-authored-by: viky <vikyw89@gmail.com>
Co-authored-by: FT_IOxCS <237263164+ft-ioxcs@users.noreply.github.com>
Co-authored-by: 方明元 <fmy3@qq.com>
Co-authored-by: Yorkstone Supplies <58149681+sycamoregroupltd@users.noreply.github.com>
Co-authored-by: Steven Cuz Leath <Steven.Leath@gmail.com>
Co-authored-by: Kyle French <248366920+Dadmin88@users.noreply.github.com>
Co-authored-by: Eugeniusz Gilewski <egilewski@egilewski.com>
Co-authored-by: Christopher Gara <79837758+christopherrobin88@users.noreply.github.com>
Co-authored-by: LironTTG <147833337+LironTTG@users.noreply.github.com>
Co-authored-by: Austin Porada <bbasketballer75@gmail.com>
Co-authored-by: cresslank <9219265+cresslank@users.noreply.github.com>
Co-authored-by: Ion Mudreac <mudreac@gmail.com>
Co-authored-by: martinramos002 <262243228+martinramos002-bot@users.noreply.github.com>
Co-authored-by: Sensie-Agents <agents@joinsensie.com>
Co-authored-by: alexwill87 <173086651+alexwill87@users.noreply.github.com>
Co-authored-by: BullishMomentum56 <218643122+BullishMomentum56@users.noreply.github.com>
Co-authored-by: pintadoai <240097310+pintadoai@users.noreply.github.com>
Co-authored-by: Alfred Sahlberg <dinmail@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Richard Ham <richard.ham@live.com>
Co-authored-by: jrcrittenden <jrcrittenden@gmail.com>
Co-authored-by: 峯岸 亮 <1920071390@campus.ouj.ac.jp>
Co-authored-by: Marcus Martini <6473852+napoleonmm83@users.noreply.github.com>
2026-07-31 14:07:32 -04:00
brooklyn!
0324849fe4
Merge pull request #61173 from NousResearch/bb/desktop-kanban
feat(desktop): Kanban — the founding plugin on the desktop SDK
2026-07-31 13:00:10 -05:00
ethernet
e803c5aeac fix(npm): self-upgrade a managed npm when engines.npm rejects it
The repo's .npmrc sets engine-strict=true and package.json pins
engines.npm, so an npm outside that range aborts every npm ci /
npm install we run inside the checkout:

    npm error code EBADENGINE
    npm error notsup Required: {"npm":"<11.10.0 || >=12.0.0"}
    npm error notsup Actual:   {"npm":"11.10.0"}

Our callers made that worse: _run_npm_install_deterministic sees
`npm ci` fail and falls through to `npm install`, which fails
identically, so the user got a buried EBADENGINE and no remedy.

React to the failure instead of predicting it. npm states the
required range in its own error, so there is no need for a version
probe on the happy path or a semver range matcher — the recovery
reads the constraint out of the output it just produced, upgrades,
and retries once.

Scope is deliberately narrow. Hermes only upgrades an npm inside its
own managed Node tree ($HERMES_HOME/node), installing with --prefix
so bin/npm keeps resolving to the upgraded lib/node_modules/npm; a
managed install writes prefix=~/.local into node/etc/npmrc, so
without the override the "upgrade" would land elsewhere while the
managed npm stayed stale. A system / nvm / brew / Nix npm belongs to
the user, so that case prints the exact command and lets the original
failure stand.

The upgrade runs from a temp cwd with npm_config_min_release_age=0,
otherwise the checkout's own min-release-age gate would refuse the
npm release we need.

_run_npm_install_deterministic's capture_output=False callers (the
desktop install) streamed npm output and returned stderr=None, which
would leave the recovery nothing to read — stderr is now teed, so
live output is unchanged and the text stays inspectable.

Verified end to end against real npm binaries on copies of a managed
tree: managed npm 11.10.0 -> EBADENGINE -> upgraded to 12.0.2 ->
retry exits 0; a foreign npm 11.10.0 hard-fails with the manual
command and is left untouched.
2026-07-31 13:42:04 -04:00
ethernet
a7efeb0829 fix(sec): update mcp to 1.28.1
mcp 1.26.0 has 3 known vulnerabilities: PYSEC-2026-3481,
PYSEC-2026-3482, PYSEC-2026-3483

they're fixed in >= 1.28.1
2026-07-31 13:42:03 -04:00
Yi Lok Enoch Lam
5835201de1
fix: keep queued paste payloads atomic (#74797)
Co-authored-by: eloklam <22125285+eloklam@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-31 12:04:59 -04:00
rob-maron
126ff7071b
Portal free user vision fix + flux3 polling improvements (#75448)
* flux3 polling improvments

* poll gap to 4s

* back to 5s

* vision model fix

* minor fix
2026-07-31 10:17:55 -04:00
Austin Pickett
74fdc578cc fix(cron): set headers for chronos JWKS requests
The chronos cron-fire verifier constructed PyJWKClient without explicit
headers, so its JWKS fetch to the NAS portal hit the same WAF 403 the
dashboard-auth providers already guard against. It reaches the same
portal issuer, so it's the same bug class — mirror the fix here and add
a constructor-contract regression test.

Co-authored-by: James Hodgkinson <james@terminaloutcomes.com>
2026-07-31 10:17:30 -04:00
James Hodgkinson
eaa9582e38 fix(dashboard): set headers for Nous JWKS requests
The Nous PyJWKClient was constructed without explicit headers, while the
self_hosted provider already sends Accept + User-Agent. Without them the
Portal WAF can block the JWKS fetch, so the same failure mode remained for
the Nous dashboard-auth route. Mirror the self_hosted fix and add a
constructor-contract regression test.
2026-07-31 10:17:30 -04:00
James Hodgkinson
83cee29ff7 fix(dashboard): set headers for JWKS requests 2026-07-31 10:17:30 -04:00
kshitijk4poor
1789e06ed8 fix: trim identity prompt — remove jargon, tighten directive
Follow-up to PR #70238. Remove 'free-response channel' and
'authorization' jargon from the model-facing prompt. Collapse the
triple-negative 'do not ask / do not reject / do not stay silent'
into a single directive. ~70 tokens vs ~175 in the contributor's
version, same semantics.
2026-07-31 17:55:23 +05:30
KCAYAAI
b24c915168 fix(slack): trust adapter routing after stripping self mention 2026-07-31 17:55:23 +05:30
kshitijk4poor
53559aaf86 fix(agent): protect batch-compaction markers from micro supersede/defrag
Phase 2 review findings on the salvage branch:

C1 (critical): batch and micro summary markers share
COMPRESSED_SUMMARY_METADATA_KEY, and compress() never reset micro state.
After micro absorbed exchanges 1..k, a batch compaction summarizing
1..m (m>k) could fire; the next micro pass's supersede then dropped the
batch marker (whose content the stale rolling summary does NOT contain)
and archive_and_compact immediately made the loss durable. Defrag had
the same hazard: it rewrote "the newest marker" even if that was a
batch marker. Empirically confirmed with a probe (batch marker content
destroyed in one pass).

Fix, three parts:
- Micro-created markers now carry MICRO_COMPACT_MARKER_KEY; supersede
  and defrag only ever touch micro-tagged markers. Rehydration in
  _resolve_compact_cursor tags the marker it absorbs (containment
  proof), which safely covers adopting a batch marker as the new
  rolling base after a reset.
- compress() success path resets micro rolling summary/cursor state so
  a stale summary can never claim cumulativeness over a batch marker.
- Regression tests for both directions plus the reset.

W4: _splice_micro_compact_result no longer strips _db_persisted stamps
from surviving messages. Micro archives in place under the SAME session
id (unlike batch's child-session rotation, #57491), so surviving stamps
are accurate; stripping them meant an archive_and_compact failure left
every previously-persisted message unstamped and the next append-only
flush re-inserted them all as duplicate active rows.

W5: finalize_turn micro gate now checks agent._persist_disabled —
persistence-isolated fork agents (background review) must not burn an
aux call per review turn, and must never archive_and_compact the
canonical session rows if their compressor ever gains a DB binding.

W1: _serialize_one_exchange now delegates to _serialize_for_summary
(was a ~70-line near-verbatim copy; one serializer, one place to fix).

S4: _find_one_exchange boundary guard rejects only assistant/tool
boundaries (the actual alternation hazard) instead of requiring user —
a stray mid-list system/injected message can no longer wedge the
cursor forever.

5 new regression tests; 38 micro/prune tests, 400 compression-suite
tests, 61 finalize/persist tests pass; ruff clean.
2026-07-31 17:44:19 +05:30
kshitijk4poor
b8bfd68af1 fix(agent): make micro-compaction alternation-safe and defrag user-preserving
Two integration bugs found during review of #74522, both confirmed with
empirical probes against the production message-repair path:

1. Alternation: the summary marker was role="user" and an exchange was a
   single assistant+tools group, so splicing between two user turns produced
   user -> marker(user) -> user. The pre-request repair_message_sequence pass
   (conversation_loop.py, runs before EVERY API call) then merged the marker
   into the neighbouring real user message: metadata gone, cursor
   unrecoverable on resume, and the summary text duplicated into the
   transcript on every later pass (the transcript GREW every turn).
   Fix: an exchange is now a full agent turn (assistant + tools + follow-up
   assistant iterations, bounded by user messages), the marker is
   assistant-role, and superseding an old marker deliberately merges the two
   adjacent real user turns (plain-text \n\n-join, identical to repair
   pass 2) so the returned transcript is alternation-valid by construction.
   Probe result: repairs 0 (was 2), marker survives, no summary leakage.

2. Defrag destroyed user messages: _defrag_rolling_summary serialized the
   whole remaining middle (user turns included) and spliced it away —
   8 of 10 user prompts destroyed in one pass, contradicting the feature's
   "your messages are never compacted" invariant. Fix: defrag now
   re-summarizes only the rolling summary TEXT and rewrites the marker
   content in place; transcript shape, cursor, and user turns untouched.
   Probe result: 10 of 10 user prompts survive.

Also: marker provenance is now COMPRESSED_SUMMARY_HAS_USER_TURN_KEY=False —
micro markers absorb only assistant/tool content (#64650 invariant), and
real user turns remain in the transcript for provenance detection.

Adds 5 regression tests (repair-pass integration, alternation on
multi-iteration tool turns, defrag user survival, defrag input scope,
marker provenance); updates the two existing tests and the design doc to
the corrected semantics. 28 tests pass.
2026-07-31 17:44:19 +05:30
Michael Jordan
9ca4ee72ca feat(agent): make the micro-compaction cadence configurable
The on/off switch was the only knob. A pass fired after every completed turn,
absorbed exactly one exchange, and there was no way to ask for less. Since a
pass is also what breaks the prompt-cache prefix, "how often does it run" and
"how often do I pay a cache break" are the same question, and it had no answer.

Add `compression.micro_compact_every_n_turns` (default 1, clamped to >= 1). At 1
the behaviour is what it was; at 5 you get a fifth of the breaks and a fifth of
the reclaim rate. The counter advances per invocation rather than per committed
pass, so a turn that finds nothing to absorb still moves the cadence along and
cannot wedge it, and a bogus 0 or negative degrades to "every turn" instead of
silently disabling compaction.

Also expose `micro_compact_defrag_threshold_tokens`, which has been a hardcoded
attribute on the compressor with no path from config since it was added.

This does not give micro-compaction the prune's reclaim-size gate -- a pass
still commits whatever the single absorbed exchange saved. It makes the break
frequency tunable, which reaches the same end by absorbing less rather than by
waiting for a bigger win. The docs now say that plainly, including that a
reclaim threshold is the obvious follow-up and does not exist yet.

Tests cover the skip-until-due window, the cursor and prefix staying untouched
on skipped turns, the clamp, and that the feature is off unless enabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
60781a0cc8 fix(agent): do not destroy compacted history when a session resumes
The rolling summary lives only in memory. A resumed session starts with an
empty one while the marker carrying every previously absorbed exchange is
still in the transcript. The first pass after a resume therefore built a
marker from a single exchange and superseded the marker holding the entire
history -- silently discarding everything micro-compaction had accumulated.

This was introduced by the supersede fix. Before it, markers piled up
wastefully, but nothing was ever lost.

Two changes, so a single failure cannot lose data:

Rehydrate. When the cursor is recovered by scanning the transcript -- the
resume path -- also recover the rolling summary from that marker, so the
next pass merges into the existing history instead of replacing it.
Extraction uses rfind for the heading because SUMMARY_PREFIX references the
heading text itself, so the first occurrence is inside the preamble.

Gate superseding. Earlier markers are dropped only when this pass's summary
is demonstrably cumulative, i.e. the rolling summary was non-empty going in.
If rehydration ever fails, the pass keeps both markers: wasteful, but the
history survives.

Tests cover the resume path, the failed-rehydration fallback, and the
round trip of a summary through a marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
d626528560 fix(agent): derive the micro-compaction cursor from the spliced list
The cursor was set to the pre-splice `exchange_end`. A splice collapses the
absorbed span -- an assistant plus its tool results, often four or more
messages -- into a single marker, and may also drop a superseded marker
further back, so every index after it shifts.

The stale cursor therefore overshot, landing inside a *later* exchange's
tool group. The next pass's `_find_one_exchange` walked forward from there
to the following assistant, so the exchange it had landed inside was never
absorbed at all. On tool-bearing conversations micro-compaction was
silently doing roughly half the work it should.

Traced on a 3-tool-per-exchange transcript: the cursor sat at index 6 when
the marker was at 2, and the message count stalled at 32 instead of
continuing to 28.

Derive the cursor from the marker's actual position in the spliced result
instead, which is self-correcting regardless of how much the splice moved.
Apply it on the defrag path too, which had the same staleness.

Found by a randomized long-horizon harness (480 conversation shapes x 25
passes, varying tool-group sizes and summarizer failure modes) asserting
structural and progress invariants after every pass. Existing tests missed
it because their fixtures have no tool results, so the absorbed span is one
message and nothing shifts. The regression test uses tool groups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
ac48add3a7 feat(agent): report context occupancy, not just tokens saved
Tokens saved is the wrong headline for this feature. Micro-compaction is
not an efficiency optimisation — the same summarization work happens either
way. What it buys is (a) that work amortized across turns instead of one
stall, and (b) a window kept low enough that a session runs much further
before needing a hard compaction at all.

Neither shows up in "net tokens saved". A session can save nothing on paper
and still be a clear win on both counts.

So the telemetry now carries occupancy: tokens_after as a share of the
compaction threshold, plus the threshold and resolved window it was
computed from. That is the number that says whether a session has headroom
left. The report leads with it, and cross-references the batch
`compression_attempt` lines already in the log so it can show how often the
long pause actually fired — ideally never.

Occupancy is read from the cached threshold only. The public
`threshold_tokens` property resolves lazily and can issue a synchronous
/models probe (#32221); telemetry must never be the thing that blocks a
turn, so an unresolved window reports null. In practice a pass has already
resolved it via the tail calculation, so the field is populated. A test
pins the no-forcing behaviour directly against the emitter.

The report is pure ASCII: `scripts/check_subprocess_stdin.py` currently
dies on a cp1252 console before printing its results, and a diagnostic tool
that crashes on the platform it is diagnosing is worse than no tool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
cac9526d2a feat(agent): token telemetry for micro-compaction
The existing log line reports message counts, which is the least
informative number available here: absorbing one tool-heavy exchange can
drop hundreds of tokens while moving the count by one. There was no way to
answer "is this actually helping?" from a real session.

Emit one content-free JSON line per pass, in the same shape as the batch
compaction telemetry: before/after tokens, the delta, the size of the
absorbed exchange, the rolling summary size, duration, and running
per-session totals so a whole run can be read off the last line. No
transcript content rides along.

Add scripts/micro_compaction_report.py to aggregate those lines into
passes, outcome mix, net tokens saved, mean exchange size and durations,
with an optional per-session breakdown.

Measuring it immediately surfaced something worth documenting: the first
pass in a session normally *costs* tokens. The summary marker carries a
fixed ~400 tokens of scaffolding, paid on pass one against a single
absorbed exchange. From pass two the marker is replaced rather than added,
so the overhead is already paid and each exchange is close to pure saving.
Break-even is typically the second or third pass. Tests cover the
telemetry contract, the cumulative totals, and that first-pass/later-pass
shape so nobody reads a single turn and concludes it made things worse.

The estimator costs ~5 ms at 600 messages and ~20 ms at 1200, taken twice
per pass, post-turn — and only once an exchange is actually in hand, so
turns that no-op early pay nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
214b5d8612 docs(agent): state that user turns are never micro-compacted
`_find_one_exchange`'s docstring described an exchange as "(optional) user
message + assistant message + its tool results", but the walk skips past
user messages and starts at the assistant, so user turns are never absorbed
into the rolling summary.

The code is right and the docstring was wrong. Assistant output is largely
an account of what was done and survives summarising with little loss. The
user's messages are the intent everything else is derived from and cannot be
reconstructed from the work that followed — paraphrasing "use the existing
helper, don't add a new one" into a summary is how an agent ends up doing
the opposite six turns later. They are also cheap: a prompt is normally a
tiny fraction of what one tool result costs.

Correct the docstring, document the property (and its cost — a floor on how
small the middle can get, since user turns accumulate), and add a test so it
stays deliberate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Michael Jordan
186cad02f9 feat(agent): per-turn micro-compaction to amortize context compression
Batch compaction pauses a session for one large summarization once the
window fills. Micro-compaction spreads that cost out: after each completed
turn, `finalize_turn` folds the single oldest un-absorbed exchange
(assistant message plus its tool results) into a rolling summary, so the
work happens in small increments during post-turn idle time instead of one
long stall.

Mechanics:
  - a cursor tracks the first message not yet absorbed, recovered from the
    transcript's last summary marker when in-memory state is unavailable;
  - protected head and tail windows are never touched, so the system prompt
    and recent turns stay verbatim;
  - the absorbed span is replaced by a marker carrying the usual
    `_compressed_summary` metadata, so resume, handoff and `/compress`
    treat it exactly like a batch summary;
  - `archive_and_compact` keeps the session DB in step, otherwise the
    append-only flush would leave the original rows active and a resume
    would double-load both summary and originals;
  - when the rolling summary itself passes a token threshold it is
    defragged: re-summarized in one shot and the cursor jumps to the tail;
  - an exchange the summarizer can't handle is retried a bounded number of
    times, then skipped, so one poison exchange can't stall every turn.

Keep only the newest summary marker. The rolling summary is cumulative, so
each marker already contains everything the previous ones held; leaving them
stacked near-duplicate copies of the same text, each with its own heading and
end-marker scaffolding, and the transcript grew on every turn instead of
shrinking. Measured over six turns on a 12-exchange conversation with tool
output: 4104 -> 4797 tokens before, 4104 -> 2572 after.

Off switch: `compression.micro_compact: false` (default on).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 17:44:19 +05:30
Gille
2de1e86c16 fix(cli): stabilize custom provider identities
Use providers keys as the canonical custom-provider identity while accepting legacy bare keys, display-name slugs, bare custom fallback, and doubled custom prefixes across resolution, pickers, doctor, and runtime reverse lookup.

Co-authored-by: Bakhtier Sizhaev <bakhtiersizhaev@users.noreply.github.com>
2026-07-31 17:26:42 +05:30
kshitijk4poor
98105f31f4 fix(file_ops): harden new-file umask chmod for portability
Follow-ups on top of #70888's cherry-picked fix:

- Replace the $((0666 & ~0$u)) shell arithmetic with POSIX who-less
  'chmod "=rw"'. zsh (reachable via _find_bash's $SHELL fallback on
  bash-less hosts) parses leading-zero constants as decimal and silently
  chmods a garbage mode (e.g. 0210); the symbolic form is spec-identical
  across bash/dash/busybox-ash/zsh and degrades to mktemp's 0600
  (pre-fix behavior) rather than corrupting perms if chmod rejects it.
- Move the new-file chmod after the content stream so the temp file
  stays owner-writable while cat runs.
- Run the chmod on a '[ ! -e "$t" ]' check after cat instead of the
  stat/else branch, keeping the overwrite path untouched.
- Update the stale perms comment #70856 called out (new files did NOT
  land with default umask perms pre-fix).
- Tests: select the atomic-write script by content instead of call
  order (the previous last-call capture only worked because the bare
  MagicMock's falsy-exit early return suppressed later execs), assert
  behavior at explicit umasks 0022/0002/0077 via parametrize, add an
  overwrite mode-preservation regression guard, and dedupe the
  real-subprocess env fake into make_real_subprocess_env() shared with
  TestSearchFilesFallbackHiddenPaths.

(webtecnica's email mapping already exists in contributors/emails/ on
current main; the PR's check-attribution red was stale-base only.)

# Conflicts:
#	tests/tools/test_file_operations.py
2026-07-31 14:22:38 +05:30
Ben Barclay
ce6dd1a65f
fix(sync): read org state from the org endpoints, not the personal ones (#75237)
Org-shared skills were unusable past the first propose. Three defects, one
root cause plus two that it masked.

ROOT CAUSE — org reads went to the personal endpoint.

`SyncClient.get_refs()` / `get_object()` only ever called `/v1/sync/refs`
and `/v1/sync/objects/:hash`. Those routes are hard-scoped server-side to
the token's own owner, so asking them for `refs/org/<id>/` returns the
caller's PERSONAL refs rather than an error, and org objects 404. Both org
call sites read org state through them:

- `pull_org_skills` resolved head=None for a populated org and reported
  `{"ok": true, "head": null, "updated": []}` — org skills silently never
  arrived, which reads as "my org has no skills" rather than as a failure.
- `propose_skill` resolved base_head=None, so the FIRST propose to an org
  succeeded by accident (`from: null` happened to be correct) and EVERY
  later one CAS'd against a head it had never seen -> 409 -> a raw
  `SyncConflict` traceback. Worse, it built its root from an empty skill
  map, so a landed CAS would have REPLACED the org set rather than splicing
  into it — the 409 was accidentally preventing data loss.

Fix: `org_scope=True` on `get_refs`/`get_object`, threaded through
`get_commit_json`, `get_tree_json`, `_root_tree_of_commit`,
`_skill_trees_of_root`, and `materialize_tree` — walking an org commit needs
the org route on every hop, not just the first. Both org call sites now go
through one `_read_org_head()` helper.

ALSO FIXED

- `propose_skill` retries on conflict. When the org HEAD moves between the
  read and the CAS (another member proposing, an admin merging), it
  re-splices this one skill onto the NEW head and retries, bounded at 5
  attempts. Re-splicing rather than replaying the old root is what stops a
  concurrent proposal being dropped.
- An empty `actual` in a 409 means "the ref does not exist", not "here is a
  commit". `SyncConflict` normalizes "" to None in its constructor, and the
  personal push path redoes the CAS as a create instead of fetching "" as an
  object — which surfaced as the baffling `object  not found` (doubled
  space). This is what a client hits after switching sync planes, since
  `.sync_state` is not environment-scoped and carries a foreign head.

THE MOCK WAS THE REASON THIS SHIPPED

The test mock served org refs and org objects off the personal routes, so
21 org tests passed against a client that could not work against the real
plane. The mock now mirrors production: `/v1/sync/org/refs` and
`/v1/sync/org/objects/:hash` exist, org objects live in a separate scope,
and the personal routes refuse org content. Two existing tests had to be
corrected to assert against the org scope — they had been passing on the
mock's over-permissiveness.

Tests: 5 new (org head invisible on the personal route; second propose
splices and preserves the first; pull resolves a real org head; empty
`actual` -> None; push recovers from a stale cross-plane head). Verified
they FAIL without the fix: reverting just `_read_org_head` to the personal
route fails the second-propose test and the pre-existing splice test.
1278 passed / 0 failed across 54 suites via scripts/run_tests.sh.

Verified against PRODUCTION with a real org token, not just the mock:
- `pull_org_skills` -> head `sha256:1adf9333…`, materialized
  `software-development/gateway-gateway-connector` into the `_org` mirror
  (was head=None, updated=[]).
- A second `hermes sync propose` succeeded where it previously raised, and
  the org set afterwards contains BOTH skills with the new commit
  descending from the first.
2026-07-30 22:31:42 -07:00
Teknium
c0689c3bcb test(tui): make _load_enabled_toolsets assertions tolerant of first-release back-filled toolsets
The two exact-list assertions in test_tui_gateway_server froze the toolset
list and broke the moment _RECENTLY_SHIPPED_TOOLSETS back-filled bfl onto a
saved platform list — the exact behavior the sibling change ships on purpose.
Assert the invariant instead: the expected base set is present, and anything
extra must be inside _RECENTLY_SHIPPED_TOOLSETS (vacuously exact again once
that set empties between releases).
2026-07-30 16:34:08 -07:00
rob-maron
97c6a183af auto populate flux3 in tools for nous portal users 2026-07-30 16:34:08 -07:00
Teknium
524ab53994 fix(telegram): apply media read_timeout to all upload send paths, not just video
send_video got the 60s read_timeout but send_voice/send_audio/send_photo/
send_document/send_media_group/send_animation upload through the same PTB
request path and hit the same server-side processing wait before the
response arrives. Same class, all sites: they all pass
_MEDIA_SEND_READ_TIMEOUT now. Also drops an unused test helper.
2026-07-30 15:20:09 -07:00
rob-maron
dcd7a95704 higher telegram media limits 2026-07-30 15:20:09 -07:00
rob-maron
4c7cc62f9f flux3 messaging system fixes 2026-07-30 15:20:09 -07:00
rob-maron
4a798f4bce
improve polling for FLUX3 video gen (#75010)
* wait between polls
2026-07-30 16:29:27 -04:00
rob-maron
07447bd5db
nous portal video gen (#74963)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Check no committed infographics (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Deploy Site / deploy-vercel (push) Waiting to run
Deploy Site / deploy-docs (push) Waiting to run
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Waiting to run
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build, Test, and Publish / merge (push) Blocked by required conditions
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
2026-07-30 14:52:15 -04:00
kshitij
14abd64b00 test: drop change-detector test, keep behavioral test
test_generated_script_contains_umask_else_branch asserted on shell
script text ('else', 'umask', '(0666 & ~0', 'chmod') rather than
behavior — a change-detector test per AGENTS.md. The behavioral
test (test_new_file_gets_umask_default_permissions) already
covers the actual behavior end-to-end via real subprocess.
2026-07-30 21:53:38 +05:30
webtecnica
fbfee8e405 fix(file_ops): apply umask-default permissions in _atomic_write for new files (#70856) 2026-07-30 21:53:38 +05:30
Matt Ezell
7965462d6c fix(compression): choose summary role by template-visible alternation
The compaction summary's role was selected against the LITERAL
neighbouring messages (compressed[-1] / tail_messages[0]). Mistral-family
chat templates (Devstral, Mistral Small 3.x, Magistral) enforce
user/assistant alternation but exempt the tool flow (tool results and
assistant messages carrying tool_calls) from the check, so a protected
head ending [user, assistant(tool_calls), tool] pinned the summary to
role="user" while the last role the template counts is "user": the
backend rejects the whole request with a Jinja alternation error
(HTTP 500). The summary persists in the stored conversation, every
retry replays the identical poisoned history, and the session is
permanently unrecoverable. Fires on EVERY compaction against a
Mistral-strict backend, captured byte-exact via a tee-proxy in front of
a llama.cpp/llama-swap Devstral deployment.

Fix: compute both neighbour roles through _template_visible_role(),
which skips template-exempt messages. The #52160 (Anthropic user-first)
and #58753 (zero-user-turn) forced-user guards are preserved; their
forced shapes (summary-user followed only by exempt messages) are
alternation-safe. When the visible head ends "assistant" and the
visible tail opens "user", no standalone role can alternate and the
existing merge-into-tail fallback now correctly fires (the literal
logic emitted a standalone user summary there: a second poisoning
shape).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGN45sMMbwM8cW9T9ga4ou
2026-07-30 21:07:36 +05:30
Brooklyn Nicholson
901205420f feat(kanban): talk to a running worker without a restart
A running worker now polls its comment thread and folds new operator notes
into the live turn via the OUT-OF-BAND steer channel (list_comments_after +
a heartbeat-driven bridge, watermarked so history isn't re-injected and the
worker's own notes are skipped). No block→comment→unblock dance. Desktop's
composer sends notes live ("delivered within a few seconds") with "Requeue
with note" as the restart option and a help tooltip.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson
346149c4f8 feat(kanban): task effort estimate via the auxiliary model
An "Estimate" action asks the auto-routed auxiliary model for a rough token
count + complexity band (S/M/L) with a one-line rationale — tokens, not
dollars, since providers don't report cost reliably. POST /estimate (typed
title/body, for the create dialog) and POST /tasks/{id}/estimate (existing
cards) share one core. Desktop renders it inline ("~15k tok · Medium") with a
"makes a model call" disclaimer; SDK exports compactNumber.
2026-07-30 07:18:08 -05:00
Brooklyn Nicholson
027ef381a4 feat(kanban): scope boards to a project
Boards gain an optional project_id. When set, the board's default_workdir
mirrors the project's primary repo and every new task inherits the project —
a deterministic worktree + branch per task — unless it names its own. New
GET /projects; board create/patch/list carry project_id + resolved name; the
create dialog defaults its workspace to the board's and allows a per-task
path override. Desktop: "Board settings…" gains a project picker.
2026-07-30 07:18:08 -05:00
brooklyn!
d8a9c17dae
fix(tui): keep the pending model shown at turn end instead of blipping back (#74766)
A model picked mid-turn is applied at the next turn start, but the end-of-turn
session.info (_emit_settled_session_info) reads the still-live agent — the OLD
model — and clobbered the optimistic paint, so the UI showed the new model, then
snapped back to the old one when the turn ended, then switched for real on the
next turn. Report the queued pick's model/provider in _session_info while a
switch is pending (it IS the model the next turn runs), so the display stays on
the user's choice through the settle. Cleared once the switch applies.
2026-07-30 10:32:55 +00:00
brooklyn!
f27d45e288
feat(tui): reach the model picker without wrecking your draft, and switch mid-turn (#74756)
* feat(tui): Ctrl+O opens the model picker without clearing your draft

Reaching the model picker meant typing /model, which forces you to wipe
whatever you'd already drafted. Bind Ctrl+O to open the same picker overlay
directly, leaving the composer untouched. Ctrl+O is added to the textInput
pass-through allowlist so the composer doesn't swallow it, mirroring the
existing Ctrl+X session-switcher path.

* feat(tui): apply a mid-turn model switch at the next turn instead of rejecting it

Picking a model while a turn was streaming hit a 4009 'session busy' reject:
switch_model() mutates the agent's model/provider/base_url/client in place and
the worker thread reads those every iteration. Now config.set queues the pick
in session[pending_model_switch] and _apply_pending_model_switch applies it on
the turn thread at the next turn start, before any model call — no race, no
interrupt, no waiting on the client rebuild. The TUI paints the pick optimistically
and notes '(applies next turn)'.
2026-07-30 10:18:30 +00:00
brooklyn!
5e807390fd
Merge pull request #74487 from NousResearch/bb/update-eol-churn
fix(update): repair managed checkouts still running core.autocrlf=true
2026-07-30 04:53:24 -05:00
Brooklyn Nicholson
8d112c05f7 fix(cli): route Cmd+Backspace and Cmd+ForwardDelete to the kill bindings
Terminals that rewrite Cmd+Backspace to Ctrl+U already reach
unix-line-discard. Kitty keyboard protocol and xterm modifyOtherKeys
terminals instead report Cmd as the super modifier bit, producing CSI
sequences prompt_toolkit has no entry for — the raw bytes fall through
the VT100 parser and land in the buffer as literal text.

Alias those to the readline kill bindings prompt_toolkit already ships.
Backspace is a CSI-u codepoint (127); ForwardDelete is a CSI tilde key,
so its modifier rides in the CSI 3 ; mod ~ form rather than CSI-u.
Ctrl+ForwardDelete keeps its own binding — that is delete-word on
Linux/Windows, not kill-line.
2026-07-30 03:39:26 -05:00
Juan Martitegui
0bf471dd68 fix(gateway): preserve voice_only semantics for text input 2026-07-30 00:11:33 -07:00
brooklyn!
fa8b959b92
Merge pull request #74630 from NousResearch/bb/update-restart-race
fix(update): GUI update self-deadlocks against its own lock — every retry fails with "Hermes is still running"
2026-07-30 01:43:38 -05:00
Jeff Watts
53f7d137ed fix(windows): native Windows correctness for CLI, gateway status, banner, and WSL browser paths
Salvaged from #57016 by @lEWFkRAD:
- cli.py: handle file:///C:/... drive-letter URIs on nt (strip the
  leading slash urlparse leaves); join Termux example paths with literal
  forward slashes so hints stay POSIX on Windows.
- gateway/status.py + hermes_cli/gateway.py: normalize backslashes to
  forward slashes before the HERMES_HOME substring match so separator
  style cannot defeat profile ownership detection.
- hermes_cli/banner.py: cprint degrades to plain print when
  prompt_toolkit has no console (NoConsoleScreenBufferError on
  redirected/absent Windows stdout).
- hermes_cli/browser_connect.py: posixpath.join for WSL /mnt/c/... bases
  (os.path.join would emit backslashes on nt).
- Test hardening: symlink skip-guards, USERPROFILE alongside HOME for
  ntpath.expanduser, SIGKILL absence skipif fixed via monkeypatch,
  drive-letter URI / separator-normalization / banner-fallback coverage.

Dropped from the original PR: tests/cli/conftest.py fixture and the
AppSession _output monkeypatch — main's merged tests/cli/conftest.py
already handles that prompt_toolkit pollution.
2026-07-29 23:16:18 -07:00
Brooklyn Nicholson
8c76fe19f8 fix(update): let the GUI updater's hermes update child pass the lock it already holds
The cross-process update lock (fe8e4d93d) made the in-progress marker
mutually exclusive across every update entrypoint — but the Tauri
updater holds that marker for its WHOLE run and then spawns
hermes update as a child stage. The child read the marker, found its
own parent's live pid, refused with exit 2, and the GUI mapped that to
"Hermes is still running. Close all Hermes windows and try the update
again." Retry spawns a fresh updater that deadlocks against itself the
same way, so every GUI-driven update dead-ends on the failure screen
with no winnable retry (observed: three consecutive self-refusals in
bootstrap-installer.log within 90 seconds).

Hand the claim off explicitly: update_child_env exports
HERMES_UPDATE_HANDOFF_PID naming the updater's own pid, and
UpdateLock.acquire treats a live holder matching that pid as the lock
we are already running under — run without claiming, and release
leaves the parent's marker untouched. The env var alone grants
nothing: the pid must also be the live marker owner, so a stale or
forged value cannot bypass the lock, and a dashboard-spawned
hermes update (no handoff env) is still refused exactly as before.
2026-07-30 00:51:33 -05:00
Brooklyn Nicholson
5ecc35f9bb fix(tests): teach gateway-server fakes the include_row_ids kwarg
Slice 1 fell over on eight DB fakes with frozen get_messages_as_conversation
signatures — the new opt-in kwarg is part of the method's contract now, so
the fakes accept **_kwargs like the real SessionDB. Also opts the child-watch
resume projection into row ids: it feeds the same _history_to_messages as the
desktop resume, so reactions on a watched child session address rows the same
way.
2026-07-30 00:08:28 -05:00
Brooklyn Nicholson
1af8839139 fix(state): make _row_id opt-in per consumer instead of universal
CI caught ACP session restore seeing an unexpected _row_id in restored
history — get_messages_as_conversation feeds more than the desktop, and
changing the default shape broke the strictest consumer. Row ids are now
include_row_ids=True, requested only by the gateway's resume/display
projections; ACP restore, export, and inspection get the transcript in its
historical shape.
2026-07-30 00:08:28 -05:00
Brooklyn Nicholson
7d92056c49 feat(gateway): iMessage-style message reactions — storage, RPC, agent tool, model context
Reactions live in the existing messages.display_metadata JSON column (no new
table), with iOS Tapback semantics enforced DB-side: one reaction per author
per message, re-tap retracts, different emoji replaces. The desktop catches up
to the reaction contract five platform adapters already ship.

- SessionDB: set/get_message_reaction, latest_message_row_id (role + offset +
  require_text so invisible tool-call-only rows are never targeted),
  take_unseen_reactions (announce-exactly-once), get_message_role
- message.react RPC: accepts row_id or newest_role for live messages that
  haven't learned their durable id yet
- react_to_message tool: desktop-gated (check_fn), defaults to the user's
  latest visible message, messages_back for retroactive reactions
- Model context rides run_message only (beside the speech-interrupted note):
  the persisted prompt stays clean, so no [The user reacted …] scaffolding in
  transcripts, and no cached prefix ever changes
- Resume projection forwards row_id + reactions; _row_id is stripped from
  outgoing API copies next to display_metadata
2026-07-30 00:08:28 -05:00
Teknium
8eb06e75b9 fix(tests): stub _ensure_vercel_sdk in vercel sandbox tests — CI has no vercel dist
The tests fake the vercel SDK entirely via sys.modules, but
_ensure_vercel_sdk checks installed DISTRIBUTION metadata through
tools.lazy_deps.ensure(): on CI (no vercel dist + lazy installs
disabled) it raised FeatureUnavailable→ImportError before the fake SDK
was ever reached — 16 failures on slice 6, green on dev boxes that
happen to have vercel==0.7.2 installed. Failure mechanism reproduced
locally by forcing _is_satisfied False; fixture patch verified to close
it while the real-dist path stays green (16/16).
2026-07-29 21:30:53 -07:00
mehmetkr-31
6cf4bdd165 test(gateway): fix order-dependent telegram-mock flake cluster
The file-local telegram mock in test_dm_topics.py installed unconditionally
(no __file__ guard), registered a separate string-valued telegram.constants
module, and force-popped the adapter — poisoning the session for any later
telegram test in the same process (assert 'MARKDOWN_V2' in "'MarkdownV2'").

Fix at the source:
- conftest: _FakeEnumMember(str) with PTB-faithful str()==value and
  repr()==<ChatType.X: 'x'>, satisfying both repr assertions and the
  adapter's str(chat.type) normalization; the same object is bound to
  mod.ParseMode and mod.constants.ParseMode.
- test_dm_topics.py: delete the divergent local mock installer; import the
  shared conftest one.
- release.py: mailmap entry for the author.

Verified: the 5-failure cluster repro (dm_topics + slash_confirm +
approval_buttons + model_picker + network_reconnect + telegram_format in one
process) goes 83/83 green (3x); full tests/gateway single-process run drops
10 -> 5 failed, the remainder being pre-existing discord order-dep failures
out of scope here.

Salvaged from #68873. Credit to @liuhao1024 for the earliest root-cause
diagnosis of this str-enum mock class in PR #33875, two months earlier.

Fixes the telegram-mock order-dependent flake cluster.
2026-07-29 21:30:53 -07:00
Jeeves Assistant
080bb83746 test(homeassistant): prevent unit tests from calling live instances
Two tests made real HTTP calls to homeassistant.local:8123 — on a LAN
with an actual Home Assistant instance they could turn on real lights,
and otherwise burned ~10s in network timeouts. Replace with AsyncMock at
_async_call_service and assert the exact production call signature
(domain, service, entity_id, data). 35 pass in ~0.2s.

Salvaged from PR #72634 by @jeeves-assistant.

Co-authored-by: Jeeves Assistant <jeevesassistant00@gmail.com>
2026-07-29 21:30:53 -07:00