Skip to content

fix(kanban): coordinate notifier ownership per profile - #72277

Closed
DeliciousHouse wants to merge 106 commits into
NousResearch:mainfrom
DeliciousHouse:hermes-agent/t_0f0114f8-reimplement-kanban-notifier-ownership-fr
Closed

fix(kanban): coordinate notifier ownership per profile#72277
DeliciousHouse wants to merge 106 commits into
NousResearch:mainfrom
DeliciousHouse:hermes-agent/t_0f0114f8-reimplement-kanban-notifier-ownership-fr

Conversation

@DeliciousHouse

Copy link
Copy Markdown

Summary

  • decouple Kanban notification polling from dispatcher ownership and coordinate duplicate gateways with one advisory lock per serviceable profile
  • stamp subscriptions/tasks with the effective inbound profile, route standalone and multiplex delivery through that profile's adapter registry, and preserve legacy default ownership
  • make retries item-safe, restore API-server wake-before-cursor behavior, validate SendResult(success=False), and preserve complete blocked/scheduled human-review briefs within platform limits
  • document multi-gateway ownership, failover, routing, retry, and troubleshooting behavior
  • bump Hermes Agent to 0.19.1 (2026.7.26)

Test plan

  • Windows focused notifier/mixin/CLI/Telegram/API-server matrix: 240 passed
  • canonical WSL focused matrix (including platform-base): 445 passed
  • uv lock --check
  • Ruff on every changed Python file
  • Windows and WSL py_compile on every changed Python file
  • independent Codex security/correctness review; valid findings fixed and re-tested
  • added secret scan and git diff --check

Broader-suite environment notes

  • repo-wide canonical WSL run was attempted; the minimal dev environment reached 6.7% before collection errors from the optional agent-client-protocol extra, unrelated to this Python-only notifier diff
  • npm run check was attempted; unchanged Desktop/TUI workspaces fail on this Windows host because bippy is absent and POSIX path/editor assumptions fail, while the web and tests-js workspaces passed

Safety and compatibility

  • lock losers do not enumerate boards or open board SQLite connections
  • legacy blank-profile subscriptions remain owned by default
  • notifier-only gateways continue delivery with dispatch_in_gateway: false
  • API-server decision wakes use a trusted envelope around explicitly untrusted worker-authored brief data
  • no production deployment configuration changed

OutThisLife and others added 9 commits July 26, 2026 16:02
…taller

The macOS launcher fast path gates on hermes_is_installed(), which needs
.hermes-bootstrap-complete next to a built desktop app. Nothing in the Rust
bootstrap pipeline ever wrote that marker -- only install.ps1 did -- so every
reopen of /Applications/Hermes.app re-ran setup instead of launching.

Publish the marker atomically (temp sibling + fsync + rename) because
hermes_is_installed() only checks existence: a torn direct write would arm
the fast path against a half-installed tree. A marker write failure emits
BootstrapEvent::Failed so the installer UI leaves the progress state.

Co-authored-by: giggling-ginger <giggling-ginger@users.noreply.github.com>
install.ps1 wrote the marker on Windows and the Rust installer now writes it,
but install.sh -- the path every Mac and Linux CLI install takes -- never did.
A machine set up with install.sh therefore looked uninstalled to the desktop
app, which re-ran first-run bootstrap on every launch.

Stamp the same schema-v1 payload install.ps1 writes, from both the staged
`complete` stage and monolithic main(). An unresolvable HEAD skips the marker
rather than writing one the desktop validator rejects: absent reads as a clean
"bootstrap needed", malformed reads as a confusing half-state.
Marker presence was the launch gate, but the marker is provenance about who
ran the install -- not proof the runtime works. A CLI-installed repo+venv, or
a healthy install whose marker a repair deleted, both read as "never
installed" and dropped the user into first-run bootstrap on every launch.

Split the two questions: classifyActiveRuntime() reports marker validity and
runtime usability separately, and the resolver launches whenever the runtime
is usable, logging when it proceeds without a marker. An unusable runtime
still falls through to bootstrap even with a valid marker, so an interrupted
install can't spawn a dead backend.

Drops isBootstrapComplete(), which had no callers left once the gate moved.

