Skip to content

Skip or fit hostmask replies that carry retained over-limit names - #94

Merged
codegeek merged 5 commits into
mainfrom
fix/issue-93-grandfathered-hostmask-replies
Oct 22, 2026
Merged

codegeek merged 5 commits into
mainfrom
fix/issue-93-grandfathered-hostmask-replies

Conversation

@codegeek

@codegeek codegeek commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Why

A reload that lowers nicknameMaxLength or channelNameMaxLength keeps existing sessions and channels ("grandfathered state"). The reply budget from #92 validates only the configured limits, so WHO, WHOIS, WHOWAS, 333, INVITE and channel notifications that embed a retained over-limit name could exceed the 512-byte command-section limit (prefix, command, parameters and CRLF; IRCv3 message tags have their own 8191-byte budget and don't count) and be dropped silently by SessionWriter. NAMES already skips an unrepresentable item with a specific WARN; this extends that policy to the hostmask-carrying replies.

Changes

  • Replies.sendChecked / Replies.isRepresentable: every numeric goes through a size check. A reply whose fixed fields (nick, channel, ident, host, timestamps) cannot fit in the 512-byte command section is skipped with a WARN naming the reply and its byte count, throttled to one per command per minute across recipients with suppressed counts reported. Identities are never truncated.
  • Free trailing text is fitted on a code-point boundary instead of dropping the line: topic text (TOPIC and JOIN), away reason, and the 312/313/315/318/369 closers.
  • INVITE goes through the same check.
  • SessionWriter applies the check to broadcasts (JOIN, PART, KICK, MODE, TOPIC); PRIVMSG/NOTICE keep their existing text fitting.
  • Commands keep going and still attempt their terminating numeric (315, 318, 369).
  • USERHOST already skips an oversized entry with a WARN via sendListed; unchanged.
  • server-configuration.md documents the policy next to the NAMES policy (separate commit).

Testing

  • New GrandfatheredReplyTest (13 unit cases).
  • ReplyBudgetReloadTest lowers the limits by reload with long names retained and checks each hostmask reply either fits in the 512-byte command section or is skipped with a WARN.
  • NamesByteBudgetTest expectations updated: an oversized 366 is still skipped, but its WARN now comes from Replies instead of SessionWriter.
  • ./gradlew cleanTest build passes (1,003 tests, 95.0% line / 87.4% branch coverage, Spotless, SpotBugs, PMD).

Fixes #93

🤖 Generated with Claude Code

Summary by Sourcery

Ensure all outbound IRC replies and notifications respect the 512-byte command-section budget while preserving retained identities and continuing command response sequences.

New Features:

  • Add shared byte-budget handling for hostmask-bearing numeric replies and channel notifications, including INVITE, with retained over-limit identities skipped rather than truncated.
  • Fit free trailing reply text at Unicode code-point boundaries while preserving command processing and terminating numerics.

Bug Fixes:

  • Prevent oversized replies caused by grandfathered nicknames and channel names after configuration reloads from being silently dropped without consistent diagnostics.
  • Ensure oversized outbound notifications and replies are checked consistently across recipients, with throttled warnings and suppressed-count reporting.

Enhancements:

  • Centralize outbound command-section size validation and distinguish oversized fixed fields from text-fitting failures.
  • Clarify the 512-byte command-section and separate IRCv3 tag budgets in user-facing configuration documentation.

Documentation:

  • Document the shared policy for hostmask replies, notifications, fitted trailing text, warning throttling, and terminating numerics.

Tests:

  • Add unit and integration coverage for grandfathered identities, reload-induced reply budgets, fitted closers, broadcast handling, warning throttling, and tag-budget failures.

Chores:

  • Relocate the warning-throttle utility into the shared session package for reuse by outbound-size diagnostics.

codegeek and others added 2 commits October 22, 2026 08:58
A reload that lowers nicknameMaxLength or channelNameMaxLength keeps
existing sessions and channels, so retained names can push WHO, WHOIS,
WHOWAS, 333, INVITE and channel notifications past 512 bytes. Those
replies were dropped silently by the writer.

Route numerics through Replies.sendChecked, which skips a reply whose
fixed fields cannot fit and logs a throttled WARN naming the reply,
and fit free trailing text (topic, away reason, closers) instead of
dropping the line. The writer applies the same check to broadcasts.
Identities are never truncated, and commands still send their closers.

Fixes #93

Co-Authored-By: GPT-6.1-Sol (Codex) <noreply@openai.com>
Describe, next to the NAMES policy, how hostmask replies and nick/channel
pairs that cannot fit after a limit-lowering reload are fitted or skipped
with a throttled WARN.

Refs #93

Co-Authored-By: GPT-6.1-Sol (Codex) <noreply@openai.com>
@sourcery-ai

sourcery-ai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR extends the 512-byte reply budget policy from configured-name validation to retained over-limit identities, using a shared representability guard that skips indivisible hostmask replies with throttled warnings, fits only free trailing text, applies to broadcast notifications, and preserves command termination; documentation and unit/integration coverage validate the behavior.

Sequence diagram for the shared IRC reply budget guard

sequenceDiagram
    participant Handler as CommandHandler
    participant Replies
    participant Serializer as MessageSerializer
    participant Writer as SessionWriter
    participant Log

    Handler->>Replies: sendChecked(session, message)
    Replies->>Serializer: commandBytes(message)
    alt fixed fields fit within 512 bytes
        Replies->>Writer: enqueueRaw(message)
    else fixed fields exceed command budget
        Replies->>Log: warn oversized reply
        Replies-->>Handler: skip reply
    end

    Handler->>Replies: sendFitted(session, serverName, numeric, params, text)
    Replies->>Replies: truncate(text, budget, false)
    Replies->>Replies: sendChecked(session, fittedMessage)

    Writer->>Serializer: serializeText(message)
    alt hostmask notification is oversized
        Writer->>Replies: isRepresentable(message)
        Replies->>Log: warn oversized reply
        Replies-->>Writer: false
    else PRIVMSG or NOTICE text already fits
        Writer-->>Writer: drop oversized message
    end
Loading

Flow diagram for fitting or skipping retained over-limit replies

flowchart TD
    Start[Build reply] --> Fixed{Fixed fields fit in 512 bytes?}
    Fixed -->|No| Warn[Emit throttled WARN and skip reply]
    Fixed -->|Yes| Free{Reply has free trailing text?}
    Free -->|Yes| Fit[Fit text on a code-point boundary]
    Free -->|No| Send[Send without truncating identities]
    Fit --> Check[sendChecked]
    Check --> Send
    Send --> Continue[Continue command and attempt terminating numeric]
Loading

File-Level Changes

Change Details Files
Centralize representability checks for indivisible replies and hostmask-bearing notifications.
  • Add byte-budget validation against the serialized command section without truncating identities.
  • Throttle WARN logs by reply command across recipients and report suppressed counts.
  • Route INVITE, numeric replies, and SessionWriter broadcast fallbacks through the shared guard.
  • Preserve existing text fitting for PRIVMSG/NOTICE and continue command terminators when preceding replies are skipped.
jircd-core/src/main/java/net/jircd/core/session/command/Replies.java
jircd-core/src/main/java/net/jircd/core/session/SessionWriter.java
jircd-core/src/main/java/net/jircd/core/session/command/InviteCommandHandler.java
Fit free trailing text while protecting fixed fields in hostmask-related command responses.
  • Fit topic text for TOPIC and JOIN on Unicode code-point boundaries.
  • Fit away reasons and WHOIS, WHOWAS, and WHO terminator text.
  • Apply the policy to WHOIS server/operator/closer lines and the WHOWAS closer.
jircd-core/src/main/java/net/jircd/core/session/command/JoinCommandHandler.java
jircd-core/src/main/java/net/jircd/core/session/command/TopicCommandHandler.java
jircd-core/src/main/java/net/jircd/core/session/command/WhoCommandHandler.java
jircd-core/src/main/java/net/jircd/core/session/command/WhoisCommandHandler.java
jircd-core/src/main/java/net/jircd/core/session/command/WhowasCommandHandler.java
Document and verify behavior for retained over-limit names after configuration reloads.
  • Document skip-versus-fit behavior, warning throttling, affected numerics and notifications, and command continuation.
  • Add unit coverage for oversized identities, topic metadata, INVITE, broadcasts, fitting, and warning behavior.
  • Add integration coverage for reload-retained nicknames/channels and wire-size guarantees.
  • Update NAMES expectations to attribute closer drops to the shared reply guard.
specs/001-ircv3-server/contracts/server-configuration.md
jircd-core/src/test/java/net/jircd/core/session/command/GrandfatheredReplyTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/ReplyBudgetReloadTest.java
jircd-integration-tests/src/test/java/net/jircd/integration/NamesByteBudgetTest.java

Assessment against linked issues

Issue Objective Addressed Explanation
#93 Prevent hostmask-carrying numeric replies and notifications containing grandfathered over-limit nicknames or channel names from being silently dropped when they exceed the IRC byte budget. ✅
#93 When fixed identity fields cannot fit, skip the reply with a specific, throttled warning; fit free trailing text where possible, and let command processing continue to attempt terminating replies. ✅
#93 Document the extended output policy and test reload behavior with retained over-limit names, verifying replies fit or are skipped with warnings. ✅

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 4 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/command/Replies.java" line_range="55" />
<code_context>
     params.addAll(List.of(trailingParams));
     Message message = new Message(Map.of(), serverName, null, numeric.wireCode(), params);
-    if (session.writer() != null) {
+    sendChecked(session, message);
+  }
+
</code_context>
<issue_to_address>
**Free reply text is dropped**

When an unknown WHOIS target or a direct message with a long away reason makes the free trailing text exceed the remaining 512-byte budget while the fixed fields still fit, `Replies.send` routes these replies through `sendChecked` without fitting the trailing text, so over-budget RPL_ENDOFWHOIS and RPL_AWAY replies are dropped; WHOIS lacks its 318 close and the sender gets no away notification.

Fit the free trailing text in `Replies.send` before checking and sending these replies.

Also at `jircd-core/src/main/java/net/jircd/core/session/command/WhoisCommandHandler.java:125`.
</issue_to_address>

