Skip to content

feat(bots): make group chat round, message, member and history limits per room - #92213

Open
oliverhees wants to merge 8 commits into
NousResearch:mainfrom
oliverhees:feat/group-chat-configurable-limits
Open

oliverhees wants to merge 8 commits into
NousResearch:mainfrom
oliverhees:feat/group-chat-configurable-limits

Conversation

@oliverhees

@oliverhees oliverhees commented Aug 22, 2026

Copy link
Copy Markdown

The problem

A group chat is capped at GROUP_CHAT_MAX_ROUNDS = 3 and
GROUP_CHAT_MAX_MESSAGES = 10 per user send, with GROUP_CHAT_MAX_MEMBERS = 6
and GROUP_CHAT_HISTORY_LIMIT = 24. All four are module constants.

The message cap binds first, and it binds harder than the numbers suggest,
because the two multiply: with N members one round costs N messages. Running
the current loop shows what a room actually gets:

members rounds reached messages stopped by
6 2 10 messages
4 3 (last one partial) 10 messages

At six members the round cap is unreachable. At four it is reached only
nominally: eight messages are gone after two rounds, so the third round seats
two members and the other two never speak.

That last line is #89545, closed as fixed by #91084. The stranded-harvest fix
addresses the symptom, that late replies were lost and the room looked
abandoned. It does not change the arithmetic: an
Orchestrator → Scout → Builder → Verifier room spends one full round per pass,
and a verifier BLOCK plus a correction handoff plus the re-build is three
passes minimum. Ten messages does not fit that shape, whatever the harvest
does afterwards.

What this changes

Each of the four axes becomes a per-room override with three states: a number,
off, or absent. Absent inherits the shipped default, and the stored form omits
every axis left alone, so a room that never opens the settings stores nothing
and behaves exactly as it does today. Defaults are unchanged.

Turning rounds or messages off hands control to a safety brake, defaulting to
50 rounds / 200 messages, which is itself switchable.

On unbounded rooms, given #91481

#91481 is open: two Telegram bots exchanged 132 messages with no loop guard
anywhere in the admission path. Offering an unlimited group room in that
climate deserves an explicit answer rather than silence, so:

  • The brake is on by default whenever an axis is switched off. Reaching
    "no limit at all" takes two deliberate steps on two separate controls, and
    the editor states in plain words what that means before the user gets there.
  • Unlike the Telegram path, this loop already has two exits that do not depend
    on a counter. spokeThisRound === 0 ends the drive when every member passes
    a round, and a new user message bumps the room epoch, which the loop honors
    at the next member boundary. An unlimited room is a room that stops counting,
    not a room you cannot get out of.
  • A room that stops on its brake records a safety activity event naming the
    axis and the count, so it does not go quiet in a way that reads as "the
    conversation settled".

If you would rather not ship the fully-unbounded state at all, the brake can
be made non-nullable in one line and everything else here still stands. I
would rather ask than guess.

Bounds on what a room can ask for

Every numeric value is clamped, not just validated. A room can raise an axis
but not past these:

axis default ceiling
rounds 3 100
messages 10 500
members 6 24
history 24 200

A value above the ceiling is clamped rather than rejected, so a typo in a
number field lands on the ceiling instead of erroring at the user. A value
that is not a positive number falls back to the default rather than reading as
"no limit", which matters because a stray true coerces to 1 under
Number() and would otherwise pass as a real setting. Both are tested.

The ceilings bound numbers. They do not bound the off state, which is the
whole point of the section above: switching an axis off hands control to the
brake, and switching the brake off too is the deliberate two-step described
there.

Which cap binds first

The message cap, in almost every room, because the two multiply: with N
members one round costs N messages.

members rounds reached on the defaults messages stopped by
2 3 6 rounds
4 3 (last one partial) 10 messages
6 2 10 messages

So raising rounds alone changes nothing above four members. Rather than leave
that to the release notes, the editor computes it live and says so under the
rows: "With 6 bots, the message budget runs out after about 2 rounds, raise it
too, or the round setting will not change anything."

Where it is edited

