Skip to content

feat(hosted-rooms): designated-owner routing as the no-match responder fallback - #105856

Open
angel12 wants to merge 3 commits into
NousResearch:mainfrom
angel12:claude/q2-designated-owner-routing-e29b85
Open

angel12 wants to merge 3 commits into
NousResearch:mainfrom
angel12:claude/q2-designated-owner-routing-e29b85

Conversation

@angel12

@angel12 angel12 commented Sep 8, 2026

Copy link
Copy Markdown

Implements the no-match fallback tier of group-room responder selection, as agreed with
@andredezzy on #98610 (that PR owns the client-side relevance filter; this one takes the
fallback and stays out of group-rounds.ts), under the five-point contract accepted on #95163.

What changes

Round 0 of a hosted-room Discussion (plan_next_task in gateway/hosted_room_discussion.py)
now resolves responders in three tiers:

  1. Explicit @handle mentions, or @everyone / @all. Always wins.
  2. Otherwise the room's configured owner, alone.
  3. Otherwise (no owner configured) the whole roster. Byte-for-byte today's behavior.

The owner is an optional boolean owner field on exactly one roster member, set through
groups.create. It is room configuration, not a registry of room names, and carries no
deployment-specific defaults. Validation: must be a boolean, at most one per roster.
HostedRoomService.create_room persists the flag.

Unavailable owner. If the owner's round-0 turn ended turn.deferred or turn.failed, the
responder list becomes owner first (already terminal, so skipped) followed by the rest of the
roster in order. Each fallback member's prompt carries one line:
[Owner @<handle> was unavailable for this message; it is routed to you.]
The fallback is derived purely from the durable log: no connectivity probing, no retry loop,
bounded by the existing 3-round / 10-message caps. A pass by the owner is an answer and settles
the round as silence, as any full-pass round does today. A mentioned owner never triggers the
fallback (tier 1 wins outright). A second deferral keeps the fallback going; a later owner
answer ends it without a second owner turn. Rounds 1 and 2 (bot-citation driven) are unchanged.

Contract check

Point Where
1. Mentions and @everyone always win _round_zero_responders, tier 1; fixture cases incl. @research @ALL
2. Unmentioned human message never gets zero responders tiers 2 and 3 plus the fallback list; test_unmentioned_human_message_never_has_zero_responders
3. Default responder is generic/configured owner flag on the roster row; no names or profiles in code
4. Offline/deferred owner falls back visibly, no loops typed terminal events trigger it; notice line; existing caps; retry-transition tests
5. Every groups.* client sees the same decision selection is server-side only; Desktop is untouched

Determinism and the member digest

plan_next_task stays pure. reconstruct_task_plan is unchanged: it rebuilds a task from its
turn-id coordinates and compares, which is independent of why a member was selected, and a
fallback task at member_index >= 1 round-trips through admit, restart, and reconstruction
(test_owner_fallback_task_is_deterministic_and_reconstructs_after_restart).

Ownership is frozen with the roster at creation, so _member_digest deliberately remains
unchanged. Future roster mutation must preserve or version the routing configuration used by
admitted tasks; hashing a member's own flag would not cover another member's changed routing.
No room.members_changed emitter exists today, so an owner change is a new room.

Shared contract for the client

tests/gateway/hosted_room_responder_precedence.json is a language-neutral table of
precedence cases driving one parametrized test here. It is the hand-off for group-rounds.ts:
once #98610's relevance tier lands and the client has a real "no match" state, it can adopt the
same cases without this PR touching that file. That is what makes "one policy, both clients"
credible without a turf conflict.

Prerequisite commit

The first commit fixes a pre-existing budget bug in _build_prompt: transcript lines were
admitted up to the full remaining budget and the [Earlier content omitted ...] marker was then
appended unaccounted, so a valid transcript within ~46 bytes of the driver limit raised instead
of planning the turn. Reproducible on main with no owner involved; the fallback notice moves
the boundary onto previously serviceable inputs, so routing must not land without it. Happy to
split it into its own PR if preferred; merge order is fix first either way.

Out of scope

Tests

Red-first for every tier and contract point, in the existing test_hosted_room_discussion.py
idiom, plus test_create_room_persists_the_designated_owner_flag in the service suite.

scripts/run_tests.sh tests/tui_gateway/ tests/gateway/test_hosted_room_discussion.py \
  tests/gateway/test_hosted_room_execution_policy.py tests/gateway/test_hosted_room_gateway_lifecycle.py \
  tests/hermes_cli/test_web_server_boot_handshake.py
=== Summary: 126 files, 1171 tests passed, 0 failed, 8 skipped ===   (rebased on c8aa5608c2)
ruff check .  -> All checks passed

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) needs-decision Awaiting maintainer decision before any implementation sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 8, 2026
@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

PR 105856 — feat(hosted-rooms): designated-owner routing as the no-match responder fallback

Well-structured routing change: no-mention messages go to the configured owner (else everyone, as before), and an owner whose round-0 turn terminally deferred/failed keeps the leading slot while the rest of the roster covers — with an explicit unavailable-owner notice in the covering prompt. The terminals set→dict change (latest-kind-wins) is the minimal enabler.

Strengths (verified in diff)

  • Fallback triggers only on turn.deferred/turn.failed; a pass counts as an answer and a mentioned owner is never a fallback (_round_zero_responders). The trigger semantics are exactly right.
  • owner validated strictly (must be bool, at most one per roster) and round-tripped through the TUI service serialization. No silent coercion of 0/1.
  • Prompt-truncation loop now reserves marker bytes before placing lines — the old code could overshoot MAX_PROMPT_BYTES by the marker length. Genuine off-by-N fix bundled in.
  • Digest exclusion of the owner flag is documented with the invariant it depends on (roster frozen at creation). Good.

