Skip to content

sync: merge main into dy-main - #29

Open
github-actions[bot] wants to merge 10000 commits into
dy-mainfrom
main
Open

sync: merge main into dy-main#29
github-actions[bot] wants to merge 10000 commits into
dy-mainfrom
main

Conversation

@github-actions

Copy link
Copy Markdown

Summary

Automated sync PR because main is ahead of dy-main.

This PR was opened automatically by .github/workflows/auto-pr-main-to-dy-main.yml.

hermes-seaeye Bot and others added 30 commits August 19, 2026 01:35
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…desktop_ui toolset

The agent could reveal single panes (focus_pane) but had no way to arrange
the workspace as one act. apply_layout closes that gap: a desktop_ui tool
that emits layout.apply over the existing bridge, resolved in the renderer
against the layouts contribution registry — the same list the layout picker
reads — so core presets (default/focus/terminal-deck/quad), plugin presets,
and user-saved presets are all addressable by id. Active session only, same
as pane.reveal: a background turn never rearranges the user's desktop.
… chat on screen

The Bots roster highlight and the Routines (Cronjobs) tile were keyed off
host.state.profile — the gateway socket's home. Tab/tile focus moves without
swapping the socket, so opening one bot's chat while the socket was homed on
another highlighted the wrong bot and showed the wrong bot's cronjobs
(community report: Newsanalyst chat open, Hermes highlighted).

- sdk: new host.state.focusedSessionProfile — owner profile of the focused
  chat, resolved from the focused stored session's row stamp via
  rememberedSessionProfile() (same ladder as remembered navigation and the
  HUD), with the gateway profile as the draft/uncached fallback.
- hermes-bots: $focusedBotProfile = focusedSessionProfile || profile
  (feature-detected; older desktops keep prior behavior). BotRow highlight,
  RoutinesPane scope, and the $selectedBot tracker use it. Turn-busy 'work'
  mood stays keyed to the socket-home profile (only it can be mid-turn).
- tests: SDK atom behavior (vitest) + plugin source-shape suite; prewarm
  harness stubs gain the new atom.
- docs: SDK page + hermes-agent skill reference list the new atom.
`/goal clear` (and pause/resume/status) can come back from the gateway as
a TYPED `{ type: "exec" }` command dispatch instead of the plain
`{ output }` slash.exec shape. The typed exec/plugin branch in
use-prompt-actions/slash.ts rendered the output ("✓ Goal cleared.") and
returned immediately — it never reached the goal-store sync that the
plain-output path runs (`applyGoalStatusText`). The composer status stack
therefore kept showing the stale "Goal paused" card, with the old goal
text, until the chat was left and reopened (which re-hydrates via
`refreshSessionGoal`).

Fix: in the typed exec/plugin dispatch branch, when the command is `goal`,
mirror the dispatch output into the goal store via
`applyGoalStatusText(sessionId, output)` before rendering — exactly what
the plain-output path already does. This covers the whole sibling class
(clear/pause/resume/status/done) since the store's text parser already
understands every /goal output shape; set (`send` dispatch notice) was
already handled.

Tests:
- use-prompt-actions/index.test.tsx: typed exec `✓ Goal cleared.` removes
  the session's goal entry immediately (NousResearch#80348), and typed exec
  `▶ Goal resumed:` flips a paused card back to active.
- store/goals.test.ts: `✓ Goal cleared.` output clears a paused goal.

Fixes NousResearch#80348
After a standing goal auto-paused on turn-budget exhaustion, every
surface's /goal resume handler only flipped the persisted state back to
active (and reset turns_used) and rendered an acknowledgement — nothing
re-entered the conversation loop, so the goal sat idle until the user
sent another ordinary message.

Fix the whole class by scheduling the canonical
GoalManager.next_continuation_prompt() through each surface's existing
input path after a successful resume:

- Desktop/TUI (tui_gateway/methods_tools.py command.dispatch): return a
  sendable {type: "send"} dispatch with the continuation as the message,
  a "Continuing now" notice, and display "/goal resume" so the
  transcript shows the concise invocation instead of the model-facing
  scaffolding. No-goal keeps the exec response.
- Classic CLI (hermes_cli/cli_commands_mixin.py): put the continuation
  on _pending_input, same as the /goal <text> kickoff.