One component, three surfaces, all writing the same shape:

  • Create dialog, behind a disclosure. Raising the member cap widens the
    picker immediately rather than forcing a create-then-edit round trip.
  • Group settings, next to rename and room picture.
  • Room header, where the label doubles as the current budget at a glance:
    3 rounds · 10 msgs, ≤50 rounds · 10 msgs, ∞ rounds · ∞ msgs.

Because the axes multiply, the editor warns when raising rounds alone would do
nothing: "With 6 bots, the message budget runs out after about 2 rounds, raise
it too, or the round setting will not change anything." That case is the one a
user hits first and the one that would otherwise read as a broken setting.

Overrides persist with the room and ride the existing gateway sync under the
same newest-revision rule as the room picture.

Verification

The plugin suite goes from 362/362 to 376/376; eslint reports no problems.
The 14 added tests cover the resolver (defaults, explicit values, off versus
absent, clamping at the ceiling, and nonsense values falling back rather than
silently disabling a limit), the brake's dependence on its axis, the stored
round trip, the header label in all three states, the multiplication warning,
and two source assertions that the drive loop reads the room instead of the
constants.

Two of those tests were red on the first run and caught real defects: a
boolean true coerced to 1 and read as a valid limit, and a stale reference
to a constant in the sync path. Both are fixed here.

Not in this change

The four constants keep their current values as the defaults. This PR does not
argue that 3 and 10 are wrong for a new room, only that a room should be able
to say otherwise.

… per room

Bot Mode caps every group chat at 3 round-robin rounds and 10 bot messages
per user send, with 6 members and 24 history lines per turn. Those numbers
are module constants, so a room that wants a longer conversation cannot ask
for one. With 6 members the message cap alone ends most rooms inside the
second round, well before the round cap is even reached.

Each of the four axes is now a per-room override with three states: a
number, off, or absent (inherit the shipped default). Rooms that never
touch the settings behave exactly as before, and the stored form omits every
axis left on its default, so an untouched room stores nothing at all.

Turning rounds or messages off hands control to a safety brake, which
defaults to 50 rounds / 200 messages and is itself switchable. Switching it
off too gives a genuinely unbounded room. That is a deliberate choice rather
than an oversight: the drive already ends on its own when every member
passes a round, and a new user message bumps the room epoch, which the loop
honors at the next member boundary. An unlimited room is therefore never a
room you cannot get out of. The editor says so in as many words before the
user gets there.

A room that stops on its brake records a `safety` activity event naming the
axis and the count, so the room does not simply go quiet in a way that reads
as "the conversation settled".

