• v1.17.0 bcc577d7e7

    v1.17.0 — espnff opens up, and learns whose team is whose
    All checks were successful
    CI / build-and-test (push) Successful in 2m53s
    CI / docker-build (push) Successful in 58s
    Stable

    joeyr released this 2026-09-10 21:59:33 -05:00 | 57 commits to main since this release

    Added

    • ESPNFF_TEAM_MAP — who is who in the league. Comma-separated
      discordUserId:teamId pairs, telling the bot which ESPN team each Discord
      user manages.

      This closes a wrong-answer bug, not a missing feature. The fantasy
      server is configured with exactly one team id and flags it is_your_team.
      That is correct for whoever set it up and wrong for every other manager in
      the league — and the flag says nothing about who is typing. So "how did my
      team do?" from a second manager came back complete, confident and about
      somebody else's team, with nothing in the answer to give it away. The
      asker's own team id now goes into the system prompt for every request, and
      the is_your_team flag is explicitly demoted where they disagree.

      Nothing can derive this mapping: ESPN knows a manager by an ESPN display
      name, Discord knows a snowflake, and no field joins them. An unmapped user
      is told apart from a mapped one and asked which team they mean rather than
      being given a guess; /aufhocker espnff help tells each person which team
      the bot has them as, or that it has none. A malformed entry is dropped with
      a warning — including 123: and 123:0, which would otherwise map someone
      onto a team that does not exist.

    Changed

    • /aufhocker espnff mcp is no longer admin-only. The ten fantasy tools
      are read-only and see one league, so the reach that makes web_search
      admin-gated does not apply; the rate limiter is the cost control, as it is
      for every other command.

      The gate this required is in ClaudeClient: naming an explicit tools
      list now authorizes that list.
      isAdmin still gates the DEFAULT set —
      general-purpose reach onto the open internet, spending an API key, for
      anyone who can type in a channel. A named list is a narrower claim the
      calling command has already made. Without this the command would have kept
      working for non-admins while quietly dropping its tools, which produces a
      fluent fantasy answer with nothing behind it.

    Downloads
  • v1.16.0 d67759f734

    v1.16.0 — espnff help, generated from the live tool list
    All checks were successful
    CI / build-and-test (push) Successful in 2m24s
    CI / docker-build (push) Successful in 43s
    Stable

    joeyr released this 2026-09-10 21:48:24 -05:00 | 58 commits to main since this release

    Added

    • /aufhocker espnff help — the fantasy commands, and what is behind
      them. Lists every tool the fantasy server currently publishes with a
      one-line gloss, plus example questions to copy.

      Rendered from the tools actually registered, never from a list kept in
      this repo.
      So a tool added to the fantasy server appears here on the bot's
      next restart, and — the part that matters when something has gone wrong — a
      server that is not connected says exactly that instead of showing a
      confident, empty list. It answers from local state: no Claude call, no
      deferral, and deliberately not subject to the rate limit, because it is the
      command you reach for when you are already stuck.

    Changed

    • /aufhocker espnff is now /aufhocker espnff mcp query: <text>. The
      fantasy commands moved under a subcommand group to make room for help.
      Shipped in 1.15.0 and renamed before it ever reached a deployment.
    Downloads
  • v1.15.0 594fc8dd99

    v1.15.0 — /aufhocker espnff, a command of its own
    All checks were successful
    CI / build-and-test (push) Successful in 2m27s
    CI / docker-build (push) Successful in 48s
    Stable

    joeyr released this 2026-09-10 21:39:40 -05:00 | 59 commits to main since this release

    Added

    • /aufhocker espnff query: <text> — a command of its own for the ESPN
      fantasy league.
      Previously the fantasy tools were only reachable through
      /aufhocker query-chat, mixed in with web_search and the channel history,
      and whether they were used at all was left to the model.

      It is the same Claude client pointed at one subject, and the three
      differences from query-chat all remove something:

      • Only the borrowed fantasy tools are offered. web_search is not on
        the table, so a question this league's data cannot answer comes back as
        "I don't know" rather than as a confident answer assembled from the open
        web — which is the failure that looks most like success.
      • No channel history is built or sent. A fantasy question is about the
        league, not the conversation. That also removes the prompt-injection
        surface the general system prompt spends a paragraph defending against.
      • It refuses outright when the tools are missing, rather than answering
        without them. A fantasy answer with no tool call behind it is a guess, and
        in Discord it would read exactly like a real one.

      Admin-gated, like every other tool-using path in this bot, and it says so
      when it refuses. Rate limiting, channel-permission checks and the "Make
      Public" button are shared with query-chat rather than reimplemented —
      the publish rules are the security-sensitive part of that plugin, and a
      second copy is a second place for them to drift.

    Changed

    • ClaudeRequest accepts an optional system prompt and an optional explicit
      tools list. An empty tools: [] means no tools and is deliberately not
      the same as omitting the field, which still means "the built-ins plus
      everything borrowed from MCP".

    Fixed

    • Tool use was gated on config.admin.availableTools.length > 0, so a
      deployment that configured no built-in tools would also have withheld every
      tool borrowed from an MCP server. The gate now counts the tools actually
      being offered.
    Downloads
  • v1.14.0 966cb0e8fe

    1.14.0 — tools borrowed from MCP servers
    All checks were successful
    CI / build-and-test (push) Successful in 2m21s
    CI / docker-build (push) Successful in 58s
    Stable

    joeyr released this 2026-09-10 21:05:39 -05:00 | 60 commits to main since this release

    Tools borrowed from MCP servers

    MCP_SERVERS takes comma-separated name=url pairs. At startup the bot connects to each,
    asks what tools it publishes, and hands them to Claude alongside the built-in web_search
    and web_fetch. A tool added on the server appears in Discord on the next restart, with
    no change to this repo
    — which is the point, and also the gotcha: the tool list is no
    longer in this repo, so "where is get_standings implemented" is answered by the server
    rather than by grep.

    The first consumer is mcp-gateway's ESPN fantasy football tenant, so
    /aufhocker query-chat can now answer about a league roster, matchups, standings, the
    waiver wire and recent transactions.

    Four deliberate non-behaviours

    • connect() never throws. Plugin init() errors are fatal to startup, so a fantasy
      server restarting for thirty seconds would otherwise take Discord's general-purpose chat
      down with it. It reports instead, and the bot starts with whatever tools it got.
    • Borrowed names are not prefixed. The servers' own descriptions cross-reference each
      other by name — "get the team_id from list_teams first" — so prefixing would silently
      break every one of those sentences. The cost is possible collisions, so a name matching a
      built-in is refused rather than overwritten.
    • A server refusal comes back as content, not an error. Those refusals are written for
      the model to read and act on: "that week has not been played yet; get_matchups shows the
      scheduled pairings instead". Throwing would replace that with "tool execution failed".
    • Nothing is reached over the public internet. Service aliases on the Swarm's internal
      network, not proxied hostnames.

    Authenticated with a single bearer token (MCP_AUTH_TOKEN) rather than OAuth: a bot has no
    browser to complete a consent screen with, which is why the server side runs in token mode.

    An eighth preflight check

    preflight/mcp reaches every configured server at boot and reports its tool count — the
    same "prove it works before something needs it hours later" the other seven exist for. It
    connects separately from the session the query plugin holds and pays a second handshake
    to do it: a line reporting what some other component happened to find is a line that goes
    stale when that component changes. A server that connects but publishes zero tools is a
    failure rather than an ok carrying a zero — that is what a misconfigured tenant looks
    like, and a green line is exactly what gets read as fine.

    Also

    • browserslist pinned to ^4.28.7 in overrides (High: unbounded memory growth, and a
      prototype write in normalizeStats). Dev-only — reached solely via jest → babel, so
      never in the production image. Newly published rather than newly introduced: it would have
      failed npm audit --audit-level=high on the next push regardless.

    833 tests green.

    Deploying this

    Two one-time prerequisites on rpi5a:

    docker network create -d overlay mcp_net
    printf %s '<token>' | docker secret create espnff_mcp_token -
    

    Then a yaml stack update for both aufhocker and mcp-gateway.

    Downloads
  • v1.13.6 3f3f77f61c

    v1.13.6 — recent form back to five games
    All checks were successful
    CI / build-and-test (push) Successful in 3m51s
    CI / docker-build (push) Successful in 38s
    Stable

    joeyr released this 2026-08-31 22:19:41 -05:00 | 61 commits to main since this release

    Changed

    • Recent form is back to five games. The live-game embed had been showing a
      three-game win/loss record since 1.13.0, where the count was cut to hold a
      detected game's enrichment burst to 40 calls on a Riot personal key shared
      with lol-companion. Three games is too small a sample to read as form, and the
      two icons the formatter reserves for a clean sweep (five straight wins, five
      straight losses) could never fire at that count. Back at five, a detected game
      asks for ~70 calls; riotMaxRequestsPer2Min still caps us at 40 per window,
      so the limiter spreads that burst rather than taking 429s — the cost is that
      the Phase 2 embed edit lands a few minutes after the notification instead of
      seconds after it. Set LOL_RIOT_MAX_REQUESTS_PER_2MIN higher to shorten that
      wait, but only alongside lowering lol-companion's ceiling: the two must still
      sum under the key's 100.
    Downloads
  • v1.13.5 ba5b945b71

    v1.13.5 — name which side a 429 came from
    All checks were successful
    CI / build-and-test (push) Successful in 1m55s
    CI / docker-build (push) Successful in 23s
    Stable

    joeyr released this 2026-08-26 10:47:02 -05:00 | 62 commits to main since this release

    Changed

    • A 429 now names which side refused it. An idle poll rotation was logging a
      steady drip of Riot API rate limited, retrying in 12s warnings with nobody in
      a game — which reads as our own pacing misbehaving, when the header on every
      one of them said otherwise. Riot's X-Rate-Limit-Type distinguishes our key's
      quota (application / method — actionable, pace down) from Riot's own
      backend shedding load (service, and server is observed in the wild), which
      no pacing on our side prevents: spectator-v5 refuses the question before it can
      answer "not in a game", so an idle rotation draws as many of these as a busy
      one.

      Each 429 line now carries a blame field (ours / riot / unknown) and
      says the same in prose, so the two incidents wearing one status code are
      tellable apart at a glance instead of by reconstructing the raw header. A 429
      Riot omits the header from is unknown, never riot — quietly blaming Riot
      for what might be our own quota is the misread the field exists to prevent.

      Riot-side retries that succeed dropped to debug: the wait is the whole cost,
      and the drip was burying real warnings. The lines that mean a poll actually
      lost its answer — the too-long-to-wait skip and exhausted retries — stay at
      warn regardless of blame.

    Downloads
  • v1.13.4 d8d207089d

    v1.13.4 — lane detection budget fix
    All checks were successful
    CI / build-and-test (push) Successful in 1m56s
    CI / docker-build (push) Successful in 23s
    Stable

    joeyr released this 2026-08-25 22:47:28 -05:00 | 63 commits to main since this release

    Fixed

    Lane detection assigned no roles at all. Every live game logged Lane detection response contained no text blocks and came back with rolesAssigned=2 — and those two were the Smite junglers, which laneDetectionService pre-assigns locally before Claude is ever called. The other eight players fell back to Unknown, so the sorted-by-role embed and the role hints handed to the prediction prompt were both running empty.

    The call asked for max_tokens: 150 and never set thinking. Current models think by default, and the default display is omitted — so the whole 150-token budget was spent inside a thinking block that carries no visible text, and the response came back with zero text blocks. callClaude returned null, and if (!raw) break skipped the retry, so the single warning per game was the whole story.

    Fixed by sending thinking: { type: 'disabled' }. Role assignment is deterministic extraction from a fixed champion list, not reasoning, so the small budget was always the right size — it just needed to be spent on the answer. gamePredictionService was never hit by this: its 500-token budget leaves room for both.

    The warning now carries stopReason and the block types that did come back, so the next variant of this failure names itself instead of looking like a parse problem.

    Downloads
  • v1.13.3 41a5d032f0

    v1.13.3 — put the running build on the first line, by itself
    All checks were successful
    CI / build-and-test (push) Successful in 1m44s
    CI / docker-build (push) Successful in 24s
    Stable

    joeyr released this 2026-08-25 21:13:00 -05:00 | 64 commits to main since this release

    Added

    • The running build is now the first line of the boot log, on its own.

      2026-08-25 21:09:59 INF bot/version > aufhocker v1.13.3
      

      The version was previously one field among three on the sixth line
      (Starting Aufhocker Discord Bot... | environment=… nodeVersion=… version=…),
      below five lines emitted by import side effects — unfindable by a human
      scrolling container output. That cost real time during the 1.13.x rollout: a
      deploy that had genuinely rolled forward was read as one that had not.

      Emitted from a new src/utils/bootBanner.ts at import time, not from
      main(). Everything appearing above main()'s first line does so from its own
      import side effects (the database pool, the shutdown handler, the admin
      services), and ES module imports evaluate in source order — so importing the
      banner first in index.ts is the only thing that puts it at the top. There is
      no import-order lint rule, so that import carries a comment saying it must
      stay first.

      Deliberately field-free, since a bare message is what makes it scannable. New
      bot/version phase so it can be grepped on its own. The structured version
      fields on the startup and config/runtime lines are unchanged for anything
      parsing the logs.

    Downloads
  • v1.13.2 b609a8c499

    v1.13.2 — stop logging network blips as unfixable mapping bugs
    All checks were successful
    CI / build-and-test (push) Successful in 1m45s
    CI / docker-build (push) Successful in 25s
    Stable

    joeyr released this 2026-08-25 21:04:13 -05:00 | 65 commits to main since this release

    Fixed

    • A passing network blip was logged as an unfixable bug. pollSummoner
      sorts poll failures with err instanceof TypeError, on the reasoning that a
      TypeError can only come from our own mapper reading a field Riot omitted. But
      fetch reports every transport failure as a bare TypeError: fetch failed,
      so DNS hiccups, resets and timeouts all landed in that branch and were logged
      at ERROR as Bug mapping Riot spectator response — game will NOT be announced. Both claims were false: nothing was wrong with the mapper, and the
      next cycle announced the game normally. Production logs show the contradiction
      a millisecond apart — Riot API network error immediately followed by
      Bug mapping Riot spectator response for the same failure.
    • RiotApiClientBase now wraps transport failures in a new exported
      RiotNetworkError, preserving the original as cause. It is not a
      TypeError, so it falls to the transient branch and a genuine mapping bug
      still reaches the error branch — which is what the instanceof TypeError test
      meant all along. That test was added to stop mapping bugs hiding under
      "transient network error" (the missing-perks crash on Classic games); this
      is the same misfiling in reverse, and both directions are now pinned by tests.

    Behaviour is unchanged — both branches already returned 'skipped', counted
    the summoner in the cycle summary, and retried next cycle. This is log accuracy
    only, which matters because these logs are the instrument used to diagnose the
    shared-key 429s in 1.13.0.

    Downloads
  • v1.13.1 48cf31b731

    v1.13.1 — stop restart recovery discarding a live game
    All checks were successful
    CI / build-and-test (push) Successful in 1m45s
    CI / docker-build (push) Successful in 25s
    Stable

    joeyr released this 2026-08-25 20:47:05 -05:00 | 66 commits to main since this release

    Fixed

    • Restart recovery discarded a live game every time the bot restarted.
      activeGameTracker persists the platformId that came back on spectator data
      (NA1), but getPlatform accepted only user-facing region codes (NA) and
      threw Unknown region: NA1. recoverActiveGames caught the throw, marked the
      game stale and cleaned it up, so a genuinely in-progress game lost its
      end-of-game notification on every deploy — observed twice in ten minutes
      across the 1.13.0 rollout (games 5628417643 and 5628441631). getPlatform now
      accepts either form, matching getRoutingFromRegionOrPlatform, which had
      handled both all along. Unrecognised values still throw, so a typo fails
      loudly rather than reaching Riot as a malformed hostname.

    Changed

    • .env.config drops LOL_RIOT_MAX_REQUESTS_PER_2MIN from 75 to 40, so the
      repo's mirror agrees with the code default 1.13.0 introduced. The deployed
      stack pins these same three variables, which is why the 1.13.0 rollout came up
      still reporting riotMaxPer2Min=75 — the image had changed but the
      environment overrode it. The comment now says outright that leaving them unset
      is preferred, since pinning a copy of the default is what made the default
      dead config.
    Downloads