Co-authored-by: iveywest <iveywest@users.noreply.github.com>
Co-authored-by: lihengming <lihengming@users.noreply.github.com>
Repair signalled "reinstall me" by deleting the bootstrap marker. That was
already destructive -- repair is reachable from a transient backend error on a
fully working install -- and it stranded users in first-run setup with no way
back short of hand-writing the marker file.

Carry the intent in an explicit flag instead. Repair forces the next resolve
through the installer and clears itself once the reinstall starts, so a forced
reinstall still works without destroying provenance about how the install was
created.

Closes NousResearch#72166
Adds an `idle-cost` scenario for the symptom Brooklyn reported: with a
thread spinning, resizing the sidebar feels slow. It holds N tiles busy,
pushes NO tokens, and measures the renderer's self-inflicted commit rate
plus fps while dragging the splitter and while typing.

It reproduces immediately. Five busy tiles, nothing streaming:

  idle commits   17.7/sec   (should be 0 — nothing is happening)
  drag           1.4 fps    p95 812ms, worst frame 1.9s
  typing         61 fps     (fine — this is specific to resize)

Attributing the drag window showed 105,385 TooltipProvider renders and
~15s of component time across a 60-frame gesture. Cause: `Tip` mounts a
full Radix provider + Tooltip per call site, and there are ~107 of them.
Radix's Tooltip holds real state and Popper subscribes to layout, so an
unrelated interaction re-rendered all of them.

Mounts the machinery lazily instead, on first hover/focus. Tooltip churn
drops ~4x (105k -> 26k) and drag doubles to 3fps.

Note `defaultOpen` on the armed Tooltip is load-bearing: the pointerenter
that armed it has already fired, so Radix never sees it and the tip mounts
silently closed. A test caught exactly that, and now guards it.

3fps is still bad — the remaining cost is the whole transcript
re-rendering per resize frame (MessagePrimitive.Parts 12,600 renders /
10.5s, Block/Ct 24,300 each, all 100% wasted). Separate fix.
The synthetic gesture oscillated +/-3px, which nets to zero displacement
and can clamp to a no-op — so it reported a confident fps number for a
drag that never moved the sash. Sweeps monotonically now, dispatches
pointer events React's synthetic system accepts (isPrimary/button/buttons),
and records dragTarget + dragMoved so a drag that silently did nothing is
visible in the output rather than passing as a measurement.

Verified: dragMoved now reports 60px where it previously reported 0.
TreeGroup called useStore($layoutTree) to build its right-click menu's
move/split directions. That subscribes every zone — and therefore every
mounted pane and its entire transcript — to the whole layout tree. A sash
drag rewrites the tree once per frame, so dragging the sidebar re-rendered
all five tiles' message lists on every pointermove, for a context menu
nobody had open.

The directions are only read when the menu renders, so read the tree there
with .get() instead. Same lazy shape the neighbouring `closable` prop
already uses.

Measured over one 60px sash drag with five busy tiles:

  commits          83 -> 12
  ChatView        150 -> 10   (4465ms -> 353ms)
  AuiProvider    9450 -> 630  (9868ms -> 774ms)
  TreeGroup       180 -> 12
  TreeSplit        90 ->  6

Also fixes an observer effect in the harness: idle-cost recorded render
attribution *during* the timed gesture, and the counter walks the fiber
tree on every commit. That was large enough to hide this 15x reduction
behind an unchanged fps, so timing and attribution are separate passes now
and `record` defaults off.

Adds scripts/diag-drag-churn.mjs — the probe that found this. It reports
the transcript chain (who above the messages re-rendered) plus every atom
that notified, which is what named TreeGroup instead of leaving it to be
guessed at. Notably the atom list came back EMPTY: this was never store
churn, so the render-attribution path was the only thing that could have
found it.
parseMarkdownIntoBlocksCached bypassed its cache for text under 1024
chars, on the theory that re-lexing a short message is cheap. The lex is
cheap; what it returns is not free. `parseMarkdownIntoBlocks` builds a
fresh array every call (verified in streamdown's dist: `let r=[]` ...
`return r`), and Streamdown mirrors the block list into useState — so a
new array identity for UNCHANGED text re-renders Streamdown and every
Block beneath it.