### Comment 2
<location path="jircd-core/src/main/java/net/jircd/core/session/SessionWriter.java" line_range="254" />
<code_context>
     } catch (MessageSerializer.MessageTooLongException e) {
+      // Report unrepresentable identities and nick/channel pairs through the same throttled
+      // policy as numeric replies. PRIVMSG/NOTICE have already attempted text fitting above.
+      if (!Replies.isRepresentable(message)) {
+        return;
+      }
</code_context>
<issue_to_address>
**Text-fit failures get misreported**

When an oversized PRIVMSG or NOTICE has fixed fields that leave less than one body code point’s bytes, `serializeText` can throw when an oversized PRIVMSG or NOTICE has no room for a complete body code point. `Replies.isRepresentable` then measures the original body and logs that a reply’s fixed fields exceed the budget, so operators receive a misleading diagnosis for a dropped message.

Separate fixed-field representability from text-fit failure, and log the latter with a text-specific reason.
</issue_to_address>

### Comment 3
<location path="jircd-core/src/main/java/net/jircd/core/session/SessionWriter.java" line_range="34" />
<code_context>
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 import net.jircd.core.config.ServerConfiguration;
+import net.jircd.core.session.command.Replies;
 import net.jircd.protocol.CapabilityName;
 import net.jircd.protocol.Command;
</code_context>
<issue_to_address>
**Writer depends on command policy**

`SessionWriter` now calls the command-layer `Replies` utility for wire-size decisions, while reply construction already depends on session types. This creates a session-to-command package dependency cycle, so reusing or extracting the writer requires pulling in command policy as well.

Move the shared representability and warning policy to a wire/session-level component that both the writer and reply builders can use.

Also at `jircd-core/src/main/java/net/jircd/core/session/SessionWriter.java:254`.
</issue_to_address>

### Comment 4
<location path="jircd-core/src/test/java/net/jircd/core/session/command/GrandfatheredReplyTest.java" line_range="148" />
<code_context>
+            : new JoinCommandHandler(channels, extensions, () -> SERVER, () -> 351);
+    try (Warnings warnings = new Warnings()) {
+      var replies = HandlerHarness.run(handler, session("requester"), command + " " + name);
+      assertThat(HandlerHarness.codes(replies)).contains("332").doesNotContain("333");
+      warnings.assertReply("333");
+    }
</code_context>
<issue_to_address>
**Fitted reply text can disappear**

When `Replies.sendFitted` regresses to dropping all free trailing text, if `sendFitted` emits an empty trailing field instead of fitting the supplied text, `topicMetadataIsSkippedWhileTopicTextIsFitted` and `whoisSkipsImpossibleIdentitiesButFitsOtherNickLinesAndCloses` still pass: they assert reply codes, while `HandlerHarness.run` checks only the line-length limit. The tests therefore accept missing topic text and away reason.

Assert that the 332 topic text and 301 away reason contain the expected text, including a non-empty fitted prefix.

Also at `jircd-core/src/test/java/net/jircd/core/session/command/GrandfatheredReplyTest.java:90`.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: jircd-core/src/main/java/net/jircd/core/session/command/Replies.java:55, jircd-core/src/test/java/net/jircd/core/session/command/GrandfatheredReplyTest.java:148


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/SessionWriter.java Outdated
Comment thread jircd-core/src/main/java/net/jircd/core/session/SessionWriter.java Outdated
codegeek and others added 2 commits October 22, 2026 10:04
… package

Review follow-ups for #93:

- Replies whose last parameter is free text now cut it to fit instead
  of being dropped, so e.g. WHOIS on a long unknown target still sends
  its 318 and an away user's 301 reaches the sender.
- OutboundSizeGuard (with WarningThrottle) moves the representability
  check into the session package, so SessionWriter no longer depends on
  the command layer.
- A PRIVMSG/NOTICE with no room for one body code point is logged with a
  text-specific reason instead of a fixed-field diagnosis.
- Tests assert the fitted topic and away text, and check list
  emptiness before element predicates.

Refs #93

Co-Authored-By: GPT-6.1-Sol (Codex) <noreply@openai.com>
The limit applies to the prefix, command, parameters and CRLF; IRCv3
message tags have their own 8191-byte section and never consume it.
Define the term once and use it throughout the reply-budget docs.

Refs #93

Co-Authored-By: GPT-6.1-Sol (Codex) <noreply@openai.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.

Use a COMMAND_NAME constant for "TAGMSG" (S1192), pass the system zone
to ZonedDateTime.now explicitly (S8688), and assert on the received
closer text rather than a literal (S3415).

Refs #93

Co-Authored-By: GPT-6.1-Sol (Codex) <noreply@openai.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 5e6613b into main Oct 22, 2026
6 checks passed
@codegeek
codegeek deleted the fix/issue-93-grandfathered-hostmask-replies branch October 22, 2026 16:52
codegeek added a commit that referenced this pull request Oct 22, 2026
* Add pre-PR checks to cut SonarCloud and Sourcery review rounds

Most PRs have needed several push-and-review rounds for findings that
could have been caught before the PR existed. Add a "Before opening a
PR" section to AGENTS.md and a review checklist built from the Sonar
issues and Sourcery threads on every merged PR (#28-#94): the Sonar
rules this repo keeps hitting and the kinds of defects Sourcery keeps
finding.

The SonarQube CLI's code analysis returns 403 on this plan, so the
local Sonar step uses the Gradle scanner's branch analysis instead.
Ignore the .sonar/ cache it leaves behind.

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

* Harden the local Sonar step and correct two checklist items

Review follow-ups:

- Run the token-bearing scan only on trusted worktree branches, since
  the build executes the branch's Gradle logic with the token present.
- Take the branch name from `git branch --show-current`, quoted, instead
  of a hand-substituted placeholder.
- Wait for SonarCloud to process the analysis (sonar.qualitygate.wait)
  so the issue query doesn't read the previous one, and page the query.
- Say when SonarCloud doesn't run (no SONAR_TOKEN, fork PRs).
- A missing bind address binds every interface where the contract says
  so, as ServerConfiguration.Listener documents; the checklist had it
  backwards.
- Public API breaks follow the constitution's migration-path rule.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

[Bug] Extend the grandfathered-state output policy to hostmask replies

1 participant