- Messaging gateway (gateway/slash_commands.py): enqueue a continuation
  MessageEvent through the adapter FIFO — the same path the post-turn
  judge uses — so queued real user messages preempt naturally and the
  pause/clear stale-continuation cleanup recognizes it.

Also correct the now-misleading gateway.goal.resumed copy ("Send any
message to continue…") across all 17 locale files.

Regression tests cover exact budget exhaustion → resume on the real CLI
handler, the real gateway handler (including the
_is_goal_continuation_event guard contract), and the TUI
command.dispatch boundary; verified each fails on the pre-fix code.

Fixes NousResearch#75362
The _MAX_RESERVED_SOCKETS cap applied to pinned CIMD sockets too, so under
heavy concurrency an ephemeral-reservation churn could close a parked pinned
socket before _wait_for_callback adopted it, silently reopening the
port-stealing window the pin exists to prevent (NousResearch#22161). Eviction now skips
the pinned range; it is already bounded by _CIMD_PORTS.

Follow-up to the NousResearch#84050 salvage.
One renderer coordinator owns every right-click and replaces the
native Electron menus. Menus are assembled from what the click
landed on:

- Links and images get open/copy/save sections; chat links add the
  reach-aware resolved-URL copy on remote gateways.
- Editables get spell-check suggestions (async-appended when
  Chromium's facts arrive from main), cut/copy/paste, and select
  all. Cut and copy need a selection; paste needs a non-empty
  clipboard; select all needs field content. The verbs show their
  accelerators instead of icons and dispatch a frame after the menu
  closes, so the radix focus trap cannot steal the target. Select
  all runs renderer-side, scoped to the field, because main's
  selectAll acts on the focused frame and could grab the transcript.
- Terminals answer through registered xterm handles; the read-only
  agent terminal hides paste.
- The in-app browser guest builds the same menus from the webview
  tag's context-menu event: Chromium's editFlags gate the edit
  verbs, spell-check rides the event, and Inspect element closes
  every menu. Coordinates arrive as window-relative device pixels,
  so the handler divides by the window zoom factor; guest edit
  commands focus the webview first, because they act on the focused
  webContents.
- Bare app chrome falls back to the window verbs.

Labels come from the locale files in all five languages. Main keeps
thin IPC verbs: edit commands, copy-image-at-gesture, spell-check
actions, and dictionary-add for guests (the tag has no session API).
The e2e spec exercises the real focus trap; it is blocked today by
the gateway-checking stall that also fails e2e/chat.spec.ts.
The browser bar gets an open-in-external-browser glyph that opens
the tab's live address. The address field gets a copy control on its
right edge, with the same pre-faded inline appearance as the
code-block copy button. Copy always takes the address the field
shows: on a remote gateway, that is the reach-resolved address. The
page verbs (copy URL, open externally, console, DevTools) live here
and not in the guest context menu, which keeps the node-scoped
tools.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
blobatar 0.2.0 -> 2.0.0 (gen2). Ten silhouettes instead of six:
capsule, triangle, hexagon and droplet join round, organic, boxy,
nub, cloud and sun. BLOB_KIND_TRAIT repinned to gen2 band centers
(empirically verified against the published package: every pinned
value resolves to its named silhouette across seeds). The avatar
picker derives from BLOB_KINDS, so the new shapes appear there
automatically. Note: gen2 remaps most unpinned seed->face mappings
by design (upstream generation change).
`botHandle()` exists so that, per its own comment, "the word 'default'
never surfaces in the UI" — it presents the primary profile as `hermes`.
The roster rows, mention resolution and the group-chat prompt all route
through it. Two preview paths did not, and rendered the raw profile name:

- `GroupRow`'s room preview line built `@${last.from?.name}`, so a group
  room read `@default: …` while the bot answers to `@hermes`.
- `previewKind()` returned the raw captured name from the bot-to-bot
  delivery prefix, so the `🤖 @<name>` badge and its tooltip could show
  `@default` too.

The mismatch is presentation-only, but it reads as a routing bug: the
room says the message came from `@default` while `@default` is not a
handle the mention resolver accepts, so users reasonably conclude
bot-to-bot addressing is broken when it is working correctly.

Both paths now map through `botHandle()`. `GroupRow` passes the matching
member so a bot with a custom handle keeps it; `previewKind` maps the
lowercased sender name, which leaves every non-primary profile unchanged.

Tests: the primary profile resolves to `hermes` and a named profile keeps
its own handle (behavioural, in the existing previewKind suite), plus a
source-shape assertion for the render path matching that file's
convention. Both new assertions fail against the pre-fix source.

Fixes NousResearch#89484
…eaves it stale

nanostores' .listen() never replays the current value the way .subscribe()
does, so the $focusedBotProfile listener in register() only kept
$selectedBot current from the moment it was attached. A disable -> profile
switch -> re-enable cycle (Settings > Plugins) left $selectedBot pointed at
whichever bot was active before the plugin was disabled, so the roster
highlight fallback and Routines scoping could start from a stale bot.

Extract the sync into bindProfileSync(), which reseeds $selectedBot from
the profile store's current value before attaching the listener. This runs
on every register() call, so re-enabling the plugin always starts in sync.

Salvaged from PR NousResearch#89637 (the pane-precedence portion was superseded on main
by the $focusedBotProfile design; this residual reseed gap remained).
Regression test mimics real nanostores get/listen semantics and fails
without the reseed.

Fixes-residual-of: NousResearch#89625
Signed-off-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
…t contract includes the !activeGroup guard
…r on

Every host.openSession call in the Bot Mode plugin omitted
keepAllProfilesScope, so the SDK applied its default and flipped
$showAllProfiles back on whenever the target session belonged to a
different profile than the live gateway (sdk/index.ts:
options.keepAllProfilesScope !== false => setShowAllProfiles(true)).

For anyone running more than one profile this silently undid the sidebar
profile filter: narrow Sessions to one profile, click any other bot, and
the unified all-profiles list came back.

Bot navigation is an explicit context switch into that bot's profile, so
pass keepAllProfilesScope: false at every openSession call site (4 on
current main after the plugin.js refactor consolidated the original 7).

Salvaged from PR NousResearch#89031 onto current main; includes contributor mapping.
…ousResearch#75576)

Some authorization servers and WAFs reject httpx's default User-Agent on
the OAuth token endpoint. mcp_servers.<name>.oauth.user_agent now stamps a
custom User-Agent onto the two token-endpoint requests (authorization-code
exchange and refresh) on both provider construction paths. Opt-in,
per-server, token requests only — never MCP traffic or discovery, and no
other headers are configurable. Empty/null/non-string values are ignored.

Completes the second half of NousResearch#75576 (the CIMD half landed via NousResearch#89566).
runGroupChatRounds' responder selection had no awareness of the stranded/
harvest state the previous round's harvest pass just confirmed. A member
whose turn timed out (marked stranded) but is STILL genuinely running
could be re-selected as a responder in a later round of the same
invocation — resolveGroupResponders has no busy filter, and a member's
watermark is bumped past the stranded timeout regardless of outcome, so a
fresh delta re-qualifies them.

Re-selecting them fires another prompt.submit into their live session.
tui_gateway's _handle_busy_submit treats that as a normal busy mid-turn
prompt: by default it either redirects the live turn in place or, for
older agents, hard-interrupts it and queues the new text as the next
turn. Either way the member's original in-flight work — exactly what the
stranded/harvest mechanism exists to protect — gets abandoned or killed,
undermining the "never lost, just late" guarantee.

Filter responders against the room's current stranded map (freshly
confirmed by this round's own harvest pass) before selecting who speaks.
A member with a live stranded marker is skipped; the next harvest pass
picks their reply up once it actually lands.

Added a regression test exercising runGroupChatRounds end-to-end with a
member confirmed still-busy: without the guard the round loop resubmits
into their session (asserted via prompt.submit call count); with the
guard it never does, and the marker survives untouched. Mutation-verified:
temporarily reverted the filter and confirmed the new test fails
(2 !== 0) fast, without a real wall-clock wait.

Full hermes-bots plugin suite: 246/246 pass (47 files). node --check
clean on both changed files.
Bot Mode group rooms only showed a single "is thinking…" line while bots
ran, and nothing after a turn settled or failed — no way to see what the
room did without reading the whole transcript.

- Runtime-only, bounded activity feed per room (GROUP_ACTIVITY_LIMIT),
  recording truthful turn events: queued, working, replied, passed,
  timed-out, failed, cancelled, settled, delivered.
- Every event is tagged with the room epoch it belongs to; the view shows
  only the CURRENT run, so a superseding send (or a rename that re-keys
  the room) can never surface stale activity.
- Quiet disclosure in the room header: collapsed by default, the collapsed
  row shows the latest event summary; expanding lists the current run's
  events newest-first with per-state glyphs and tones.
- Never persisted and never hydrated — the transcript stays the only
  durable record, so activity cannot be replayed as history.

Tests: 8 new behavioral + source-contract cases in
tests/group-activity.test.mjs (settled arc, failed turn, supersede/cancel,
epoch filtering, bounded feed, runtime-only guarantee, labels, disclosure
a11y contract).
OutThisLife and others added 29 commits August 19, 2026 18:40
…ab-trap

fix(desktop): terminal pane no longer traps the window when you switch tabs
…p uses

The HUD asked for vibrancy directly and always with the 'hud' material —
one of the two rungs the macOS census rejected, because it collapses into
under-window on blur and so changed the frost the moment another app took
focus. It also ignored the translucency setting entirely: Glass off still
frosted, and Windows got nothing at all.

hudFrostFor is the mapping for a transparent window, beside vibrancyFor in
the shared module both processes read. Two gates give it its answer: the
renderer's report that the band actually covers the window, and the user's
Glass setting. Off resolves to no material rather than a resting one, since
a transparent window has no opaque page to hide an unwanted frost behind.

Windows 11 rides setBackgroundMaterial through the same call, so the HUD
follows the frost ladder on both platforms. Main self-diffs and keys the
latch to the window, so a Settings change re-frosts a live HUD, a tint drag
touches nothing native, and a HUD respawned on another profile is not
mistaken for the window that already carried the material.
…lass

The band wore its own card tint at a hardcoded 80/92%, so a HUD beside the
docked window read as a lookalike rather than the same surface, and the Tint
slider moved one and not the other. It now paints --ui-bg-chrome at
--translucency-glass-keep: one painter, one token, one lever.

That needed the setting and the surface rewrite to stop being one flag.
data-hermes-glass means "this window's field surfaces may be rewritten" and
is deliberately false in the HUD, which owns its own backgrounds; the new
data-hermes-glass-on means "the user's Glass setting is live" and is
published everywhere, along with the tint number the band reads.

The 0.5rem side inset drops to zero while glass is on. It exists to keep an
opaque sheet clear of the bar's corner controls, but the frost is the whole
window — an inset sheet left a hairline of bare untinted material down both
sides.

An open completion drawer now drops the frost along with the band it belongs
to. The drawer takes the band to 25% and blurs it while the native material
stayed at full strength, which is the same bare slab in a different
disguise. It mounts without a focus change, so it is observed rather than
passed in, coalesced to a frame because the shell mutates with every
streamed token.
Dictation, spoken replies, the wake word and start-conversation were four
separate icon buttons in a Spotlight bar a few hundred pixels wide — most of
the row spent on toggles that are set once and rarely touched. In the HUD
they collapse into a single menu; the docked composer has the width and
keeps them inline, same controls and same state.

The trigger is not a static glyph. It reports the loudest live voice state —
recording, transcribing, listening for the wake word, speaking replies — and
lights while any is on, because a folded menu that looked idle with the mic
open would be a worse trade than the space it saves. The three toggles are
checkbox rows that hold the menu open on select, so the state you just
changed is the state you can see.

The shared control class names move to a module of their own so the row and
the menus it renders can wear them without importing each other, and the
pressed-toggle tint stops being written out at each of its four sites.
… above it

The exit chip floated over the composer in a 26px transparent strip reserved
for it (--hud-chip-strip), hidden until you hovered the bar. Under glass that
strip is bare untinted material across the top of the HUD — a band of chrome
above the surface, present in every state, holding a control you cannot see.

It rides the composer's controls row now, next to send. That costs no
reserved space and takes about 120 lines of CSS with it: the chip needed its
own placement, hover reveal, leave-hold, and an opaque card to stay legible
over an unknown desktop. None of that applies to a button on the bar, which
is already our surface — the problem was the placement, not the control.

Trade-off worth naming: the way out is now always visible in the HUD rather
than revealed on hover. It is one more permanent glyph on a Spotlight bar, in
exchange for an escape hatch that no longer depends on discovering it.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
host.openSession awaited ensureGatewayProfile with no deadline. That await
gates waitForFocusedSessionHydration, which arms the only timer on the path,
so a profile dial that never settles left the open pending for the life of the
window: the pane froze with no error, no Retry and - the part that made this
hard to recognise - no timeout either. The gateway log signature is a bare
`ws accepted` with no matching `ws closed`.

Bound the activation with its own copy of the wake budget rather than folding
it into the hydration one. A cold profile backend can legitimately spend most
of the hydration budget painting a large transcript, and that race is already
tight enough to lose, so charging activation to the same clock would trade a
wedge for a regression. The timeout reuses the hydration message prefix on
purpose - openSession keys the core stranded-session surface off it - and the
[bot-wake] support log now names which phase expired, so a stuck dial is not
read as a slow transcript.

Scoped to callers that passed awaitHydration. A plain open never asked for a
deadline and has nowhere to render one, so its behaviour is unchanged.

Two existing tests counted microtask ticks between the call and the core open.
The bounded activation adds a tick, so they now flush a macrotask instead,
which asserts the same thing without depending on the await count.

Refs NousResearch#89556
A review of the previous commit found that retrying at the plugin layer
(openStoredBotChat catching and re-calling host.openSession) didn't fix
the reported bug: host.openSession's own catch block unconditionally
calls setResumeExhaustedSessionId on a hydration timeout before
rethrowing, and only an explicit resumeSession() (the manual Retry
button) clears that latch for the currently-routed session. A
plugin-side retry is a different code path that can hydrate the
transcript fine while the full-screen "Couldn't load this session"
overlay stays latched over it.

host.openSession now takes a retryHydrationTimeoutOnce option and
retries the open+hydration-wait internally, before the latch is ever
set, so a successful retry never arms the overlay. openStoredBotChat
just opts in via that option.
Bot Mode passed keepAllProfilesScope:false, which re-homed the sidebar
onto the bot profile. That profile forever-chat is hidden, so Sessions
and the roster looked empty. Opening a bot is navigation, not a workspace
switch. Also restore all-profiles when the bot backend is already live.

Related: NousResearch#89789
Opening a plugin/Bot Mode session is navigation, not a workspace switch.
keepAllProfilesScope (default true) now dials the named backend without
moving $activeGatewayProfile or setApiRequestProfile. Session-owned RPCs
still route to the session owner. Pass false to switch chrome and collapse
the Sessions sidebar.
…ut a deadline

Threading timeoutMs/signal through requestForSessionProfile and
requestGatewayForProfile handed every session-scoped RPC a trailing
`undefined, undefined`. Only the plugin host bridge actually supplies those,
so the rest of the app's calls changed observed arity for no reason — and the
resume/activate paths assert on the exact call shape.

Forward the deadline args only when the caller set them; the plugin bridge
keeps the full four-argument route it needs.
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ive-plugin-init

feat(relay)!: initialize static/dynamic plugins via native integration, remove opt-in plugin
…I is signed out

/api/model/options probed Copilot auth via `gh auth token` four separate
times per payload build. When gh has no credential store for the backend's
HOME (fresh profile, desktop-spawned backend, CI), each probe blocks its
full 5s subprocess timeout on keyring/D-Bus, so every open of the Desktop
Models or Providers settings page took 20s — past the renderer's 15s IPC
budget, painting 'Error invoking remote method hermes:api: Timed out'.

Fix: cache the gh-CLI probe result (hit or miss) for 5 minutes with an
invalidation hook, feed gh stdin=DEVNULL, and disable gh interactive
prompts/update notifier in the probe env.

Measured on the failing profile: 20.5s -> 5.3s cold (one bounded probe),
0.03s warm.
…S markers

The backend's session search wraps matched terms in sqlite snippet()
delimiters '>>>'/'<<<' (hermes_state_search.py). The sidebar rendered the
snippet as plain text via searchResultToSession(), so searching 'foo'
painted rows literally titled '>>>foo<<<'. Strip the markers before the
snippet becomes the row preview.
…h when no search is active

Both cron empty states used search-flavored copy unconditionally; a fresh
panel with zero jobs and no query told users 'Try a broader search
query'. Copy now follows the query state, reusing existing i18n keys.
…reconnect

Community report (X @Cobalt_Peak): Reconnect and Restart gateway in the
statusbar gateway popover rendered the same RefreshCw icon side by side,
so users triggered full gateway restarts when they meant to reconnect.

- Restart now uses a Power icon with a destructive hover tint
- Moved restart to the end of the row, after the system-panel button,
  behind a visual divider separating it from the benign actions
…ow that spins forever

Archived rows render from $archivedSessions (their own capped store —
they're excluded from $sessions by design), but removeSession only pruned
$sessions. Deleting from the Archived filter left the row in place; a
click on it resumed a hard-deleted id: resume 404 -> goneSessionVerdict
saw the row still listed -> 'retry' -> unrecoverable spinner.

removeSession now resolves the row from either store, evicts both
optimistically, restores the archived row on RPC failure, and forwards
the archived row's owning profile to deleteSession.
…endor clamp drift

The NousResearch#89503/NousResearch#70058/NousResearch#74295/NousResearch#87279 bug class kept regenerating because every
transport and provider profile hand-rolled its own effort translation map
(9 sites, 4 distinct policies). New agent/reasoning_effort.py is the single
source of truth:

- EFFORT_LADDER: canonical low->high ordering (superset check against
  VALID_REASONING_EFFORTS pinned by test)
- clamp_effort(): one policy — supported passes verbatim, otherwise nearest
  WEAKER supported level (never escalate, never invert the ladder), floor
  when nothing weaker, 'none' never a degradation target, declared
  vendor-documented overrides win, bespoke names pass through
- declared wire vocabularies as data: OpenAI-compat, Codex Responses,
  xAI (4.6/legacy), Actual relays, Kimi K3/K2, TokenHub, GLM-5.2,
  DeepSeek V4, Ollama Cloud, Meta, Solar

Converted sites (all behavior-preserving except noted):
- chat_completions chokepoint, Kimi + TokenHub paths
- codex transport (backend branches now pick a declared set)
- auxiliary_client Responses path
- hermes_cli.models clamp_reasoning_effort_to_supported -> thin wrapper
- plugins: kimi-coding, zai, opencode-zen, deepseek, ollama-cloud,
  meta-ai, upstage, custom (copilot already routes via the wrapper)

Behavior fixes the shared policy surfaces:
- ollama-cloud/opencode-go 'minimal' now degrades to 'low' instead of
  being dropped (drop left the server default = MORE thinking than asked)

New tests: ladder contract (every configurable level is clamped by every
declared wire set; monotonicity across the full ladder for every set).
…tions seen in CI)

test_progress_advances_while_the_orchestrator_blocks raced its subject on
both edges within one hour of PR CI (NousResearch#90358):

- Run 1: sampled right after the shim URL printed, before the orchestrator
  published its stage — caught the page boot default
  ('Hermes will open once done.' != 'Testing quiet update').
- Run 2 (rerun): with HOLD=4s on a slow runner, the second sample slid past
  the hold and caught the cleared terminal state ('' != 'Testing quiet
  update').

Fix: wait (<=10s) for the published stage to actually land before starting
the 1.5s stability window, and raise the hold to 10s so both samples land
inside it. Same assertions, same contract — just anchored to the event the
test is about instead of wall-clock luck.
…ousResearch#90268)

Worker sessions are deny-listed out of every conversation list, so a
profile grinding through a 30-minute kanban task read idle ('3 hr ago')
with no ACTIVE NOW entry the entire run.

- tui_gateway/methods_profiles.py: profiles.list rows gain worker_session
  — the newest kanban/tool row (id, source, title, last_active). Workers
  heartbeat last_activity_at every <=60s while running (NousResearch#72016), so the
  field stays fresh exactly while work is happening. last_session keeps
  its deny-list contract; include_sessions:false omits the field; older
  clients ignore it.
- hermes-bots plugin: workerActiveAt() (150s window, one missed heartbeat
  of slack) feeds ACTIVE NOW, the row pulse dot ('Working on a task right
  now'), and the row age label while a worker runs. Chat semantics are
  untouched when no worker is live.
- Tests: 4 new pytest (real SessionDB on temp HERMES_HOME), 2 new node
  behavior tests; sabotage-verified.

Session-list visibility of workers (the issue's first half) is left as-is
by design — auto-resume and shared lists must keep excluding workers; the
roster signal was the actionable gap.
@donovan-yohan
donovan-yohan deployed to trusted-automation August 20, 2026 02:36 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.