Most messages are short, so most of the transcript was on the uncached
path. Caching every length cuts the idle cost of five mounted tiles:

  Streamdown   5.2ms -> 2.6ms
  Block        128ms -> 85ms
  Ct           122ms -> 81ms

Cache bumped 64 -> 256 entries to cover the now-larger key space.

This does NOT reduce Streamdown's 105 idle self-renders — array identity
turned out not to be what drives those, and I verified the cache returns
a stable identity, so that root is still open. This is a cost win, not
the churn fix.
Decouple notification polling from dispatch ownership, route subscriptions through their stamped profile adapters, and coordinate duplicate gateways with profile-scoped locks. Preserve item-safe retries, API-server wake ordering, and complete human review briefs.
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #72241 and #56802. This patch adds per-profile advisory ownership locks and notifier independence from dispatch ownership; #72241 salvages profile routing. The overlapping notifier policies need maintainer consolidation rather than a duplicate verdict.

OutThisLife and others added 13 commits July 26, 2026 17:48
…marker-triage

fix: make the bootstrap-complete marker consistent across every install path
…pace

`plainTextInRange` serialized the caret's preceding content through a bare
<div>, but `composerPlainText` appends "\n" to any block element that isn't the
editor slot. So `beforeText` always looked like it ended in whitespace and the
separating space was never inserted — dragging a file in after a word produced
`review@file:...` glued together.

Marking the scratch container with RICH_INPUT_SLOT makes it serialize in the
same coordinates as the editor. Same fix lands in the new `caretOffsetInEditor`,
which measures caret offsets the same way.
Reverts the tooltip half of 4798994; keeps the idle-cost scenario.

Lazily mounting Radix on first hover measured well (105k -> 26k
TooltipProvider renders per drag) but broke 18 tests across 12 files.
Those tests are not incidental: the repo has an established convention of
asserting `[data-slot="tooltip-trigger"]` at mount to prove a control
carries a tooltip, and deferring the mount invalidates all of them at
once. There is also a real behavior risk the convention was protecting —
`asChild` puts the slot on the button element itself, so arming REPLACES
the node, which is exactly the kind of identity change that breaks focus
restoration and ref-holding call sites.

A 4x cut in tooltip churn is not worth reworking every tooltip assertion
in the app plus taking that risk, on a component with ~107 call sites.
If it's worth revisiting, the right shape is probably making
TooltipProvider itself cheap (one app-level provider) rather than
deferring the mount per call site — that preserves the DOM contract these
tests encode.

The genuine win in this branch stands on its own: the $layoutTree
subscription fix (commits 83 -> 12 on a sash drag) is unaffected.
 with NousResearch#60769

Both the wake chat-scope salvage (NousResearch#72191, merged) and the DM-topic
metadata salvage added HERMES_SESSION_CHAT_TYPE plumbing; the rebase
auto-merge kept both copies. Dedupe the ContextVar declaration, _VAR_MAP
entry, set_session_vars parameter/token, and the run.py call-site kwarg,
and prefer the persisted chat_type column with delivery_metadata as the
legacy fallback in the notifier wake path.
The kanban notifier _collect() loop iterates subscriptions without
per-subscription error handling. When claim_unseen_events_for_sub raises
for one subscription (e.g. DB corruption, lock contention), the entire
tick aborts — silently blocking delivery for ALL other subscriptions.

Wrap the per-subscription logic in try/except so one bad subscription
logs a warning and continues to the next, instead of jamming the
entire notifier.

Closes NousResearch#59269
- honor SendResult(success=False) instead of discarding it, so an adapter
  that REPORTS (not raises) a soft send failure — e.g. the Telegram adapter's
  "Not connected" mid-reconnect — no longer advances the cursor past an
  undelivered event and silently loses the notification. Addresses the
  notifier half of NousResearch#31901.
- add block_loop_detected to the notifier's TERMINAL_KINDS so a task routed to
  triage for a human decision (re-blocked past the recurrence limit) actually
  pings its subscribers instead of stalling silently.
- raise MAX_SEND_FAILURES 3 -> 12 (~60s at the 5s tick) so a transient
  Telegram/API outage does not permanently unsubscribe a live channel now that
  reported soft-failures also reach this counter.
- route active-profile-stamped subscriptions via the primary adapter on a
  single-profile gateway (self.adapters[platform] when the stamped
  notifier_profile equals the active profile). Related to NousResearch#56802.

Adds test_kanban_notifier_rewinds_claim_on_reported_send_failure asserting a
reported send failure leaves the event unseen (rewound) rather than consumed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Salvaged from PR NousResearch#63001 (reduced scope): probe each board with the new
read-only kanban_db.count_notify_subs() before the writable connect(),
so boards with zero subscriptions are never opened writable on the 5s
notifier tick (no schema migration, no WAL/-shm sidecar churn, no
checkpoints).

The PR's machine-global .notifier.lock singleton gate was deliberately
NOT salvaged: a lock-winning default-profile gateway cannot deliver a
secondary profile's subscriptions in standalone-profile deployments
(profile routing fails closed in _authorization_adapter), so the lock
could suppress delivery entirely. The probe captures the per-tick cost
win without that regression.
…cted e2e coverage

Follow-ups from review of salvaged PRs NousResearch#59278 and NousResearch#62712:

* test_kanban_notifier_isolates_per_subscription_failure previously
  created the good subscription first; list_notify_subs() has no
  ORDER BY, so the good delivery happened before the bad claim raised
  and the test passed even without the isolation fix. The bad task is
  now created first AND a deterministic-order shim forces the failing
  subscription to be iterated first, so the test fails on the old
  whole-tick-abort behavior.

* New test_notifier_delivers_block_loop_detected_triage_ping: drives a
  block_loop_detected event through one notifier tick end-to-end,
  asserting the triage ping reaches the adapter and the cursor advances
  (the sweeper review of NousResearch#62712 flagged that only DB-level emission was
  tested).
teknium1 and others added 25 commits July 26, 2026 17:48
Parity with Claude Desktop quick-entry-window / ChatGPT Quick Chat.
…hidden-console design

Two halves close the 'legacy pythonw gateways survive updates forever' gap:

1. hermes update now regenerates the installed Scheduled Task / Startup
   launcher scripts (gateway.cmd + gateway.vbs) during the gateway resume
   phase. They are persistence artifacts written once at install time;
   updates never touched them, so pre-aa2ae36c3f installs kept launching
   the gateway through pythonw.exe forever — every descendant spawn
   flashed a conhost (NousResearch#54220/NousResearch#56747) and, since NousResearch#70344, the console-less
   gateway died at startup with RuntimeError: sys.stderr is None (NousResearch#71671).
   The task /TR points at a stable script path, so rewriting the files
   retargets it with no schtasks call and no UAC. No-op for modern
   installs; best-effort so a failed refresh never fails the update.

2. _resolve_detached_python() normalizes a legacy pythonw.exe interpreter
   to its sibling console python.exe when it exists, so the update
   pause/resume argv-replay path (and any other caller handed a legacy
   command line) respawns on the current design instead of faithfully
   resurrecting the old one. Keeps pythonw when no sibling exists — a
   failed respawn is worse than a console-less gateway.
The status bar shipped every affordance it had, so approvals, the terminal
toggle, agents, cron and webhooks sat there permanently for users who never
touched them.

Those five now start hidden and the bar owns a context menu that turns them
back on, persisted per install. Items opt in by naming themselves with
`toggleLabel`, so a plugin contribution that doesn't opt in always shows;
the system icon and the version/update pills are listed but locked on,
since hiding the way back into settings strands the user.

Preferences store the hidden set rather than the visible one, so an item
added to the bar in a later version appears for existing users instead of
staying silently off.
A CDP trace of one sash drag settled what the render counters could not.
I had assumed the remaining cost was layout/paint; it was not:

  script 6770ms | style 1866ms | layout 71ms

Layout was never the problem. The top attributable callsite in our own
code was use-resize-observer.ts at 977ms.

Counting the callbacks named the mechanism exactly: 8,620 ResizeObserver
instances constructed, and during a 40-move drag 2,600 callbacks each
carrying exactly ONE entry — 65 separate callbacks per pointermove. Every
consumer owned a private observer, so N elements resizing under a common
ancestor meant N trips through the observer machinery instead of one
batched delivery. With five mounted tiles that is ~100 user bubbles, each
with its own observer, all woken by a width change.

One shared observer with a WeakMap of target -> handlers. Callers keep
their exact contract: a handler observing several elements is still
invoked once with all of its entries, and unobserve happens when the last
handler for an element goes away.

Verified by trace, before -> after:

  use-resize-observer   977ms -> 42.5ms   (-96%)
  style recalc         1866ms -> 1145ms   (-39%)
  total script         6770ms -> 3929ms   (-42%)

Callback count 2,600 -> 43: one delivery per frame instead of 65.

Adds the two probes that found it. diag-drag-trace.mjs takes a real
timeline trace and prints the style/layout/script split plus the top
script callsites — that split is what disproved the layout theory.
diag-ro-storm.mjs counts RO callbacks vs entries, which is what
distinguished 'a few expensive calls' from 'very many cheap ones'.
Every `Tip` carried its own `TooltipProvider`, and there are ~107 call
sites. Each is a subtree that re-renders when anything above it does, so
they dominated unrelated interactions: 52,784 TooltipProvider renders and
18.3s of component time in a single sash drag.

Radix's provider holds only refs and stable callbacks (no reactive state)
— hoisting one to the app root is what it is designed for. `Tooltip`
still reads delayDuration/disableHoverableContent from context, and the
per-Tip overrides are preserved.

`Tip` keeps a local provider as a FALLBACK, chosen by context: a
component rendered in isolation has no root provider and Radix throws
"`Tooltip` must be used within `TooltipProvider`". Without this, 20 unit
tests that render a single control fail. Inside the app the flag is
always true, so the common path is a bare Tooltip.

This is the shape the earlier lazy-mount attempt should have taken. That
one deferred the Radix subtree until hover, which moved
data-slot="tooltip-trigger" off the mounted DOM and broke 18 tests
encoding that contract. Hoisting keeps the contract intact — every one of
those tests passes unchanged.

Measured on the same drag:

  TooltipProvider   52,784 renders / 18.3s -> gone from the table
  Primitive.div     40.5s -> 13.4s
  Popper            10.5s ->  2.7s
  Tooltip           15.7s ->  4.4s
withFrames ran its own requestAnimationFrame ticker while the gesture body
independently awaited rAF per step. Two rAF consumers, so the observer's
deltas counted the driver's frames as well as the app's — it reported
~3fps for a drag that a single-clock probe measures at ~23fps, and it
never moved no matter what got fixed underneath.

Timing now comes from the same callbacks the body drives (__MARK__).

This also fixes a silent false-negative on the typing pass: it paced on
setTimeout, so the independent ticker was mostly sampling idle waits
between keystrokes and reported a flat 61fps. On the driving clock the
same interaction reports ~30fps with 27 of 40 frames over 33ms — which
matches the 'typing feels slow' symptom I previously could not reproduce.

TYPE now records __TYPE_TARGET__ and the runner throws when no composer is
found, so a pass that measures nothing fails loudly instead of scoring a
perfect 0 deficit — same guard DRAG already had.
…rt-only

Captures medians of 5 runs for multitab and render-churn so tonight's
wins can't silently regress.

idle-cost is deliberately NOT gated. Its render attribution and idle
commit rate are trustworthy and are what the scenario exists for, but the
drag fps it reports (~0.6fps, p95 814ms) contradicts a direct
single-clock probe of the same gesture on the same build (57fps). I ruled
out sash selection, tile setup, render-counter residue, and a 20s soak,
and could not explain the gap — so the metric ships as a report, not a
gate. Gating CI on a number I can't defend would either fire on a phantom
or mask a real stall.

tier: 'report' is outside GATED ('ci','cold'), so the scenario still runs
and prints but neither compares nor writes a baseline.
After N consecutive guardian denials in a session the deny message escalates to a hard-stop instruction. Inspired by ChatGPT Work auto-review circuit breaker.
…goes through OpenRouter

The Nous Portal docs claimed routing 'happens through OpenRouter under
the hood' with OpenRouter-equivalent failover, and that the catalog
'mirrors OpenRouter's model list'. That is not the Portal's contract:
some models route through OpenRouter, others through proprietary or
secondary providers, and per-model routing can change over time.

The stale wording licensed users to expect OpenRouter-proprietary
request extensions (top-level cache_control, session_id sticky
routing, provider preferences) to work through the Portal, producing
misfiled bug reports like NousResearch#71576. Reworded both pages (en + zh-Hans)
and added an explicit note that OpenRouter-specific extensions are not
part of the Portal API contract.
A dedicated /context (alias /ctx) gateway slash command that gives a full
context-window view with:

- Usage gauge: visual bar + fraction + percentage + headroom
- Auto-compression threshold and how far away it is
- Compression count and how much the last one freed
- Cumulative session throughput (explicitly labelled as throughput,
  NOT context size — each call re-sends the window)
- Cascading fallback: running agent → cached agent → SessionStore metadata
  → rough transcript estimate

Not included (per current-main design):
- Cache reporting removed: commit 446b8e2 intentionally removed cache
  reporting from user-facing surfaces because providers that omit cached-token
  details produce misleading values
- Sync DB calls replaced with async_session_store (current main requires
  AsyncSessionStore with await)

Also rewords the /status tokens line from 'Cumulative API tokens (re-sent
each call)' to 'Lifetime tokens billed: ... (not your current context size;
use /context)' to reduce the recurring confusion that the cumulative figure
is the current context window.

Fixes salvation of PR NousResearch#52184 (salvage commit replaces a 12K-commit-behind
fork branch with a fresh implementation against current main, incorporating
reviewer feedback from @whoislikemiha and the hermes-sweeper).
`hermes prompt-size` reported skills as one <available_skills> block total
and tools as one json-bytes total, so there was no way to see which
installed skill or toolset actually dominates the fixed prompt budget.

Add two additive breakdowns to compute_prompt_breakdown (hermes_cli/
prompt_size.py):

- toolsets_breakdown: each resolved tool is attributed to its single
  canonical registry toolset (registry.get_tool_to_toolset_map), summed by
  group. Fully attributable — the grand total equals the existing
  tools.json_bytes minus JSON array framing (2*count bytes).
- skills_breakdown: parsed from the rendered <available_skills> block, one
  entry per skill with two honest, distinct numbers — index_line_bytes (the
  always-on cost of listing the skill) and skill_md_bytes (on-disk SKILL.md
  size, the real read cost paid only on skill_view). Sorted largest-first
  by read cost.

render_breakdown prints both as sorted "Toolsets by size" / "Skills by
size" tables (skills capped at 20; --json carries them all). All existing
keys and output are unchanged.

Runs fully offline (dummy credentials, no network). Tests cover shapes,
largest-first ordering, per-tool attribution reconciling to the total,
namespaced-name parsing, and unmapped-skill handling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extends the cherry-picked /context command (PR NousResearch#52184) and prompt-size
attribution helpers (PR NousResearch#66656) into one visual context view across
surfaces, and absorbs the per-component budget-visibility goal of the
/tokens proposal (PR NousResearch#48470):

- agent/context_breakdown.py: pure renderers over the existing payload —
  a 5x20 glyph block grid (1 cell ~= 1% of the model window), an
  'Estimated usage by category' table with free space, and expanded
  per-skill / per-toolset listings via compute_context_details(), which
  reuses the prompt-size attribution mechanism (skills index-line bytes +
  registry tool->toolset map) converted to the same chars/4 heuristic.
- cli.py: /context [all] renders grid + category table (+ expanded
  listings) from the live agent and in-memory conversation history.
- gateway/slash_commands.py: /context appends the plain-text category
  table (no grid — monospace not guaranteed on messaging platforms);
  /context all adds the expanded listings. Fail-open: breakdown errors
  never break the gauge.
- hermes_cli/commands.py: /context gains the 'all' subcommand; /version
  demoted to /hermes version on Slack to keep the 50-slash cap.
- tests: renderer unit tests against synthetic payloads, registry test,
  gateway /context + /context all + failure-degradation handler tests.
- docs: slash-commands reference + CLI guide entries.

Read-only and locally computed: no provider calls, no prompt-cache impact.

Co-authored-by: RemyFevry <29257684+RemyFevry@users.noreply.github.com>
Co-authored-by: joelbrilliant <joelbrilliant1@gmail.com>
Co-authored-by: CharlesMcquade <6466275+CharlesMcquade@users.noreply.github.com>
…ser-row

Preserve the original prompt when a mid-turn redirect corrects a turn
…prefs

Quieter status bar and sidebar counts
… status indicator

Display-only port of Claude Code /focus; composes with existing /verbose tool-progress modes.
…rf-finish

perf(desktop): drag at 60fps with five streaming tabs
Shows staged and unstaged changes in the current working directory.
/diff shows stat summary + full diff, /diff --stat shows summary only.

Uses git diff directly — no checkpoint system required. Works in any
git repository.

Closes NousResearch#4250
Widen the cherry-picked /diff base (NousResearch#4839 by @SHL0MS) into one
cross-surface implementation, folding in the review feedback and the
best ideas from the two sibling PRs (NousResearch#22703, NousResearch#53527):

- tools/working_diff.py: shared git collection layer — unstaged
  (default), staged, and all (vs HEAD) modes; untracked files folded in
  via `git diff --no-index` so new files appear as additions (Codex
  /diff parity); shlex-split arguments preserve quoted paths.
- CLI: handler moved to hermes_cli/cli_commands_mixin.py per the
  current god-file decomposition (dispatch stays in cli.py), renders
  through the rich console with a 400-line terminal-flood guard.
- Gateway: _handle_diff_command in gateway/slash_commands.py + dispatch
  in gateway/run.py; fenced ```diff output truncated to 60 lines /
  3000 chars before the platform senders apply their own per-platform
  message clamps (tool-progress-style layered truncation). Localized
  strings in all 17 locale catalogs.
- /diff session (from NousResearch#53527): cumulative checkpoint-baseline diff of
  everything Hermes changed, via new CheckpointManager.session_diff();
  docstring records the retained-baseline approximation caveat from
  review. Works on both surfaces; degrades with an actionable message
  when checkpoints are off.
- Slack: /diff routed via /hermes diff (50-slash cap; keeps
  telegram-parity test green and /version native).
- Registry: cross-surface CommandDef with staged|all|session
  subcommands; docs: slash-commands reference (CLI + gateway tables +
  both-surfaces list) and hermes-agent skill reference.
- Tests: tests/tools/test_working_diff.py (real git repos),
  tests/hermes_cli/test_diff_command.py (real git + stubbed checkpoint
  manager), tests/gateway/test_diff_command.py (end-to-end handler,
  real checkpoint store), TestSessionDiff in
  tests/tools/test_checkpoint_manager.py.

Salvaged from the /diff PR cluster NousResearch#4839 + NousResearch#22703 + NousResearch#53527.

Co-authored-by: Ninso112 <ninso112@proton.me>
Co-authored-by: Harshkamdar67 <harshkamdar67@gmail.com>
Decouple notification polling from dispatch ownership, route subscriptions through their stamped profile adapters, and coordinate duplicate gateways with profile-scoped locks. Preserve item-safe retries, API-server wake ordering, and complete human review briefs.
@DeliciousHouse

Copy link
Copy Markdown
Author

Correction verification for exact head a9b50681d2475562c777432b5da9adf7142bf42c against live upstream/main 0fa5e41c86f022bba147797849f0b44865721476.

  • Rebased and resolved the live conflicts while preserving upstream active-profile authz, per-subscription isolation, block_loop_detected, zero-sub probing, Telegram delivery_metadata/chat_type, wake/session behavior, and stale routing-env cleanup.
  • Added cross-process per-subscription delivery locking plus inflight cursor state so notifier ownership transfer cannot overlap send/ack or replay an in-flight item.
  • Windows focused matrix: 308 passed across 12 notifier/mixin/multiplex/Telegram/CLI/tool files.
  • Canonical WSL/Linux matrix: 513 passed across the same paths plus tests/gateway/test_platform_base.py.
  • uv lock --check, Ruff on every changed Python file, Windows + WSL py_compile, git diff --check, and added-line secret/security scan: passed.
  • The remote branch was updated by a normal fast-forward (no force-push); the ancestry-preserving merge commit has the exact validated tree 064ba337465f7917b8702e518e2cdc4ea9344834.
  • Live PR read-back: OPEN, draft=true, MERGEABLE; exact-head status checks had not populated yet and remain required before merge.

No Hermes install/runtime, gateway/config, dispatcher, Telegram, live application board, Jira, production, deploy, ready-for-review, or merge mutation was performed.

@DeliciousHouse

Copy link
Copy Markdown
Author

Blocking re-review of corrected head a9b50681: the focused suites are green, but the ownership/delivery contract still has merge-blocking gaps.

  1. P1 — runtime profile is being persisted as transport ownership. gateway/slash_commands.py:474 stamps source.profile into notifier_profile, but gateway/authz_mixin.py:123-144 explicitly distinguishes that runtime namespace from the credential-owning adapter. gateway/kanban_watchers.py:539-620 later requires an adapter under the stamped profile. Shared-bot profile_routes can therefore strand notifications or choose the wrong credential. Multiplex API-server subscriptions are guaranteed to strand because secondary port-binding adapters are intentionally skipped (gateway/run.py:10060-10067), and the self-wake posts to the unprefixed default route (gateway/wake.py:125). Persist/resolve transport ownership separately from runtime profile, route sends through the credential owner, and wake API sessions through /p/<runtime-profile>/...; add routed shared-bot and secondary API-server E2E coverage.

  2. P1 — the inflight cursor protocol cannot safely recover process failure. claim_unseen_events_for_sub() durably advances last_event_id before delivery (hermes_cli/kanban_db.py:9834-9903), while same-profile takeover never rewinds stale inflight state (hermes_cli/kanban_db.py:9620-9629). A crash/cancellation after claim but before send permanently loses the event. Conversely, a crash after external send but before advance_notify_cursor() followed by profile transfer rewinds and duplicates the already-sent event. The current tests cover live lock interleavings, not these process-death windows. The failure model and recovery semantics need to be made explicit and exercised with pre-send and post-send process termination.

  3. P1 — one slow/hung adapter blocks unrelated profiles and transfer. The single watcher holds the per-sub delivery lock across an unbounded push send (gateway/kanban_watchers.py:641-992, :1233-1236) and processes every profile serially. API self-wake can hold the loop through multiple 600-second attempts (gateway/wake.py:34, :136-180). That violates the required different-profile progress, while ownership transfer gives up after 60 seconds (hermes_cli/kanban_db.py:9446). Add bounded send/wake handling and isolate per-profile progress; test one permanently hung adapter while another profile continues and ownership transfers.

  4. P1 — the TUI/desktop subscription consumer was not migrated to the new claim/ack and event contract. It claims at tui_gateway/server.py:11255 but never calls advance_notify_cursor(), so retained successful notifications remain marked inflight and replay after a later profile transfer. _KANBAN_NOTIFY_KINDS at :11138-11145 also omits scheduled and block_loop_detected, and the blocked formatter at :11175-11177 truncates the human-review reason to 160 characters instead of preserving the approval brief documented by this change.

  5. P2 — chunk retry state is only event-level. If a later chunk fails, successful prefix chunks are resent after rewind (gateway/kanban_watchers.py:803-824, :1233-1236); after twelve failures the subscription is removed and the unsent tail is lost. Persist chunk progress or define/test an idempotent delivery mechanism so long review briefs do not duplicate prefixes or disappear partially.

Verification on this exact head: uv lock --check, Ruff on all 13 changed Python files, Windows and WSL py_compile, git diff --check, and added-line secret-pattern scan all passed. Focused Windows matrix: 473 passed across 12 files. Focused WSL matrix: 679 passed across 13 files. Independent Codex review reached the same do-not-merge conclusion. GitHub currently reports no PR checks, so CI is not green either.

This PR remains draft and was not marked ready or merged. The one engineering correction cycle has already been used, so I am routing the remaining decision to dev-lead rather than opening a second correction bounce.

@DeliciousHouse

Copy link
Copy Markdown
Author

Closing after the single correction cycle remained below the merge contract. No risk waiver or same-branch exception is authorized; a clean-upstream replacement will use an explicit at-least-once delivery contract that prefers no lost human-floor notifications while bounding duplicate risk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.