• v5.4.0 722e49b7c7

    v5.4.0
    All checks were successful
    CI / build-and-test (push) Successful in 2m33s
    CI / docker-build (push) Successful in 29s
    Stable

    joeyr released this 2026-10-11 16:54:10 -05:00 | 0 commits to main since this release

    The debug:True footer names the model and settings that answered, and
    Make Public posts it with the answer.

    Added

    • The footer ends with the model, effort and thinking setting:
      -# Model: claude-sonnet-5-5 · effort high · thinking on. The model is
      the one the API says answered. Effort and thinking are what the request
      sent. Thinking is adaptive, so "on" means the model was free to think, not
      that it did. Sonnet 5.5 returns its thinking blocks empty and also uses
      them for progress notes between tool calls, so a block in the response
      does not show that it reasoned.

    Changed

    • Make Public posts the debug footer under the answer. The public post
      now matches what the asker saw. Before, the footer was dropped on publish.
      The footer is still kept out of the saved conversation turn and out of the
      shared context that later questions read, so turning debug on changes
      nothing about the next answer.
    Downloads
  • v5.3.7 5ed1ce4b3a

    v5.3.7
    All checks were successful
    CI / build-and-test (push) Successful in 2m12s
    CI / docker-build (push) Successful in 25s
    Stable

    joeyr released this 2026-10-10 13:04:51 -05:00 | 1 commits to main since this release

    Two small fixes found while fixing 5.3.6, and the dependencies brought up to
    date.

    Fixed

    • A fetched page that shows an HTML entity reached Claude with it decoded
      twice.
      web_fetch decoded & first and then the other entities, so
      the & the first step produced started a second one: a page writing about
      HTML that showed &lt;p&gt; reached Claude as the tag <p>. Each entity is
      now decoded once.
    • The migration rollback logged the migrations it undid as fields named
      0, 1, ….
      They now go in one migrations field, as they already did
      when migrating.

    Changed

    • @anthropic-ai/sdk 0.131 → 0.133, @modelcontextprotocol/sdk 1.32.1,
      dotenv 18.0.7, and on the dev side Prettier 3.9.10, typescript-eslint
      8.71.1, @types/node 24.19.2 and the js-yaml override 5.4.3. None of the
      SDK changes touch what the bot uses. TypeScript stays on 6.0: TypeScript 7
      is out, but the newest typescript-eslint (8.71.1) still accepts only
      <6.1.0 and the newest ts-jest (29.4.14) only <7.
    Downloads
  • v5.3.6 770c72a5b5

    v5.3.6
    All checks were successful
    CI / build-and-test (push) Successful in 2m12s
    CI / docker-build (push) Successful in 24s
    Stable

    joeyr released this 2026-10-10 12:48:38 -05:00 | 2 commits to main since this release

    The last 22 findings from the whole-repo review, each minor or rare. LoL
    results survive a restart and a watchlist change, registering checks the
    region and Riot ID first, failed buttons say so, a broken mappings file no
    longer wipes names, and the logs keep their details and never show the
    database password. A setting that did nothing, D2EMU_POLL_INTERVAL_MS, is
    gone.

    Fixed

    • A LoL result still pending when the bot restarted was never posted.
      Riot can take a few minutes to publish a finished match, and the bot keeps
      checking for it in the background. A restart in that window, such as a
      deploy, stopped the checks and nothing started them again, so the post said
      "results unavailable" for good. After a restart the bot now picks up any
      result from the last half hour that never arrived.
    • A restart could drop a LoL game's result over one failed Riot call. On
      startup the bot checks whether each game it was following is still on. One
      timeout or busy answer from Riot at that moment wrote the game off, so if it
      had ended, its result never appeared. The game is now checked again on the
      next poll, as it already was outside a restart.
    • Removing a player from the watchlist mid-game could cost that game its
      result.
      A game is followed through the first watched player found in it.
      If that player was removed while it was on, nobody noticed it end, and it
      was written off hours later with no result. Another watched player in the
      game now takes it over, and the result shown is theirs.
    • A LoL game that came back from a false end could lose its ranks and
      result after the notification channel changed.
      Riot sometimes briefly
      reports a live game as over, and when it reappears the bot edits its
      original post back. If monitor-channel had moved notifications meanwhile,
      the bot then looked for that post in the new channel, so the ranks and the
      result never arrived. It now follows the post in the channel it is in.
    • The cleanup of stuck LoL games always reported none. Its count was
      always 0, so its log line never appeared, and the games it closed had no
      end time. It now reports how many it closed and records when.
    • A LoL player registered under a region they don't play on was accepted,
      and their games were never posted.
      Riot finds an account whatever region
      is typed, so the wrong region was stored and their games were looked for on
      the wrong server. Register and monitor-add now ask Riot where the player
      plays and register them there, and say so when it differs from what was
      typed. If Riot can't be asked, the typed region is used as before.
    • A mistyped LoL region or a name without its #TAG got "try again
      later".
      Neither was checked until Riot was asked, so input that could
      never work failed as if Riot were down, and was logged as an error.
      Register, unregister, monitor-add and monitor-remove now reply at once with
      the valid regions or the Name#TAG format.
    • A connection to Riot that dropped mid-answer was logged as a bug in the
      bot.
      A cut-off response surfaced as a bare TypeError rather than a
      network error. It is now reported, and handled, like any other network
      failure.
    • Every query-chat answer said "(with context: last 1 hours)". The
      default one-hour window was named on every answer, private and public,
      though nobody chose it, and a follow-up reads what was said since the last
      question rather than an hour anyway. The header now names a window only when
      the asker typed one, and says "1 hour", not "1 hours".
    • /aufhocker espnff help said the fantasy server wasn't connected when it
      was.
      When the server was up but none of its tools were open to the person
      asking, help said "not connected" while espnff mcp said connected. Help
      now says the same as espnff mcp, and no longer retries a server that is
      already up.
    • A web page with one bad character code couldn't be read. A numeric
      character code past the last valid one, such as &#x110000;, failed the
      whole fetch, so Claude answered without the page. The code is now left as
      written and the rest of the page comes through.
    • When chat history was too long to send in full, part of a multi-line
      message could lose its author.
      If the cut fell inside a message several
      lines long, its last lines were kept without the name and time, so Claude
      read them as nobody's, or as the next person's. A message is now either
      kept whole or left out.
    • Buttons and menus that hit an error showed only "This interaction
      failed".
      The bot logged the error but never answered the click. The
      clicker now gets a private error message, and the message the button sits
      on is left alone.
    • The role sign-up board couldn't be posted with one role for both games,
      and the bot blamed a permission.
      Each button needs its own role, so
      Discord refused the board, and the bot told the admin to check its Send
      Messages permission. The command now asks for a different role per game up
      front, and a post that fails for any other reason no longer blames
      permissions unless Discord said so.
    • Reloading name mappings from a broken file erased every real name. If
      user-mappings.json had a typo or was missing, /aufadmin reload-mappings
      dropped every mapping, reported success, and the bot fell back to Discord
      usernames everywhere. A file that can't be read now leaves the current names
      in place, and the command says the reload failed.
    • The startup log could show part of the database password. A password
      holding an @ followed by /, ? or # (or digits followed by # or
      ?), pasted into DATABASE_URL unencoded, confused the address parser, and
      pieces of it were logged as the database host, port or name on every boot.
      When the address can't be read with certainty, the line now says
      (unparseable) and shows nothing from it.
    • The startup checks said only "fetch failed" when a service couldn't be
      reached.
      The real reason (address not found, connection refused, a bad
      certificate) was dropped, so DNS, firewall and TLS problems all looked the
      same. The check now names it. Any token or password is removed from the
      line first, including one the error text quoted back, which could already
      happen before.
    • A crash caused by an unusual error could skip the clean shutdown or log
      no reason.
      When code threw something that wasn't a normal error, the crash
      handler either crashed itself, exiting without closing the database or
      Discord, or logged a line with no reason in it. It now logs what was thrown
      and always shuts down properly, and normal errors keep their type and cause.
    • A log line whose text contained a % sign could lose its details,
      including the error.
      The logging library treats %s, %o, %% and the
      like as fill-in markers, and a message with one dropped everything logged
      with it; a user typing 50%off was enough. The details are now always kept,
      and the text is left exactly as written.
    • A number setting written as 10_000, 1e4 or 30s was quietly cut
      short.
      Only the leading digits were read, so CLAUDE_MAX_TOKENS=10_000
      ran as 10 tokens and RATE_LIMIT_INTERVAL_MS=30s as 30 milliseconds, and
      the startup check passed both. Number settings must now be plain digits, and
      anything else stops the bot at startup with an error naming the setting.
      Every shipped config file and the Swarm stack already use plain digits.
    • The connection that runs migrations ignored
      DATABASE_SSL_REJECT_UNAUTHORIZED.
      With DATABASE_SSL=true, the bot's own
      connection verified the database server's certificate, but the one that
      applies schema changes at startup accepted any. Both now use the same SSL
      settings. The live stack runs with DATABASE_SSL=false, so it is unaffected.
    • CI's dependency audit passes again. handlebars 4.7.9 had newly
      published critical advisories. It is dev-only, reached through
      ts-jest, so it was never in the production image, and 4.7.10, within the
      range ts-jest asks for, fixes it.

    Changed

    • D2EMU_POLL_INTERVAL_MS is removed. It looked like it set how often the
      D2 tracker polls, but nothing read it: Terror Zones are checked on the
      5-minute marks and DClone every 10–30 seconds. A stack that still sets it
      gets a startup warning saying it is ignored, so remove it from the stack.
    Downloads
  • v5.3.5 1c9c16ec5b

    v5.3.5
    All checks were successful
    CI / build-and-test (push) Successful in 2m17s
    CI / docker-build (push) Successful in 26s
    Stable

    joeyr released this 2026-10-10 08:58:37 -05:00 | 3 commits to main since this release

    Nine rarer bugs, each needing bad timing or an unusual answer to show up. A
    cut-off answer now says so, Make Public and the fantasy Confirm buttons hold
    up under slow or failed clicks, a LoL game is posted once and keeps its
    result, a long watchlist lists, a stalled Data Dragon no longer holds up
    startup, and a new DClone board shows the latest grid.

    Fixed

    • An answer cut off at its length limit was shown as if complete.
      Thinking counts toward Claude's token limit, so an answer can stop half
      written, or before it starts. A half answer was shown and saved for
      follow-ups, a "let me look that up" before a cut-off tool call passed for
      the answer, and thinking alone failed as an API error. What was written is
      now shown with a note that it was cut off (or a message saying none of it
      was), and it isn't saved for follow-ups.
    • Make Public could post an answer twice. A long post can outlast
      Discord's 3 seconds to answer a click, which shows "This interaction
      failed" beside a button that still works, and a second click posted the
      answer again. A second click while posting now says so. A retry after a
      post failed partway also posted its start a second time; it now picks up
      at the part that failed.
    • A fantasy Confirm message could pass 2,000 characters and stop
      updating.
      Each line was clipped, but the message wasn't: five long
      changes with five long failures under them went over, and Discord refused
      the edit after the change had run, so its outcome never showed. Lines are
      now clipped shorter until the message fits.
    • A Confirm press that Discord didn't take left the change stuck. If
      answering the press failed, the change stayed "running" though nothing had
      been sent, and every later press said it had already been handled. It now
      goes back to waiting, so pressing again runs it.
    • Two watched players in one LoL game could get it posted twice. Their
      detections, a moment apart, both found no row for the game, and the step
      meant to reserve it succeeded for both. The game is now claimed with an
      insert that only one detection can win.
    • A game that ended while its ranked data was loading lost its result.
      Ranks and recent form can take minutes behind the Riot rate limit, and a
      game that ended meanwhile (a remake ends at the next poll) had its result
      replaced by the in-progress embed. The late edits now check that the game
      is still live first.
    • /aufhocker lol monitor-list failed past about 30 summoners. The whole
      list went in one message, and Discord refuses one over 2,000 characters.
      It now continues in follow-up messages.
    • A stalled Data Dragon could hold up startup for minutes. Its two
      requests had no timeout, and every plugin after the LoL one, and the health
      endpoint, waited on them. Each now has 10 seconds, and champion names fall
      back to IDs, as they already did when Data Dragon was down.
    • A new DClone status message could show an old grid for hours. A poll
      that landed while /aufadmin d2 dclone-board was posting couldn't see the
      new message to update it, and later polls with the same data skipped it.
      It is now brought up to date as soon as it is recorded.
    Downloads
  • v5.3.4 dcfaf6a52b

    v5.3.4
    All checks were successful
    CI / build-and-test (push) Successful in 2m38s
    CI / docker-build (push) Successful in 32s
    Stable

    joeyr released this 2026-10-10 00:34:15 -05:00 | 4 commits to main since this release

    Long answers get time to finish, a deploy no longer fails a Confirm click
    partway through, a stale DISCORD_GUILD_ID no longer keeps the bot down, and
    a DClone spawn reported as 6/6 stays up for 12 hours.

    Fixed

    • A long answer from Claude could be cut off, and the question failed.
      Each API call had 60 seconds, set before the models thought and before
      chat ran on Sonnet 5.5. An answer arrives whole once the model has
      finished, thinking included, so a hard question at high effort could run
      out of time, and the identical request was then sent three more times,
      about four minutes, before the question failed. Each call now has 3
      minutes, a request that runs out of time is sent once more rather than
      three times, and no call runs past 13 minutes from the question, so the
      reply or the error still reaches Discord inside its 15-minute window. The
      start-up key check keeps its 60 seconds.
    • A Confirm click during a deploy could report a failure after the change
      had gone through.
      Shutdown closed the fantasy server's MCP sessions
      before waiting for interactions in progress, so a Confirm click mid-call
      failed, and an espnff mcp question mid-loop lost its tools. The sessions
      now close after that wait. The LoL and D2 pollers still stop before it.
    • A stale or mistyped DISCORD_GUILD_ID kept the bot from starting.
      After registering its commands globally, the bot clears that guild's
      commands, and a refusal there (a 403 from a guild the bot has left) failed
      startup, so the container restarted into the same failure. It now logs a
      warning and starts. .env.example now says what the variable is for: a
      guild to clear leftover commands from, not one to register to.
    • A DClone spawn that the tracker reports as 6/6 was deleted at the
      reset.
      The bot counted a spawn only when a game dropped from a step
      straight back to idle. A game reported at 6/6 ("Diablo has invaded
      Sanctuary") pinged spawn subscribers under "DClone progress", and the drop
      to idle a poll later deleted that post instead of keeping it up for 12
      hours. The first arrival at 6/6 now counts as the spawn either way.
    Downloads
  • v5.3.3 5bb1c96036

    v5.3.3
    All checks were successful
    CI / build-and-test (push) Successful in 2m40s
    CI / docker-build (push) Successful in 1m37s
    Stable

    joeyr released this 2026-10-10 00:13:38 -05:00 | 5 commits to main since this release

    A busy moment at Anthropic or a stalled web page no longer fails a question,
    and a LoL game that seemed to end twice still gets its result.

    Fixed

    • One overloaded or server-error answer from Anthropic failed the whole
      question.
      A 529 "overloaded", or a 500, 502 or 503, got no retry. The
      SDK's own retries are off because the bot runs its own, and the bot's
      matched only rate limits and network errors. Inside a tool loop, the
      failure also threw away every round already done. These are now retried
      up to 3 times, waiting 1, 2 and 4 seconds, or as long as Anthropic's
      Retry-After asks, up to a minute.
    • web_fetch could leave a question hanging with no answer. Its
      15-second timeout ended once the headers arrived, so a server that sent
      headers and then trickled the body (a live feed, a long-poll endpoint, a
      deliberately slow page) held the question open past Discord's 15-minute
      window, with no answer and no error. The 15 seconds now cover the body
      too, and web_search reads Brave's results inside its timeout the same
      way.
    • A LoL game that ended twice could lose its result. A spectator 404
      mid-game ends a game falsely and starts a result retry, the next poll
      brings the game back, and the real end starts another. The second
      orphaned the first, which went on calling Riot every minute for up to half
      an hour where shutdown couldn't stop it, then logged every minute until
      the next restart. When its attempts ran out it stopped the second retry
      instead, so a result Riot published late was never posted. The newer retry
      now replaces the older one, and each retry stops only itself.
    Downloads
  • v5.3.2 6e828fe586

    v5.3.2
    All checks were successful
    CI / build-and-test (push) Successful in 3m10s
    CI / docker-build (push) Successful in 32s
    Stable

    joeyr released this 2026-10-09 23:54:54 -05:00 | 6 commits to main since this release

    Fixes a query that any user could send to crash the bot.

    Fixed

    • A query with ``` followed by about 2,000 characters without a
      space crashed the bot.
      The message splitter reads everything between the
      backticks and the first space or line break as a code block's language
      tag, and repeats the tag in every chunk of a long block. A tag close to a
      message long left no room for code, so the splitter looped until it ran out
      of memory. The reply header quotes the query, so any user could trigger
      it, and again after each restart. A block whose tag would take more than
      half of each chunk is now split as plain text. Before, a slightly shorter
      tag sent one message per character or two of code, and a block that was
      all tag was dropped from the reply. Both now split as text too.
    Downloads
  • v5.3.1 5c5cea9e49

    v5.3.1
    All checks were successful
    CI / build-and-test (push) Successful in 2m59s
    CI / docker-build (push) Successful in 51s
    Stable

    joeyr released this 2026-10-09 23:50:26 -05:00 | 7 commits to main since this release

    Rate-limit bypasses last as long as they say, OCE and SEA players work with
    the LoL monitor, and LOG_LEVEL can no longer switch the logs off.

    Fixed

    • A temporary rate-limit bypass expired as soon as it was granted. Its
      end time was stored as a TIMESTAMP, which keeps the bot's local time
      (America/Chicago) and drops the zone, then compared against NOW() in the
      database's UTC session. It read as five or six hours earlier than it was,
      so any bypass shorter than that was over before it started, and the
      longest ran 5–6 hours short. Migration 018 (applied at boot) turns every
      TIMESTAMP column into TIMESTAMPTZ, which stores the instant.
    • Channel history around the November clock change. Local time runs
      1 AM twice that night, and the same zoneless columns sorted messages from
      the second 1 AM before those from the first, so a follow-up asked in that
      hour missed what was said since the question before it. Fixed by the same
      migration.
    • OCE game results and recent form. Riot serves OCE match data from
      sea, but the bot asked americas, which answers 404, so every OCE game
      ended "results unavailable" after half an hour of retries.
    • Players on PH, SG, TH, TW and VN couldn't be registered. Their account
      lookups went to sea, which the account API doesn't serve. They now go to
      asia; any of the three account clusters returns any account.
    • LOG_LEVEL=INFO, warning, or any name winston doesn't know silenced
      every log line, errors included.
      winston drops every record at a level it
      doesn't know. Case is now ignored, warning reads as warn, and anything
      else logs at info, with a startup warning naming the value.
    • A LOG_LEVEL set in .env or .env.config never reached the logger
      under npm run dev or npm start: the logger read it before those files
      were loaded, while config/runtime reported the file's value. Docker, where
      the variables arrive in the environment, was unaffected.

    Deployment

    Migration 018 runs at boot and rewrites the messages table once. It reads
    the columns the bot writes in the bot's own time zone (the stack's TZ),
    which MigrationRunner now passes to every migration, so let the bot apply
    it rather than running npm run db:migrate from a machine in another zone.
    Rows those columns got before 1.10.0, when the bot ran in UTC, come out 5–6
    hours late; only spent bypasses and audit history are that old. Rolling the
    image back to 5.3.0 needs no database rollback: 5.3.0 reads and writes
    TIMESTAMPTZ correctly.

    Downloads
  • v5.3.0 521b082df7

    v5.3.0
    All checks were successful
    CI / build-and-test (push) Successful in 3m8s
    CI / docker-build (push) Successful in 34s
    Stable

    joeyr released this 2026-10-09 12:21:45 -05:00 | 8 commits to main since this release

    query-chat and espnff mcp run on Claude Sonnet 5.5, and LoL lane
    detection thinks before it answers.

    Changed

    • query-chat, espnff mcp and the start-up key check run on Claude
      Sonnet 5.5
      instead of Haiku 5.5. Everything else they send is
      unchanged: thinking on, high effort, 10,000 max tokens, a one-hour
      prompt cache. CLAUDE_MODEL now defaults to claude-sonnet-5-5. The
      live model is the stack's CLAUDE_MODEL
      , so set it to
      claude-sonnet-5-5 there. 5.2.0 runs fine on Sonnet 5.5 too, so the
      stack can go first. The cache belongs to the model, so each
      conversation's first follow-up after the switch reads its history at
      full price once, then caches again.
    • LoL lane detection thinks before it assigns lanes. It stays on Haiku
      5.5 at medium effort. Its token cap goes from 150 to 10,000, because
      thinking counts toward it, and a cap that thinking fills leaves no answer
      and every lane Unknown.
    Downloads
  • v5.2.0 a4ce36a489

    v5.2.0
    All checks were successful
    CI / build-and-test (push) Successful in 2m13s
    CI / docker-build (push) Successful in 25s
    Stable

    joeyr released this 2026-10-07 22:47:02 -05:00 | 9 commits to main since this release

    Finished walk posts no longer get stuck in the channel.

    Added

    • A Clone Hunters post that is never struck through goes 24 hours after
      its walk.
      Every post that names a time now goes on the delete list when
      it arrives, with the bot's reply under it, due a day after the last time
      in it. A post they forget to strike, or strike in a way the bot misreads,
      still goes. A strike still wins: struck through, the post goes in 30
      minutes as before. If they edit the walk time, the day counts from the
      new time. A post with no time in it ("Walk NOW EU") stays until it is
      struck, as before.

    Fixed

    • A walk post edited before its walk now goes once it is struck
      through.
      When Clone Hunters change a post first, crossing out the old
      region beside the new one (Region: ~~Asia~~ **Americas**), and strike
      the whole thing later, the strike marks pair up wrongly and Discord leaves
      Asia uncrossed. That one word made the bot read the post as still live,
      so it was never deleted. A post now counts as struck when it opens and
      closes with a strike mark, whatever is inside.
    Downloads