Skip to content

Support the multi-prefix capability - #99

Merged
codegeek merged 4 commits into
mainfrom
fix/issue-96-multi-prefix
Oct 22, 2026
Merged

codegeek merged 4 commits into
mainfrom
fix/issue-96-multi-prefix

Conversation

@codegeek

@codegeek codegeek commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Clients that negotiate multi-prefix expect every channel status prefix a member holds, highest first (@+nick), so they can show accurate member status. jircd didn't offer it, so CAP REQ multi-prefix got CAP NAK. irctest uses multi-prefix as its sample capability for CAP negotiation, so cap.CapTestCase testReqOne, testReqTwo, testReqOneThenOne and testReqPostRegistration failed. They were the only failures left after excluding deprecated tests.

Changes

  • New jircd-capabilities/multi-prefix module registers the capability so it is advertised in CAP LS and ACKed. Like the other capabilities it is opt-in: capabilities: multi-prefix: enabled.

  • The prefix rendering stays in core, the same split as away-notify and batch, because the capability SPI has no rendering hook yet. [Feature] Let capability modules own their behaviour through a real plugin SPI #98 tracks moving capability behaviour into the modules.

  • Channel.memberListing(boolean) / statusPrefixes(...) render all prefixes in ISUPPORT PREFIX order. NAMES 353 (targeted, bare, on JOIN and SAJOIN) and WHO 352 flags (H@+) use them when the client receiving the reply negotiated the capability; other clients keep the single highest prefix.

  • Reply budgets reserve one more byte for the longer prefix: NAMES 353 is S + 2N + Cmax + 15 (was 14) and WHO 352 +22 (was 21). The default server-name ceiling is unchanged (166 bytes), but a configuration tuned to the old limit by one byte is now rejected at load.

  • Docs (separate commit): README and the spec contracts list the capability and the new formulas.

  • AGENTS.md: the local Sonar step adds -Dsonar.scm.disabled=true. SonarCloud judges new code from git blame, so the documented scan reported nothing for uncommitted changes; that's how the first local scan for this PR missed S1845 and S1612.

Disabling a capability at runtime doesn't yet affect clients that already negotiated it. That's true of every capability today, and #97 tracks it.