Findings (all Non-blocking)

  1. Non-blocking — truncation corner: when the oldest line (index == len(delta) - 1, reserve = 0) doesn't fit, the loop appends "Earlier content omitted" even though no older lines remain unplaced — the dropped line is the current one, not earlier content — and the marker bytes are unreserved in exactly that case. Consider reserve = marker_bytes whenever the marker will be appended, or a distinct "oldest message truncated" marker.
  2. Non-blocking — _member_digest is now owner-blind: two rooms differing only in the designated owner hash identically. Fine under the frozen-roster invariant, but if the digest is ever used as a cache/admission key across roster versions, add a routing-config version to the hash rather than one member's flag (as the comment itself anticipates).
  3. Non-blocking — _OWNER_UNAVAILABLE_NOTICE names @{handle} to covering members. Appropriate transparency, just confirm the notice can't leak into user-visible transcripts beyond the intended member prompts (it rides _build_prompt, which is member-prompt-only — looks contained).
  4. Non-blocking — terminals dict keeps the last terminal event per (round, member) in validation order; confirm validated is seq-ordered so "latest" is genuinely latest and not file order.

No blocking issues.

angel12 and others added 3 commits September 11, 2026 08:14
…pt budget

_build_prompt admitted transcript lines up to the full remaining budget and
then appended the '[Earlier content omitted ...]' marker without accounting
for it, so a valid transcript within ~46 bytes of the driver limit raised
'Discussion prompt exceeds the driver limit' instead of planning the turn.
The marker's bytes are now reserved while older lines remain unplaced; the
truncated-first-line path shares the same reserve. Covered by a boundary
sweep through plan_next_task that fails on the previous code.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r fallback

Round 0 of a hosted-room Discussion now resolves responders in three tiers:
explicit @mentions or @everyone, else the room's configured owner, else the
whole roster (unchanged behavior for rooms without an owner). The owner is an
optional boolean 'owner' flag on one roster member, validated in the policy
module (boolean, at most one) and persisted by the service's create_room.

An owner whose round-0 turn ended deferred or failed keeps its slot and the
rest of the roster follows in the same round with a visible prompt notice, so
an unmentioned human message never has zero responders and the fallback is
derived purely from the durable log: no probing, no retry loop, bounded by the
existing round and message caps. A pass by the owner is an answer and settles
the round as silence, as today. Mentioned owners never trigger the fallback,
a second deferral keeps the fallback going, and a later owner answer ends it
without a second owner turn.

plan_next_task stays pure and deterministic and reconstruct_task_plan is
unchanged. Ownership is frozen with the roster at creation, so _member_digest
deliberately remains unchanged; future roster mutation must preserve or
version the routing configuration used by admitted tasks.

tests/gateway/hosted_room_responder_precedence.json is a language-neutral
precedence contract the Desktop client's group-rounds.ts can adopt once its
relevance tier lands, without touching that file here.

Depends on the preceding prompt-budget fix: the fallback notice moves the
omission-marker boundary onto previously serviceable transcripts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ariants

Answers the two open review findings on the designated-owner routing
change without altering behaviour:

- Prompt budget: every delta line but the oldest is placed only if the
  omission marker still fits after it, so the marker always has its
  bytes whenever a later line is dropped; the oldest may take the full
  remainder because nothing older can be omitted after it, and when it
  is the line dropped it is still earlier than everything shown. State
  that at the loop and pin it with two maximal user messages: the
  newest survives whole, the marker leads, the prompt fits.
- `terminals` latest-wins: `validated` is in strict `seq` order
  (`_validate_event` rejects anything else, already covered by the
  fail-closed log test), so the last write per (round, member) is the
  highest-seq terminal. Say so where the dict is built.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@angel12
angel12 force-pushed the claude/q2-designated-owner-routing-e29b85 branch from 4da8983 to 718dafb Compare September 11, 2026 14:23
@angel12

angel12 commented Sep 11, 2026

Copy link
Copy Markdown
Author

Rebased onto current main (test-file-only conflict from the shared-state.db isolation work). Findings 1 and 4 are addressed in 718dafb86b — both turned out to be invariants rather than defects, so I pinned them with comments and a test instead of reshaping the loop: the non-oldest lines always leave the marker its bytes, and validated is strict-seq-ordered by _validate_event. Still gated on needs-decision for the Q2 direction from #95163.

@angel12

angel12 commented Sep 22, 2026

Copy link
Copy Markdown
Author

@teknium1 Flagging this against your #98610 decision, since the two overlap and I'd rather not waste your review time.

You declined #98610 because routing a round to a subset changes who sees what. This PR's tier 2 does route an unmentioned round to one member, so the same objection may apply — with one difference I want to check before you spend time on it: this is the hosted driver, where every member's session still receives the full room log regardless of who is asked to speak, so visibility and order are unchanged; what narrows is only who is prompted to produce a turn.

If the standing ruling covers that too — everyone gets a turn, not just everyone sees the turn — say so and I'll close this. If the distinction matters, I'll add a test pinning that non-responding members still observe the complete round. Either answer also settles Q2 on #95163, which is the last of its three questions still open.

This branch has not been deployed

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

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) 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 type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants