Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
2a43735
docs: Organize Claude reports & add bug report
cypress-exe Jul 12, 2026
1c5a1a2
Fix L1: message-edit handler no longer deletes the DB record it just …
cypress-exe Jul 12, 2026
21045a9
Fix B4: to_embed() no longer mutates the stored EmbedProperty color i…
cypress-exe Jul 12, 2026
81e40bf
Fix B1/B3: match birthday runtime against the scheduler cycle window,…
cypress-exe Jul 12, 2026
34f2851
Fix B2: CPU throttling delays guild processing instead of skipping th…
cypress-exe Jul 12, 2026
c75dcd0
Fix U1: resolve guild owner via owner_id instead of the member-cache-…
cypress-exe Jul 12, 2026
676195c
Fix U2: embed placeholder/channel replacements now reach fields and f…
cypress-exe Jul 12, 2026
980007b
Fix L4: entry_is_fresh compares a real time delta instead of calendar…
cypress-exe Jul 12, 2026
05080fa
Fix L2: member-update logging filters audit entries by action and val…
cypress-exe Jul 12, 2026
1777692
Fix L3: member-removal logging only matches kick/ban audit entries ta…
cypress-exe Jul 12, 2026
339da36
Fix L5: edit log sends raw user content with AllowedMentions.none()
cypress-exe Jul 12, 2026
b6ca5b3
Fix S1: await send_error_message_to_server_owner in default-roles For…
cypress-exe Jul 12, 2026
64dd35c
Fix S2: DM clear-last no longer calls the nonexistent DMChannel.purge()
cypress-exe Jul 12, 2026
c04331d
Fix S4: role-message selects skip roles deleted from the guild
cypress-exe Jul 12, 2026
4dcaa31
Fix M1: nickname-profanity self-edit detection uses a workable freshn…
cypress-exe Jul 12, 2026
783a518
Fix L6: don't force-chunk the whole guild on the first member update
cypress-exe Jul 12, 2026
61a551e
Fix O2/O3: options-menu ban button — no per-open REST calls, no User.…
cypress-exe Jul 12, 2026
c939672
Harden B7: typed_property getter tolerates malformed/legacy stored va…
cypress-exe Jul 12, 2026
b7fac99
Fix B11: batch member resolution in daily maintenance
cypress-exe Jul 12, 2026
9e10cbc
Fix B13/B14: daily DB maintenance off the event loop; guard orphan cl…
cypress-exe Jul 12, 2026
6ae7ea4
Fix C1/C4: config singletons + no whole-file deep copies on read paths
cypress-exe Jul 12, 2026
f2b06dd
Fix B6: warn the owner when the configured birthday channel can't be …
cypress-exe Jul 12, 2026
bef473b
Fix B10: midnight log rotation preserves the configured log level
cypress-exe Jul 12, 2026
105083e
Fix L7: delete log tolerates unresolvable audit user/target instead o…
cypress-exe Jul 12, 2026
5dea298
Fix L10/L12: consistent aware message timestamps; delete log survives…
cypress-exe Jul 12, 2026
da2f280
Fix L9: message_checks no longer rejects DB-sourced MessageRecords
cypress-exe Jul 12, 2026
306034d
Fix M3: message edits respect the configured admin-role profanity exe…
cypress-exe Jul 12, 2026
0afa814
Fix M4: strike timestamps are written as aware UTC
cypress-exe Jul 12, 2026
3b83bfc
Fix O5/O6: message editors stop after an error response and guard the…
cypress-exe Jul 12, 2026
10b5217
Fix C2: JSONFile writes are atomic (temp file + os.replace)
cypress-exe Jul 12, 2026
6598150
Fix C5: stop building a new scoped_session registry per query
cypress-exe Jul 12, 2026
35c7e25
Fix C8/C9: IntegratedList single-query iteration, COUNT(*) len, param…
cypress-exe Jul 12, 2026
9c1c0fc
Fix C13: on_shard_ready dedups shard ids
cypress-exe Jul 12, 2026
d6fe507
Fix C15: error handlers copy the shared error embed before setting th…
cypress-exe Jul 12, 2026
e00e151
Fix: chunk_guild_with_feedback chunks regardless of bot load status
cypress-exe Jul 12, 2026
f9cdbe5
Fix P1: permission-denied notice uses followup when the interaction w…
cypress-exe Jul 12, 2026
5b00343
Fix X1: link-only views use the finite default timeout
cypress-exe Jul 12, 2026
e21e3b5
Fix S3: join-to-create VCs HTTPException handling
cypress-exe Jul 12, 2026
b82b359
Fix B9: correct scheduler docstring interval and demote the noisy ski…
cypress-exe Jul 12, 2026
abe1be7
Fix L8: delete-log embed count matches what is actually attached
cypress-exe Jul 12, 2026
23c7e11
Fix L11: drop the redundant cache removal on message edit
cypress-exe Jul 12, 2026
9fc96b3
Fix L13: remove the mmap round-trip copy in file_computation
cypress-exe Jul 12, 2026
8b1477a
Fix L15: action-log embed timestamps are aware UTC
cypress-exe Jul 12, 2026
81cb073
Fix U3: utils.timeout reports Failure Forbidden when the bot lacks Mo…
cypress-exe Jul 12, 2026
3bfd649
Fix U7: owner-DM failure handling no longer swallows everything silently
cypress-exe Jul 12, 2026
8796878
Fix O4: BanButton label check matches the capitalized type the view p…
cypress-exe Jul 12, 2026
3871e1a
Fix O7: color selects compare strings with == instead of identity
cypress-exe Jul 12, 2026
c97f051
Fix C6: build the default-row DELETE with a proper ' AND ' join
cypress-exe Jul 12, 2026
354a206
Fix C7: execute_query re-raises the original exception instead of wra…
cypress-exe Jul 12, 2026
baa4dff
Fix C12: shard-count math uses ceiling division
cypress-exe Jul 12, 2026
27bc78a
Fix C14: LogIfFailure re-raises BaseExceptions like CancelledError
cypress-exe Jul 12, 2026
e18ae8e
Fix C16: rename the /set command function so it doesn't shadow the se…
cypress-exe Jul 12, 2026
03d37f0
Fix C3: stop defining __dict__ as a method
cypress-exe Jul 12, 2026
5f3d01d
Fix X2: ExpiringSet.add purges expired entries
cypress-exe Jul 12, 2026
6f702d9
Fix reaction-role force-chunk: resolve only the reacting member
cypress-exe Jul 12, 2026
7e8466b
Fix C11: cheaper existence check and attribute lookup in Simple_Table…
cypress-exe Jul 12, 2026
ed7daaa
Fix O8: align the OptionsButton.load contract with its implementations
cypress-exe Jul 12, 2026
c543924
docs: add FIX PASS banner to the July 2026 bug hunt report
cypress-exe Jul 12, 2026
5d250ca
Fix circular import between components.utils and components.ui_compon…
cypress-exe Jul 13, 2026
49f8545
Fix O1: OptionsView always tracks the menu message by id
cypress-exe Jul 13, 2026
feeb209
Fix L14: make the persistent log/moderation views safe as post-restar…
cypress-exe Jul 13, 2026
7a6c271
Fix M2: timeout-revoke freshness check used .seconds (wraps at 24h)
cypress-exe Jul 13, 2026
3be87c0
Fix M6/L10 residual: treat legacy naive last_updated rows as UTC on read
cypress-exe Jul 13, 2026
94361a6
Fix B5: celebrate Feb-29 birthdays on Feb 28 in non-leap years
cypress-exe Jul 13, 2026
480b491
Fix B3 residual: window-match all exact-minute gates in the scheduler
cypress-exe Jul 13, 2026
d780b3e
docs: record the July 13 re-review of deliberately-unfixed findings
cypress-exe Jul 13, 2026
68d9c81
docs: document member-cache limitations for logging and generic repla…
cypress-exe Jul 19, 2026
aa57dd7
docs: fresh-eyes bug hunt report (2026-07b), 27 findings pre-verifica…
cypress-exe Jul 19, 2026
a8bf5f7
docs: annotate 2026-07b bug hunt with verification verdicts (26 confi…
cypress-exe Jul 19, 2026
c24aece
docs: Updated bug report to include my notes
cypress-exe Jul 19, 2026
5f3d846
fix(db): pin orphaned-guild cleanup to a single connection (D1)
cypress-exe Jul 19, 2026
337805b
fix(moderation): guard the nickname-profanity DM (M1)
cypress-exe Jul 19, 2026
81df305
fix(dashboard): return the strike list when it exceeds the display li…
cypress-exe Jul 19, 2026
2529184
fix(reaction-roles): give the 10th numbered role its own emoji (R1)
cypress-exe Jul 19, 2026
3c6c435
fix(role-messages): stop packing role IDs into the select option valu…
cypress-exe Jul 19, 2026
dbe0e1e
fix(logging): close rotated-out log handlers explicitly (C1)
cypress-exe Jul 19, 2026
30f5523
fix(jokes): handle unreachable support server and channel on submissi…
cypress-exe Jul 19, 2026
d78fee3
fix(jokes): stop responding twice to the deny interaction (J2)
cypress-exe Jul 19, 2026
d29b011
fix(embeds): survive empty color selection and view/modal timeouts (E1)
cypress-exe Jul 19, 2026
557a2df
fix(main): treat an absent required env var as unset, not a crash (C2)
cypress-exe Jul 19, 2026
4b810f1
perf(moderation): cache compiled profanity patterns per word list (M3)
cypress-exe Jul 19, 2026
8ba2ce4
fix(reaction-roles): check the message before resolving the member (R4)
cypress-exe Jul 19, 2026
c8824d8
fix(dashboard): stop arming birthdays just by viewing the page (B1)
cypress-exe Jul 19, 2026
ad3b580
fix(autobans): keep the entry when the ban fails (X1)
cypress-exe Jul 19, 2026
9447741
fix(dashboard): preselect the member's actual strike count (DASH2)
cypress-exe Jul 19, 2026
8797414
fix(datetime): store UTC-aware timestamps in the dashboard strike edi…
cypress-exe Jul 19, 2026
bb8834d
fix(dashboard): explain the problem instead of crashing when no chann…
cypress-exe Jul 19, 2026
e4f1dd6
fix(dashboard): validate the level-reward "Choose Level" input (DASH5)
cypress-exe Jul 19, 2026
60089e1
fix(leveling): delete zero-point rows on the run that zeroes them (L1)
cypress-exe Jul 19, 2026
6d43cfe
fix(logging): don't offer "Show More" when the log is already at 10 e…
cypress-exe Jul 19, 2026
fbf0140
fix(jokes): report an empty joke list instead of crashing (J3)
cypress-exe Jul 19, 2026
2a1446f
fix(reaction-roles): parse custom role mentions independent of spacin…
cypress-exe Jul 19, 2026
b332acc
fix(dm-commands): reference the real slash command names (X2)
cypress-exe Jul 19, 2026
68f14f8
fix(bot): don't repeat one-time startup work when on_ready re-fires (C3)
cypress-exe Jul 19, 2026
1beb69f
fix(utils): make get_member tolerate string IDs; pass an int from the…
cypress-exe Jul 19, 2026
72fbb06
feat: prod release scripts
cypress-exe Jul 19, 2026
9c94b5d
docs: bug report review #3
cypress-exe Jul 19, 2026
a733e5c
fix(autobans): suppress join processing when an autoban ban fails (FR1)
cypress-exe Jul 19, 2026
f5cf3db
fix(moderation): escape filtered words before building regex patterns…
cypress-exe Jul 19, 2026
a47fdbc
perf(moderation): cache the default profane word list (FR3)
cypress-exe Jul 19, 2026
98bab38
perf(db): run orphaned-guild cleanup off the event loop (FR4)
cypress-exe Jul 19, 2026
337619e
perf(dashboard): use fetch_ban for the autoban already-banned check (…
cypress-exe Jul 19, 2026
89d69b9
fix(reaction-roles): keep emoji and role lists aligned in the Custom …
cypress-exe Jul 19, 2026
2636e0a
fix(ui): give SelectView a built-in empty-options state (FR7)
cypress-exe Jul 19, 2026
5a638f5
refactor(role-messages): share one select view between the two button…
cypress-exe Jul 19, 2026
3c74e3a
refactor: one shared role-mention parser (FR9)
cypress-exe Jul 19, 2026
50a11a7
refactor(logging): derive the embed-count limits from the constant (F…
cypress-exe Jul 19, 2026
6b7ab38
fix(channels): resolve channels via utils.get_channel instead of guil…
cypress-exe Jul 20, 2026
e98f515
fix(dashboard): correct admin-roles help docs link
cypress-exe Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions backup_prod_db.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash
# AGENT INSTRUCTIONS: If editing the file, ensure that your model name is included here.
# Generated by Anthropic Claude Sonnet 5 via Claude Code (CLI).
# Makes a consistent point-in-time copy of generated/files/prod.db into
# generated/backups/, using SQLite's online backup API so it's safe to run
# while the bot container is up and writing to the DB.

set -e

DB_PATH="./generated/files/prod.db"
BACKUP_DIR="./generated/backups"

if [ ! -f "$DB_PATH" ]; then
echo "ERROR: $DB_PATH not found. Run this from the repo root." >&2
exit 1
fi

mkdir -p "$BACKUP_DIR"

timestamp=$(date -u +"%Y%m%dT%H%M%SZ")
backup_path="${BACKUP_DIR}/prod_${timestamp}.db"

if command -v sqlite3 >/dev/null 2>&1; then
# The SQLite backup API (.backup) takes a consistent snapshot even while
# another process (the running container) holds the DB open, so the
# container does NOT need to be stopped for this.
sqlite3 "$DB_PATH" ".backup '${backup_path}'"
else
echo "WARNING: sqlite3 CLI not found on this host — falling back to a raw file copy."
echo "This is only safe if the container is stopped (no concurrent writes)."
read -p "Container stopped? Continue with raw copy? [y/N] " confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
echo "Aborted."
exit 1
fi
cp "$DB_PATH" "$backup_path"
fi

if [ ! -s "$backup_path" ]; then
echo "ERROR: Backup file is missing or empty: $backup_path" >&2
exit 1
fi

echo "Backup written to: $backup_path ($(du -h "$backup_path" | cut -f1))"
400 changes: 400 additions & 0 deletions documentation/claude-reports/bug_hunt_2026-07b.md

Large diffs are not rendered by default.

659 changes: 659 additions & 0 deletions documentation/claude-reports/bug_hunt_2026_07.md

Large diffs are not rendered by default.

215 changes: 215 additions & 0 deletions documentation/claude-reports/fix_review_2026-07.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
<!--
AGENT INSTRUCTIONS: If editing the file, ensure that your model name is included here.
Bugs found by Anthropic Claude Fable 5 via Claude Code (CLI).
Generated by Anthropic Claude Sonnet 5 via Claude Code (CLI).
Description: Review of the 25 fix commits applied on fix/bug-fixes for the confirmed
findings from bug_hunt_2026_07.md and bug_hunt_2026-07b.md. Eight parallel review
angles were run against the diff to find regressions the fixes themselves introduced,
plus adjacent issues the diff touched, moved, or sits near. The 10 bug-shaped
candidates were each independently re-checked by bug-verifier subagents. Documentation
only — no code was changed as part of this pass (findings are pending a follow-up fix
pass).
-->

# InfiniBot Fix-Commit Review — July 2026

**Branch:** `fix/bug-fixes`
**Range reviewed:** `ca60550^..6421c2a` (25 commits, one per bug-hunt finding ID:
D1, M1, H1, R1, R2, C1, J1, J2, E1, C2, M3, R4, B1, X1, DASH2, DASH3, DASH4, DASH5,
L1, A1, J3, R3, X2, C3, X3).
**Method:** Eight parallel review angles (correctness of each fix against its stated
bug, regressions the fix could introduce, code the fix moved/touched without fixing,
adjacent logic sharing the same file/function, cleanup opportunities, and a fresh
top-to-bottom diff read) produced ~19 candidates. The 10 bug-shaped candidates were
each independently re-verified by a `bug-verifier` subagent (static tracing against
the repo). Documentation only — **no code was changed.**

**Reported symptoms:** none — this was a review of the fix commits themselves, not a
symptom-driven investigation.

## Verification status

All 10 bug-shaped candidates were independently re-checked by bug-verifier subagents
(static tracing only). Result: **6 CONFIRMED, 4 refuted** (folded into "Investigated
and refuted" below — no separate false-positive IDs assigned since these were
candidates from this review pass, not carried-over findings).

## Severity legend

- **CRITICAL** — data loss / security / breaks the bot broadly
- **HIGH** — breaks a common user-facing path
- **MEDIUM** — edge case with real user impact, or perf that compounds at scale
- **LOW** — cosmetic, latent, or cleanup

---

## The one real regression

### FR1 — Autoban failure now welcomes the member it meant to ban
**[VERIFIED: CONFIRMED]**
**src/features/autobans.py:36**, **src/core/bot.py:692** (introduced by `bf9d0b7`, fix for X1)

The X1 fix correctly stopped deleting the autoban entry when `member.ban()` fails
(`Forbidden`), but it also changed `check_and_run_autoban_for_member`'s return value
to `False` in that case (autobans.py:36-38). `on_member_join` uses that single
boolean to mean "not an autoban target":

```python
if await autobans.check_and_run_autoban_for_member(member):
return
# falls through to trigger_join_message + add_roles_for_new_member
```

So when the ban fails — most commonly because the bot lacks Ban Members permission —
the join falls through to `trigger_join_message` and `add_roles_for_new_member`:
**publicly welcoming and auto-roling the exact person an admin flagged for an
autoban.** The pre-X1 code suppressed normal join processing even on a failed ban
(it just also incorrectly deleted the entry). The fix collapsed two different
meanings — "was this member banned" and "should join processing be suppressed" —
into one boolean, and picked the wrong one to preserve.

**Fix direction:** separate the two meanings. Either return a tri-state (banned /
retry-pending / not-a-target), or simply return `True` whenever the member matched
an autoban entry, regardless of whether the ban call itself succeeded.

---

## Confirmed, but pre-existing (the diff touched or relocated them)

### FR2 — Filtered words aren't `re.escape`d
**[VERIFIED: CONFIRMED — pre-existing, moved by `bc9c5a0` (M3)]**
**src/features/moderation.py:290** (`_generate_profanity_regex_pattern`)

An admin adding a filtered word like `(` or `[` through the dashboard — which
validates only length and duplicates, not regex-safety — makes the pattern
`re.compile` call at `_get_compiled_profanity_patterns` (moderation.py:319) raise.
`functools.lru_cache` does not cache exceptions, so the same invalid word
**re-raises on every message** in that guild, not just once. `LogIfFailure` around
the moderation call swallows the exception, so profanity moderation is **silently
disabled for that guild** until an admin removes the bad word — with no error
surfaced anywhere. M3 relocated this code from an uncached per-message call into the
cached helper but did not introduce the missing `re.escape`.
**Fix direction:** `re.escape` the literal portions of `word` before wildcard
substitution (or catch and skip unparseable words with a logged warning at add-time).

### FR3 — The M3 cache doesn't remove the dominant per-message cost
**[VERIFIED: CONFIRMED]**
**src/features/moderation.py:353** (`str_is_profane`), **`Server.filtered_words`**

For default-word-list guilds — the common case, since the storage optimization
stores `[]` for "using the default list" — every moderated message still
constructs a fresh `Server` object, whose `filtered_words` getter calls
`read_txt_to_list("default_profane_words.txt")`: an **uncached disk read + parse**,
executed before the M3 pattern cache is even consulted. The regex-compile cost M3
targeted is real but secondary to this read for the majority of guilds.
**Fix direction:** cache the parsed default word list (module-level, read once) so
the M3 pattern cache actually sees a stable, hashable key on the hot path.

### FR4 — Orphaned-guild cleanup blocks the event loop
**[VERIFIED: CONFIRMED]**
**src/core/db_manager.py:1172** (`daily_database_maintenance`)

Its siblings in the same function — `get_database().checkpoint` (line 1136),
`optimize_database(throttle=True)` (line 1145), `stored_messages.cleanup_db` (line
1156) — are all offloaded via `asyncio.to_thread`, with a comment explaining exactly
why ("blocking phases run in a worker thread... would otherwise freeze the bot").
`cleanup_orphaned_guild_entries(...)` at line 1172 is called directly on the event
loop. It used to *have* to run synchronously (shared pooled connection + a SQLite
TEMP table, per the D1 fix), but D1's `pinned_connection` change removed that
constraint — the old justifying comment for keeping it off-thread was deleted along
with the bug, and nothing was moved back onto `to_thread` to match its siblings.
**Fix direction:** wrap the line-1172 call in `await asyncio.to_thread(...)` like the
other three phases.

### FR5 — Autoban "already banned?" check paginates the entire ban list
**[VERIFIED: CONFIRMED]**
**src/features/dashboard.py:4729** (autoban-add flow)

The inline comment at dashboard.py:4725-4726 claims "nextcord doesn't provide a
direct method to check if a user is banned through an ID" and falls back to
`async for ban_entry in interaction.guild.bans(): ...` — paginating the *entire*
ban list on every autoban addition. This is incorrect: `await
guild.fetch_ban(nextcord.Object(id=user_id))` is a single REST call that raises
`NotFound` when the user isn't banned, and it's already used elsewhere in this exact
codebase (`src/features/options_menu/ban_button.py:70`). On a server with thousands
of bans, adding one autoban entry costs seconds of paginated REST calls instead of
one.
**Fix direction:** replace the `async for` loop with a
`try: await guild.fetch_ban(...) / except nextcord.NotFound: pass` pair.

### FR6 — Reaction-role emoji/role misalignment on role deletion
**[VERIFIED: CONFIRMED, narrow window]**
**src/features/reaction_roles.py:263** (`create_reaction_role`, Custom flow)

In the Custom flow, `roles` is built by filtering `new_roles_str` against
`interaction.guild.roles` — so a role deleted between validation and creation is
silently dropped from the list. The parallel `emojis` list (line 265) is built
independently from the raw `roles_str` and is **not** filtered, so if any role in
the middle of the list was deleted, the two lists desync and emoji[i] no longer
corresponds to roles[i] for every entry after the gap. This is reachable because an
earlier validation pass runs, then the flow waits on a modal for up to 5 minutes
(plenty of time for a role to be deleted), after which `create_reaction_role`
re-parses the original raw string rather than re-validating against current guild
state. Pre-existing indexing logic in `create_reaction_role`; R3 only changed the
mention-parsing regex used to build `new_roles_str`, not this filtering asymmetry.
**Fix direction:** filter `roles` and `emojis` together (zip, filter, unzip) instead
of filtering `roles` alone.

---

## Cleanup opportunities (verified real, lower stakes)

- **FR7.** `SelectView` raises `ValueError` on an empty options list
(`src/components/ui_components.py:332`). DASH4 added proper empty-state handling
at 2 call sites (the channel-select pages), but roughly 5 other `SelectView` call
sites across the dashboard still carry their own ad-hoc guards against the same
crash. The empty-state handling belongs inside the shared `SelectView` component
so new pages can't regress it.
- **FR8.** The R2 fix (using the field index as the select option value, plus an
`option_roles` map to resolve it) was pasted identically into both
`RoleMessageButton_Single` and `RoleMessageButton_Multiple`
(`src/features/role_messages.py:25` and `:122`). The two classes are near-clones;
any future encoding fix to this mechanism has to be made twice, and it's easy to
update one and forget the other.
- **FR9.** `extract_role_id` (`src/features/reaction_roles.py:12`) is roughly the
5th independent implementation of `<@&id>` mention parsing in the codebase.
Worth extracting to one shared utility so fixes like R3 (spacing-independent
parsing) don't need to be re-discovered per copy.
- **FR10.** `src/features/action_logging.py:467` and `:489` still use bare literals
(`> 9`, `[0:8]`, `>= 10`) for the embed-count limit right next to the new
`MAX_EMBEDS_PER_MESSAGE` constant A1 introduced. If the 10-embed limit logic ever
changes, these will silently drift out of sync with the constant.

Minor items noted but cut for being below the 10-finding review cap: the DM-send
try/except-Forbidden boilerplate repeated at roughly 7 call sites, a duplicated
`go_back` closure (`dashboard.py:656` and `:1860`), `raise Exception` used as a
goto-style control-flow mechanism in `reaction_roles.py:165` and `:175`, and the
profanity `functools.lru_cache(maxsize=256)` sizing (untuned, no data behind the
number).

---

## Investigated and refuted (no action needed)

- **`start_scheduler()` re-running on `on_ready` re-fires (C3 concern).** Run times
are grid-aligned to wall clock (e.g. `:00/:15/:30/:45`), so a reconnect that
re-triggers scheduler startup converges on the same boundary as the original
schedule rather than drifting or duplicating runs. No starvation or duplicate-run
risk.
- **The R4 reorder (message fetched before member).** Net *reduction* in REST calls
versus the prior code, which fetched both unconditionally regardless of whether
the early-exit conditions were met. Not a regression.
- **`get_channel`/`get_message` lacking the X3 string-ID coercion.** Every call site
was traced; all already pass native `int`s. `utils.get_member`'s new tolerance for
string IDs (X3) is a hardening improvement for future callers, not a fix for a
currently-reachable bug elsewhere.
- **`asci_to_emoji` mishandling counts of "11" or more.** Option counts are
structurally capped at 10 in two independent places (the select-menu builder and
the Custom/Numbers type validation), so the theoretical double-digit-overflow path
is unreachable.

---

*Generated by Anthropic Claude Fable 5 and documented by Sonnet 5 via Claude Code (CLI).
Documentation-only pass — no source files were modified. Line references are against
`fix/bug-fixes` at `6421c2a`; verify against current tree before fixing.*
17 changes: 12 additions & 5 deletions github-pages-site/docs/core-features/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ InfiniBot logs the following activities, categorized into three main types:

### Member Events

| Event Type | Description |
|--------------------|--------------------------------------|
| Nickname Changes | Tracks changes to member nicknames. |
| Role Changes | Logs updates to member roles. |
| Event Type | Description |
|--------------------|---------------------------------------------------|
| Nickname Changes | Tracks changes to member nicknames. (best-effort — see below) |
| Role Changes | Logs updates to member roles. (best-effort — see below) |

### Moderation Events

| Event Type | Description |
|--------------------|--------------------------------------|
| Member Timeouts | Logs when a member is timed out. |
| Member Timeouts | Logs when a member is timed out. (best-effort — see below) |
| Member Bans | Tracks bans and unbans of members. |
| Member Kicks | Logs when a member is kicked. |

Expand Down Expand Up @@ -93,6 +93,13 @@ Removing message data from InfiniBot's database does NOT delete the message from

For more information on InfiniBot's data handling and retention policies, please refer to our [Privacy Policy]({% link docs/legal/privacy-policy.md %}).

## Member Event Limitations

Nickname changes, role changes, and timeouts can only be logged for members InfiniBot currently has cached. InfiniBot does not proactively load the full member lists, so a member who hasn't recently interacted with the server may not be cached, and a change to that member will silently go unlogged.

After a member interacts with the server or updates their information (nickname, roles, etc.), they will be cached by InfiniBot, and subsequent changes to that member can be logged.


## Tips for Effective Logging

- Set up a dedicated logging channel separate from your main admin channel and disable notifications for this channel to minimize distractions.
Expand Down
14 changes: 11 additions & 3 deletions github-pages-site/docs/messaging/generic-replacements.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ Generic replacements can be used in embeds, join/leave messages, role messages,
- `@username` - Shows the member's username
- `@displayname` - Shows the member's display name
- `@id` - Shows the member's Discord ID
- `@joindate` - Shows when the member joined the server
- `@accountage` - Shows how old the member's Discord account is
- `@joindate` - Shows when the member joined the server (best-effort; see below)
- `@accountage` - Shows how old the member's Discord account is (best-effort; see below)

## Server Replacements

- `@server` - Shows the server name
- `@serverid` - Shows the server's Discord ID
- `@membercount` - Shows the total number of members in the server
- `@owner` - Shows the server owner's display name
- `@owner` - Shows the server owner's display name (best-effort; see below)

## Time and Date Replacements

Expand All @@ -55,8 +55,16 @@ This would appear as:
Welcome @discord-user to Awesome Server! Please check out #rules to get started!
```

## Best-Effort Replacements

{: .warning }
A few replacements depend on information Discord may not have handed InfiniBot at the moment the message is sent. When that information isn't available, the placeholder is replaced with the literal text **`Unknown`** rather than being left in place.

This behavior is a deliberate trade-off: InfiniBot avoids requesting full member lists for large servers, since doing so is slow and rate-limited by Discord. In practice these replacements resolve correctly the majority of the time in smaller and moderately sized servers, where the entire member list is cached. For large servers, these fields frequently can't be resolved. See also the [Logging]({% link docs/core-features/logging.md %}) page, which has the same underlying limitation.

## Usage Notes

- These replacements work in most text-based messages sent by InfiniBot
- Replacements are case-sensitive
- The user replacements (`@mention`, `@username`, `@displayname`, `@id`, `@joindate`, `@accountage`) only apply where a specific user is associated with the message. In contexts with no such user (for example, a role message or reaction role embed) these placeholders are left untouched and will appear literally in the message
- If a replacement can't be processed, it will remain as plain text or appear as "Unknown"
30 changes: 30 additions & 0 deletions release_prod.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash
# AGENT INSTRUCTIONS: If editing the file, ensure that your model name is included here.
# Generated by Anthropic Claude Sonnet 5 via Claude Code (CLI).
# Prod release playbook: back up the DB, tag the current image as a rollback
# point, then rebuild and restart the container. Pass --no-cache to force a
# clean Docker build (forwarded to build.bash).
#
# Rollback afterwards: ./rollback_prod.bash

set -e

echo "==> Step 1/3: Backing up the database..."
bash ./backup_prod_db.bash

echo ""
echo "==> Step 2/3: Tagging current image as rollback point..."
if docker image inspect infinibot:latest >/dev/null 2>&1; then
docker tag infinibot:latest infinibot:rollback
echo "Tagged infinibot:latest -> infinibot:rollback"
else
echo "No existing infinibot:latest image found — nothing to tag (first deploy?)."
fi

echo ""
echo "==> Step 3/3: Rebuilding and restarting the container..."
bash ./rebuild_and_run.bash -d "$@"

echo ""
echo "Release complete."
echo "If something's wrong, roll back in under a minute with: ./rollback_prod.bash"
Loading