Testing

  • ChannelTest: prefix ordering with and without the capability.
  • MultiPrefixReplyTest: exact 353 params for NAMES, bare NAMES and JOIN, and exact 352 flags, including that the receiving client's capability decides the format.
  • MultiPrefixTest (real sockets): CAP LS/ACK, @+alice in 353 and H@+ in 352 for the negotiating client, @alice / H@ for one that didn't negotiate.
  • Budget tests updated for the new formulas.
  • Pre-PR checks: ./gradlew cleanTest build passes; a local SonarCloud branch analysis had the gate OK with 0 issues; a cross-vendor (Codex) review against .github/REVIEW_CHECKLIST.md found only the runtime-disable gap ([Bug] Disabling a capability at runtime leaves it active for clients that negotiated it #97).

Fixes #96

🤖 Generated with Claude Code

Summary by Sourcery

Support opt-in IRCv3 multi-prefix negotiation and render complete channel statuses in client-specific NAMES and WHO replies.

New Features:

  • Add opt-in IRCv3 multi-prefix capability negotiation and advertise it through CAP LS/ACK when enabled.
  • Return all channel status prefixes in NAMES and WHO responses for clients that negotiated multi-prefix, while preserving existing output for other clients.

Bug Fixes:

  • Enable clients and capability negotiation tests that request multi-prefix to complete successfully.

Enhancements:

  • Update reply-size validation and documentation to account for multi-prefix status fields.

CI:

  • Document the Sonar analysis option needed to assess uncommitted changes correctly.

Documentation:

  • Document multi-prefix support, configuration, response behavior, and revised reply-size formulas.

Tests:

  • Add unit, reply-format, integration, and configuration-budget coverage for multi-prefix behavior.

Chores:

  • Register the new capability module in the project and server runtime.

codegeek and others added 2 commits October 22, 2026 12:36
Clients that negotiate multi-prefix need every channel status prefix a
member holds, highest first, to show accurate member status; irctest's
CAP tests also use it as their sample capability, so CAP REQ for it was
NAKed and those tests failed.

A new jircd-capabilities/multi-prefix module registers the capability.
The prefix rendering stays in core, as for away-notify and batch, since
the capability SPI has no rendering hook yet (#98). NAMES 353 (targeted,
bare, JOIN, SAJOIN) and WHO 352 flags list all prefixes in PREFIX order
when the receiving client negotiated it, and keep the single highest
prefix otherwise. The NAMES and WHO reply budgets reserve one more byte
for the longer prefix string.

Fixes #96

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add multi-prefix to the supported capabilities and its reply contract,
and update the 353 (S + 2N + Cmax + 15) and 352 (+22) budget formulas
and examples.

Refs #96

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds configurable IRCv3 multi-prefix support by registering the capability in a new module, gating core NAMES/JOIN/WHO prefix rendering on the receiving client’s negotiated capabilities, increasing reply-budget reservations, and covering negotiation and wire-format behavior with unit and socket-level tests.

Sequence diagram for negotiated multi-prefix replies

sequenceDiagram
    participant Client
    participant CapabilityExtension
    participant JoinCommandHandler
    participant WhoCommandHandler
    participant Channel

    Client->>CapabilityExtension: CAP LS
    CapabilityExtension-->>Client: multi-prefix
    Client->>CapabilityExtension: CAP REQ multi-prefix
    CapabilityExtension-->>Client: CAP ACK multi-prefix
    Client->>JoinCommandHandler: JOIN channel
    JoinCommandHandler->>Channel: memberListing(true)
    Channel-->>JoinCommandHandler: @+nick
    JoinCommandHandler-->>Client: 353 NAMES @+nick
    Client->>WhoCommandHandler: WHO channel
    WhoCommandHandler->>Channel: statusPrefixes(member, true)
    Channel-->>WhoCommandHandler: @+
    WhoCommandHandler-->>Client: 352 WHO H@+
Loading

Flow diagram for client-specific prefix rendering

flowchart TD
    Request["NAMES, JOIN, or WHO request"] --> Negotiated{"Receiving client negotiated multi-prefix?"}
    Negotiated -->|Yes| All["Render all held prefixes in PREFIX order\n@+nick or H@+"]
    Negotiated -->|No| Highest["Render highest prefix only\n@nick or H@"]
    All --> Budget["Use expanded reply budget"]
    Highest --> Reply["Send IRC reply"]
    Budget --> Reply
Loading

File-Level Changes

Change Details Files
Registers multi-prefix as an opt-in, service-loaded capability module and wires it into the server build and configuration model.
  • Adds the capability identifier and extension implementation.
  • Advertises and ACKs the capability when enabled through configuration.
  • Updates module/developer documentation for the supported capability set.
jircd-capabilities/multi-prefix/build.gradle.kts
jircd-capabilities/multi-prefix/src/main/java/net/jircd/capabilities/multiprefix/MultiPrefixExtension.java
jircd-capabilities/multi-prefix/src/main/resources/META-INF/services/net.jircd.core.extension.CapabilityExtension
jircd-protocol/src/main/java/net/jircd/protocol/CapabilityName.java
jircd-server/build.gradle.kts
settings.gradle.kts
AGENTS.md
README.md
specs/001-ircv3-server/contracts/server-configuration.md
specs/001-ircv3-server/contracts/irc-protocol-commands.md
Extends core channel-status rendering so replies contain all held prefixes for clients that negotiated multi-prefix, while preserving the existing highest-prefix output for other clients.
  • Adds requester-sensitive status-prefix rendering in ISUPPORT PREFIX order, including @+.
  • Applies the negotiated capability to targeted and bare NAMES, JOIN/SAJOIN NAMES listings, and channel-scoped WHO flags.
  • Keeps capability behavior in core because the current capability SPI only handles registration.
jircd-core/src/main/java/net/jircd/core/session/Channel.java
jircd-core/src/main/java/net/jircd/core/session/ChannelRegistry.java
jircd-core/src/main/java/net/jircd/core/session/command/JoinCommandHandler.java
jircd-core/src/main/java/net/jircd/core/session/command/WhoCommandHandler.java
Raises reply-size reservations and validation limits to account for the additional status prefix.
  • Increases the conservative NAMES formula from +14 to +15.
  • Increases the widest WHO flags reservation from 3 to 4 bytes and updates related documentation and boundary tests.
  • Adjusts reload and integration scenarios for configurations affected by the tighter NAMES budget.
jircd-core/src/main/java/net/jircd/core/config/ConfigurationLoader.java
jircd-core/src/main/java/net/jircd/core/config/HostmaskReplyBudget.java
jircd-core/src/test/java/net/jircd/core/config/ReplyBudgetConfigurationTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/ReplyBudgetReloadTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/Story4LengthLimitConfigTest.java
specs/001-ircv3-server/contracts/irc-numeric-replies.md
specs/001-ircv3-server/contracts/server-configuration.md
Adds focused unit, reply-fitting, and real-socket integration coverage for negotiation and requester-dependent formatting.
  • Verifies prefix ordering and fallback behavior in channel rendering.
  • Checks exact 353 and 352 output for negotiated and non-negotiated requesters, including JOIN, bare NAMES, away markers, and combined operator/voice status.
  • Validates CAP LS/ACK and end-to-end output over sockets.
jircd-core/src/test/java/net/jircd/core/session/ChannelTest.java
jircd-core/src/test/java/net/jircd/core/session/ChannelRegistryTest.java
jircd-core/src/test/java/net/jircd/core/session/command/MultiPrefixReplyTest.java
jircd-core/src/test/java/net/jircd/core/session/command/HostmaskReplyFitTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/MultiPrefixTest.java

Assessment against linked issues

Issue Objective Addressed Explanation
#96 Register and advertise the multi-prefix capability, and ACK requests both before and after registration. ✅
#96 For clients that negotiated multi-prefix, include every status prefix in PREFIX order in NAMES/JOIN 353 replies and WHO 352 flags; retain the single highest prefix for other clients. ✅
#96 Account for the longer prefix in reply-size validation and documentation, and document the capability as supported. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="jircd-core/src/main/java/net/jircd/core/session/Channel.java" line_range="131-133" />
<code_context>
+   * Without {@code multiPrefix} only the highest is returned. Each symbol comes from a separate
+   * read, so a concurrent privilege change can yield a combination the member never held at once.
+   */
+  public String statusPrefixes(ClientSession member, boolean multiPrefix) {
+    boolean operator = operators.contains(member);
+    boolean voice = voiced.contains(member);
+    if (operator) {
+      return multiPrefix && voice ? "@+" : "@";
</code_context>
<issue_to_address>
**Replies show impossible statuses**

When a privilege change overlaps status-prefix generation for a NAMES, JOIN, or channel-scoped WHO reply, `statusPrefixes` reads `operators` and `voiced` separately while privilege updates are synchronized. A revoke-operator followed by grant-voice between the reads makes a multi-prefix reply show `@+` even though the member never held both statuses.

Read both status sets under the same synchronization used by privilege updates.
</issue_to_address>

### Comment 2
<location path="specs/001-ircv3-server/contracts/server-configuration.md" line_range="270" />
<code_context>
   Let `S` be the resolved server name's UTF-8 byte length, `N` be
   `nicknameMaxLength`, and `Cmax` be `channelNameMaxLength` used as the channel
-  UTF-8 byte cap. Both `S + 2N + Cmax + 14 <= 512` (353 RPL_NAMREPLY) and
+  UTF-8 byte cap. Both `S + 2N + Cmax + 15 <= 512` (353 RPL_NAMREPLY) and
   `S + N + Cmax + 29 <= 512` (366 RPL_ENDOFNAMES) MUST hold. The first reserves
-  a trailing colon and one member status character; the two nickname lengths
</code_context>
<issue_to_address>
**Data model keeps old NAMES budget**

When a configuration consumer relies on the data-model formula to choose boundary values, a consumer following the data-model validation rule calculates the 353 bound with 14 bytes of overhead, while `ConfigurationLoader.validateNamesBudget` requires 15; configurations that rule says fit can therefore be rejected at startup or reload, and the published limits disagree.

Update the data-model validation formula and its explanation to reserve the extra multi-prefix byte.
</issue_to_address>

### Comment 3
<location path="specs/001-ircv3-server/contracts/irc-protocol-commands.md" line_range="317" />
<code_context>
   `@` only, matching standard client expectations (operator implies the
   ability to speak; the `+` prefix communicates *additional* information
   a plain `@` doesn't).
+- **`multi-prefix`** (issue #96): to a requester that has negotiated it, `353`
+  lists every status the member holds, in ISUPPORT `PREFIX` order (`@` before
+  `+`), so an operator who is also voiced is `@+nick`, and `352`'s `<status>`
</code_context>
<issue_to_address>
**Prefix contract contradicts itself**

When an implementer applies the operator-and-voice rule to a requester that negotiated multi-prefix, an implementer reading the normative rule that an operator who is also voiced is prefixed with `@` only gets a different result from the later `multi-prefix` rule requiring `@+nick`; the contract leaves the expected 353 output contradictory for negotiated clients.

Qualify the `@`-only rule as applying when the requester has not negotiated `multi-prefix`.
</issue_to_address>

Sourcery assessment

Approval pending. 3 findings to address first.

Blocking findings: jircd-core/src/main/java/net/jircd/core/session/Channel.java:133, specs/001-ircv3-server/contracts/server-configuration.md:270, specs/001-ircv3-server/contracts/irc-protocol-commands.md:317


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread jircd-core/src/main/java/net/jircd/core/session/Channel.java Outdated
Comment thread specs/001-ircv3-server/contracts/server-configuration.md
Comment thread specs/001-ircv3-server/contracts/irc-protocol-commands.md
Review follow-ups for #96:

- statusPrefixes read the operator and voice sets separately, so under
  multi-prefix a concurrent "-o then +v" could render @+ for a member
  who never held both. Privilege updates now bracket themselves with a
  version counter under the channel monitor, and the lock-free read
  retries when an update overlapped it, so NAMES stays lock-free.
- data-model.md still gave the old 353 budget constant (14); it is 15.
- The "operator who is also voiced shows @ only" rule now applies only
  to a requester without multi-prefix.
- Rename MultiPrefixExtension.ID, which clashed with id() (S1845), and
  pass a typed method reference in ChannelRegistryTest (S1612).

Refs #96

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
sourcery-ai[bot]
sourcery-ai Bot previously approved these changes Oct 22, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

SonarCloud reports only new code on a short-lived branch and judges
"new" from git blame, so the documented scan reported nothing for
uncommitted, staged or untracked changes. That is how the first local
scan for #96 missed S1845 and S1612. With sonar.scm.disabled the scan
reports them, and SonarCloud still leaves out issues already on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sourcery assessment

Approved.

@sonarqubecloud

Copy link
Copy Markdown

@codegeek
codegeek merged commit 5912b68 into main Oct 22, 2026
6 checks passed
@codegeek
codegeek deleted the fix/issue-96-multi-prefix branch October 22, 2026 20:02
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.

[Feature] Support the multi-prefix capability

1 participant