Three places edit the budget, all writing the same shape through one
component: the create dialog (behind a disclosure, where raising the member
cap immediately widens the picker), group settings, and the room header,
whose new label doubles as the current budget at a glance ("3 rounds · 10
msgs", "≤50 rounds", "∞ rounds").

Overrides persist with the room and ride the existing gateway sync under the
same newest-revision rule as the room picture, so a room keeps its rules on
every machine that mirrors it.

Verified: the plugin suite goes from 362/362 to 374/374, eslint reports no
problems. The 12 added tests cover the resolver (defaults, explicit values,
off vs absent, clamping at the ceiling, nonsense values falling back rather
than disabling a limit), the brake's dependence on its axis, the stored
round trip, the header label in all three states, and two source assertions
that the drive loop reads the room instead of the constants.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 22, 2026
First live test on a dark theme: every limit was active, but all four
switches read as off. Radix renders the on state as a slightly different
dark track plus a shifted thumb, and at this size neither is legible against
the panel. A control whose state cannot be read is worse than no control,
because the user then changes a setting to find out what it currently is.

Each switch now carries the state in words next to it: "limit" or
"no limit" for an axis, "brake" or "none" for its safety stop, with the
off state in the warning color so a room running without a bound stands out
in the list. A switched-off axis shows the infinity sign as its placeholder
instead of an empty box.

Plugin suite 376/376 to 378/378. The two added tests assert that every
switch has a state word beside it, so a future control cannot go back to
being unreadable.
@AllanGamal

Copy link
Copy Markdown

Real-world repro on current Desktop main: a two-bot room reached the shipped 3-round cap with the final member reply explicitly addressing the other member. The room logged that directed handoff at 12:35:14, but the callee got no turn; only a new user message at 12:47:45 restarted the drive and delivered it (12m31s silent stall).

This PR gives the room a way to raise its budget, which is useful for deliberate long-form sessions. Two review points from the repro:

  1. Hitting the ordinary inherited round/message cap should surface as capped/safety, not settled; otherwise the user cannot distinguish completion from a dropped directed handoff.
  2. Consider one bounded directed-handoff grace path (only the newest member entry, only explicit @member, only while unseen) so the default room does not silently strand a handoff on its last slot. This stays bounded and does not make @everyone or old mentions extend the loop.

I verified the narrower grace behavior locally with a regression test plus the full Bot Mode plugin suite and TypeScript checks. I am not opening a competing PR while #92213 and #92041 are active; happy to contribute the test/logic here if desired.

@oliverhees
oliverhees force-pushed the feat/group-chat-configurable-limits branch from ae52dcb to a92bd9b Compare August 22, 2026 11:15
Review point 1 from AllanGamal on NousResearch#92213, with a repro on current main: a
two-bot room hit the shipped 3-round cap on a directed handoff, logged
nothing, and stayed silent for 12m31s until a new user message restarted the
drive. From the room, that is indistinguishable from the conversation having
settled, so the user has no way to tell completion from a dropped handoff.

The first version of this branch only reported a stop when the axis had been
switched off, which is exactly backwards: the room most likely to stop
surprisingly is the one still on the defaults.

Both counts now report whenever they end a drive. `capped` is the ordinary
limit and names the way out ("stopped at the rounds limit (3), raise it in
the room budget"); `safety` stays the brake behind a switched-off axis. Both
render in the warning colour, so neither reads as routine.

Plugin suite 378/378 to 379/379.
@oliverhees
oliverhees force-pushed the feat/group-chat-configurable-limits branch from a92bd9b to 9f3f569 Compare August 22, 2026 11:17
@oliverhees

Copy link
Copy Markdown
Author

Thanks for the repro. The 12m31s stall is exactly the case I missed, and your first point is a real hole in this branch.

Fixed in 9f3f569.

I had the reporting backwards. A stop was only announced when the axis had been switched off, which means the room most likely to stop without warning, the one still on the shipped defaults, was the one that said nothing. That is your two-bot room.

Both counts now report whenever they end a drive. capped is the ordinary limit and names the way out: stopped at the rounds limit (3), raise it in the room budget. safety stays what it was, the brake behind a switched-off axis. Both render in the warning colour so neither looks routine. There is a test asserting the report no longer depends on the axis being off, so it cannot quietly slide back.

On your second point: yes, please open it here.

It is a different mechanism from this PR and the two do not overlap much. Raising a budget is a deliberate act by someone who knows the room needs longer. Your grace path catches the case where nobody changed anything and a specific member was addressed but never got a turn. That should hold on the default budget, which is where it matters most.

The three constraints are what make it safe. Newest member entry, explicit @member, only while unseen. None of that can grow into "@everyone keeps the loop alive".

I will rebase around whatever you land. If opening against my branch is easier for you, that works too.

One thing worth settling early: if a grace turn wants to fire while the room is already at its message cap, my instinct is to let the cap win and still log capped. Otherwise the grace path spends a message the budget did not allow, which is a small hole but the kind that gets found later at a bad moment. I am not attached to it if you see it differently.

First live test: with every limit active, all four switches read as off. I
had invented a row layout instead of following the one the app already uses
for exactly this, a labelled control with a description.

src/app/settings/primitives.tsx has ListRow and ToggleRow for that shape: a
container-queried grid, label and description on the left in
--conversation-text-font-size, the control right-aligned via
@2XL:justify-self-end, and a haptic on toggle. Plugins cannot import those
primitives, so the structure is mirrored here rather than reinvented, down to
the class names, and both switches now fire haptic('tap') like every other
toggle in the app.

The number field moves next to its switch instead of sitting in a separate
column, which is what pushed the switch into a cramped corner where its state
was hard to read at all.

Three tests hold the shape: the grid classes, the two haptics, and the
infinity placeholder on a switched-off axis. Suite stays green at 380.
Second live test: the switches are readable now, but every row stacked, and
the number field's border was hard to see.

Both came from copying the app's settings row without adapting it to where it
lives. ListRow splits at @2XL, which is 672px of container width. These rows
sit in a 448px dialog, so the split never fired and each row rendered label
over control. The rows now split at @xs with a fixed 11rem control column,
which fits the dialog and still stacks in anything narrower.

The field carried a hard w-20. The standard number field in
src/app/settings/config-field.tsx has no width at all: it fills its column
and takes CONTROL_TEXT ('text-xs'). At 5rem the border had almost no edge to
read against. The field now fills the control column next to its switch.

The label keeps the full left column and may wrap to two lines, which is what
the description text needs anyway.

Tests assert the early breakpoint and the absence of both the @2XL split and
the fixed width, so this cannot drift back. Suite green at 380.
…ast round

My first answer to NousResearch#92213 review point 1 put the report in the wrong place. It
fired at the top of the final round, before it was known whether the budget
would end the drive at all. A room that settles in that round reported a limit
stop that never happened, and a room that ended earlier reported nothing.

The real problem was one level up: `finally` recorded `settled` for every
ending, which is what made a budget cutoff indistinguishable from a finished
conversation in the first place.

The drive now records why it exits at each exit (`settled` when everyone
passes, `messages` at the message cap, `rounds` when the loop runs out) and
`finally` reports that once. `capped` for an ordinary limit, `safety` for the
brake behind a switched-off axis, `settled` only when the room really settled.

Checked against the live case that prompted this: two bots agreeing after
three messages still reports `settled`, correctly. Two bots talking through
the budget reports `capped` on rounds; six bots report `capped` on messages;
with an axis off, the brake reports `safety`.

The test now asserts the reason is recorded at the exits and that
`round === caps.rounds - 1` does not come back.
Live test found this one: with rounds set to 1, the room correctly reported
"stopped at the rounds limit (1)", dated "20688 days ago".

The event carried the count as `at`, and recordGroupActivity builds its entry
as `{ at: Date.now(), ...event }`, so the caller's field won. `at: 1` is one
millisecond past the epoch, which relativeTime renders as five decades.

The count now travels as `count`, and recordGroupActivity spreads the event
first so its own `at` and `epoch` always win. Any future caller that ships an
`at` gets its timestamp stamped correctly instead of silently backdating the
event.

Two tests: the record's fields must come after the spread, and the limit event
must not carry `at`. Neither is reachable from a render test, since nothing
asserts on the displayed age.
oliverhees added a commit to oliverhees/aiianer-hermes-extensions that referenced this pull request Aug 22, 2026
Screenshot aus einem echten Lauf am 22.08.2026: Zwei Bots streiten ueber TDD,
der letzte Beitrag spricht @Hermes namentlich an und stellt zwei Fragen.
Hermes antwortet nicht mehr, der Raum meldet turn settled. Genau der Fall,
den AllanGamal in NousResearch/hermes-agent#92213 beschrieben hat.
@oliverhees

Copy link
Copy Markdown
Author

Tested both points on a real desktop today rather than in a harness, and both reproduced.

Point 1 works after the fix. With rounds set to 1, a two-bot room ends like this:

stopped at the rounds limit (1), raise it in the room budget

The same run said turn settled before, which is the ambiguity you described.

The live run also showed my first attempt was in the wrong place. I had the report firing at the top of the final round, before it was known whether the budget would end the drive at all. A room that settled in that round would have claimed a limit stop that never happened. The real problem sat one level up: finally recorded settled for every ending. The drive now records why it exits at each exit and reports it once. That is 45ad8b6.

That run turned up a second bug too, and it is one no test of mine would have caught. The event carried its count as at, which recordGroupActivity uses for the timestamp. So a correct message arrived dated 20688 days ago. The count travels under its own name now, and the record spreads the event first so its own at always wins.

Point 2 reproduced separately. Default budget, two bots arguing about TDD:

stranded directed handoff

The last message is from test-bot and opens with @hermes Du sagst, ..., then puts two direct questions to him. Hermes never answers. The room reports turn settled.

One caveat on reading this screenshot: it predates the fix above, so turn settled was what every ending reported back then. I cannot tell from it whether the round budget was actually exhausted. What it does show is three bot messages against a budget of ten, so the message cap was nowhere near, and a named question left hanging while the room called itself finished.

If the rounds were not spent, that is a slightly wider case than yours: a member can pass, be addressed by name straight after, and the room still ends. Your three constraints would still cover it, since the addressed entry is the newest one and it is unseen. But it would mean the grace path cannot key on the last slot being consumed.

Still happy to have you land the test and logic here.

Live test: a room set to 6 rounds / 30 messages came back as 3 / 10 after a
restart. The budget was written and synced correctly; it was never read.

A persisted room field has three sites: durableGroupChatRooms writes it, the
gateway merge carries it, and the hydrate path rebuilds the room from storage
field by field. I extended the first two and missed the third, so every budget
silently reset on the next launch, which makes the whole feature pointless.

Beyond adding the field, there is now a test that walks the durable shape and
asserts the hydrate path reads every field it writes, minus epoch and running,
which are reset by design. Removing the line again fails it with "these fields
are written but never read back", so the next field to be added cannot repeat
this.
oliverhees added a commit to oliverhees/aiianer-hermes-extensions that referenced this pull request Aug 22, 2026
…liegen

Lauf vom 22.08.2026 mit einem Budget von 6 Runden und 30 Nachrichten. Nach
drei Bot-Nachrichten endet der Raum. Der letzte Beitrag spricht @Hermes
namentlich an und stellt eine Frage. Die Aktivitaetsliste zeigt drei replied
und kein passed, Hermes kam also nicht an die Reihe. Anders als im Repro zu
NousResearch/hermes-agent#92213 war das Rundenbudget hier nicht erschoepft.
@oliverhees

Copy link
Copy Markdown
Author

@AllanGamal One more run, and it narrows your point 2 quite a bit.

Same two-bot room, but with the budget raised to 6 rounds and 30 messages. It still ends after three bot messages.

thread

The last message is from test-bot and closes with @hermes: Wenn die Implementierung der Referenzpunkt für die Testkorrektheit ist, wie verhinderst du dann, dass deine Tests nur das bestätigen, was dein Code ohnehin schon tut? …. Hermes never answers. The room reports turn settled.

Here is the activity list for that run:

activity

Three replied, and no passed at all.

That third detail is what makes this worth reporting, because it rules things out. runGroupChatMemberTurn records { kind: 'passed' } when a member declines, so a pass would be visible here. It is not. And I simulated the delta gate on its own with the real logic: with two members answering every round it never blocks, six rounds would yield twelve messages.

So Hermes did not decline, and the delta did not skip him. He was not among the responders in the first place, which puts it in resolveGroupResponders and the mention parse behind it, before the budget is ever read.

That is a different trigger than your repro suggests. Yours reads as "the final slot went to the handoff", which a grace path could key on the budget running out. This room had 27 of 30 messages left. Whatever drops the addressed member happens earlier than the budget, so keying on exhaustion would miss this case entirely.

None of that path is touched by this PR: resolveGroupResponders, rotateGroupSpeakers, parseGroupChatMentions and the watermark writes are all zero lines in the diff. I stopped digging at that point rather than debugging upstream selection inside a PR about budgets, but I would rather hand you a narrowed cause than a screenshot.

Reproducing takes about two minutes: two bots, ask them to argue opposite positions and not concede until the other refutes. Three separate runs here ended after three messages, on budgets of 3, 6 and 6 rounds.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

The multiplicative framing is what makes this PR convincing: N members × up to 3 rounds means the message cap is the effective constraint (6 members → 2 rounds, 10 messages), so raising limits without per-room control would either stay useless for small rooms or explode large ones. Making all four caps per-room configuration while keeping module constants as defaults is the right shape, and the measured before/after table shows the actual behavior rather than asserting intent.

Two suggestions:

  1. Upper bounds on room config. Per-room GROUP_CHAT_MAX_MESSAGES-style settings need sane ceilings (or at least validation warnings) — a misconfigured room allowing hundreds of agent messages per user send turns a group chat into a token furnace. If validation exists in the diff, say so in the description; if not, add it alongside tests for the boundary.

  2. Interaction with round cap. With both configurable, document which binds first when they conflict (the table implies message cap wins) so users setting rounds=10 don't expect ten full rounds at high member counts.

@oliverhees

Copy link
Copy Markdown
Author

Both of these were already in the diff, and the fact that you had to ask means the description was doing a poor job. I have added two sections to it rather than answer only here.

On the ceilings: every numeric value is clamped, not merely validated.

axis default ceiling
rounds 3 100
messages 10 500
members 6 24
history 24 200

A value above the ceiling lands on the ceiling instead of erroring at the user. A value that is not a positive number falls back to the default instead of reading as "no limit". That second one is not theoretical: Number(true) is 1, so a stray boolean would otherwise have passed as a real setting, and a test caught exactly that during development.

One honest qualification, since your concern is a token furnace. The ceilings bound numbers. They do not bound the off state, which is a separate feature and deliberate: switching an axis off hands control to a safety brake that defaults to 50 rounds and 200 messages, and switching that brake off as well takes a second, separate control. The reasoning, including why an unbounded room is still not a room you cannot get out of, is in the section above the new ones, written against #91481.

On which cap binds first, you read the table correctly, and it is worse than "message cap usually wins":

members rounds reached on the defaults messages stopped by
2 3 6 rounds
4 3, the last one partial 10 messages
6 2 10 messages

Above four members, raising rounds alone does nothing at all. Leaving that to documentation felt like leaving a rake in the grass, so the editor computes it live and says it under the rows: "With 6 bots, the message budget runs out after about 2 rounds, raise it too, or the round setting will not change anything." That line is the first thing a user hits when they try to make a room talk longer.

OutThisLife added a commit that referenced this pull request Aug 28, 2026
GROUP_CHAT_MAX_ROUNDS and its four siblings carry over at the values
plugin.js shipped, so no rebase inherits a behavior change on top of a
rewrite. Making them configurable is live contributor work — #92213 for
per-room limits, #96842 for config plus a token budget — and both want the
same single seam, so say so where the constants are instead of adding a
config hook this PR has no consumer for.
andredezzy pushed a commit to andredezzy/hermes-agent that referenced this pull request Aug 29, 2026
A Bot Mode room stops after 3 serial rounds and 10 member messages. That
suits a quick two-bot exchange and cuts short a workflow whose hops are
genuinely serial: a coordinator triaging to a domain expert, to a data
operator, and back needs more passes than three, so the room reports
'turn stopped at the round/message cap' with a bot still holding an
addressed handoff.

Read the ceilings from a group_chat block in config.yaml, falling back to
today's values when it is absent, so the person paying for the model calls
decides what a room may spend.

Config is advisory, never authoritative: each value is clamped to a hard
ceiling (20 rounds / 100 messages / 20 continuations), and anything below 1
or unparseable reads as unset rather than as zero. A typo can neither
silence a room nor uncap it.

The limits resolve once at plugin load and are snapshotted per drive rather
than read inside the round loop, which is hot; a gateway round-trip there
would be a stall the user feels. A config edit reaches a room on its next
send. Reading fails silently to the shipped defaults, because a room that
runs beats a room that refuses to start because config could not be read.

Ports the design from NousResearch#96842 (beplee), which was closed unmerged when
NousResearch#96726 deleted the plugin.js it patched, onto the new modules. Uses the
existing config.get 'full' key rather than adding a gateway method.
Per-room overrides (NousResearch#92213) can layer on the same seam.

Refs NousResearch#98004, NousResearch#96842, NousResearch#89545
@ahrazzle

ahrazzle commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Operating evidence for the persistence/survival half of this PR, from a six-member team room.

We run a six-bot orchestration room (coordinator -> researcher -> architect -> builder -> verifier). Our stop condition is the message cap, exactly as this PR's table predicts: six members one round is six messages, so the 10-message budget seats barely two rounds and the tail of the roster never speaks on a multi-pass handoff. Raising rounds alone would change nothing. Confirmed live, not in a harness.

The point I have not seen raised yet is what happens to the room's budget across an app update, and that is where per-room persistence earns its keep over a bundle-level override.

Before this PR was on our radar we raised the caps by patching the shipped renderer bundle (the constants are inlined into the minified assets). Two consequences we hit:

  1. An app update replaced the bundle, so the override was silently lost and the room reverted to 3 rounds / 10 messages / 2 continuations. The minifier also renamed the variables, so our pattern detector could no longer even find the caps to re-apply them; the room ran on the restrictive defaults for hours until we rebuilt the patch against the new build shape.

  2. Because the caps were patched code, no state distinguished "the room settled" from "the room hit a cap I no longer know exists." A stalled handoff read as quiet consensus.

Per-room overrides that persist with the room and ride the existing gateway sync are the durable answer to both: the budget survives an app update because it lives in room state, not in the bundle, and a room that stops on its (visible) budget reports capped/safety instead of going silent. That is the property that makes this the right design for long-lived team rooms, and it is the reason a config-only global ceiling (e.g. the #98047 / #96842 approach) would not fully close the gap for us: it still depends on re-reading configuration that an update could leave in a default state, whereas room-persisted overrides ride the same sync that already keeps the room alive across restarts.

@alt-glitch alt-glitch added area/config Config system, migrations, profiles sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 29, 2026
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
GROUP_CHAT_MAX_ROUNDS and its four siblings carry over at the values
plugin.js shipped, so no rebase inherits a behavior change on top of a
rewrite. Making them configurable is live contributor work — NousResearch#92213 for
per-room limits, NousResearch#96842 for config plus a token budget — and both want the
same single seam, so say so where the constants are instead of adding a
config hook this PR has no consumer for.
@mazierovictor

Copy link
Copy Markdown

Confirmed against current upstream main (d4d4ecf, Hermes Agent v0.21.1): this is an intentional hard limit, not an account or runtime failure. The current modular TypeScript implementation enforces it in both directions:\n\n- apps/desktop/src/plugins/hermes-bots/create-dialog.tsx:1151, :1223, :1268-1299: selection reaches GROUP_CHAT_MAX_MEMBERS, the copy says Pick 2–6, and unchecked rows are disabled after six.\n- apps/desktop/src/plugins/hermes-bots/group-chat.ts:1214: GROUP_CHAT_MAX_MEMBERS = 6.\n- group-chat.ts:250: synced room members are sliced to that constant, so bypassing the UI would still lose members in the mirror.\n- website/docs/user-guide/bot-mode.md:95 documents 2–6 Bots.\n\nThis is a feature gap for larger orchestration teams. Please port the per-room/configurable member limit to the current modular TypeScript tree; this PR still changes the deleted plugin.js. Keep 6 as the default, expose a validated ceiling/override, and preserve the sync and turn safety caps. #101101 is already marked duplicate of this PR.

@DataJockey13

Copy link
Copy Markdown

I have a current modular TypeScript implementation of the core per-room runtime limits, rebased onto current main. It exposes max rounds, max Bot replies, and follow-on mention rounds in Group Settings; persists and gateway-syncs them per room; keeps the existing 3/10/2 defaults; and raises the group member picker/sync cap from 6 to 12.

It is intentionally narrower than #92213: no unlimited mode/safety brake, history override, per-room member override, header budget summary, or create-dialog advanced editor. The branch passes all 62 Hermes Bots UI test files (580 tests), desktop typecheck and lint, and the contributor attribution audit.

Since #92213 is currently conflicting and still targets the former plugin.js implementation, would you prefer that I open this as a clearly labelled smaller successor/replacement PR against the current modular TypeScript code, or coordinate it another way? I have pushed the ready branch here: https://github.com/DataJockey13/hermes-agent/tree/feat/group-chat-configurable-limits. I do not want to open a silent duplicate.

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

Labels

area/config Config system, migrations, profiles comp/desktop Electron desktop app (apps/desktop/*) comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants