Commit Graph

38 Commits

Author SHA1 Message Date
Marcos Paulo e70df329b3 fix(chunkloader): release tickets on disable/toggle, fix BlueMap world matching, and prevent item loss on remove 2026-08-19 15:09:44 -03:00
Marcos Paulo e71babca1c fix(memory): align substring index with trimmed input in extractHeuristicFact 2026-08-18 22:00:42 -03:00
Marcos Paulo 01ada6d987 feat(chunkloader): add native chunk loader module with LuckPerms limits and BlueMap support 2026-08-18 21:45:51 -03:00
masi 01e6c28fef Merge pull request 'feat(ia): add Judite & Narrador personas, per-player AI selection, persistent memory, and event reactivity' (#3) from feat/ia-personalities-and-memory into main
Reviewed-on: #3
2026-08-18 22:52:02 +00:00
Marcos Paulo 998757d610 fix(ia): make memory persistence async, wire heuristic fact extraction and /ia lembrar, and add tag and event tests 2026-08-18 19:42:46 -03:00
Marcos Paulo 2220f11e64 fix(ia): prevent private notes leak in places tool, guard recordTurn against disk failure, and add tests 2026-08-18 19:15:02 -03:00
Marcos Paulo ea55813019 feat(ia): add Judite & Narrador personas, per-player AI selection, persistent memory, and event reactivity 2026-08-18 19:00:11 -03:00
masi dafd96a4b6 i18n: per-player EN/PT via Adventure GlobalTranslator (#1)
Foundation + commands module of the i18n spec.

- I18n registry/loader + Lang.tr facade + reloadI18n
- PT source-of-truth bundle + EN translation
- CanalhandiaCommand player-facing strings migrated; admin-tuning/help/enum-labels deferred
- I18nTest: parity + per-locale render + pt_BR fallback; 316/316 green

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-12 15:53:27 +00:00
marcos c1a6b9730f docs: document the IA module
README: add ia to the modules table, the four ia permissions, and a full
IA section covering commands, profile switching, operator corrections,
memory/context, and the key + rate-limit model. Notes the safety boundary
(no tools, reply never executed, leading slashes stripped) and which config
values are baked vs live.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 03:56:58 +00:00
marcos a3b2e7dd27 fix(ia): wire canalhandia.ia.privado gate, tighten subcommand hijack
Review of Task 11 (commit 5f616d3) flagged a blocking spec regression:
canalhandia.ia.privado was declared (default: true) but never checked --
ia(sender, args, isPrivate) only checked canalhandia.ia, so /iap silently
required canalhandia.ia and the privado perm did nothing.

Per operator decision, /ia and /iap are now separate gates, both default
op, so LuckPerms can grant them independently (operator + permitted only,
not everyone). /ia needs canalhandia.ia; /iap needs
canalhandia.ia.privado.

Also fixes three should-fix findings:
- Subcommand hijack is no longer greedy: perfil and feedback only hijack
  when the second token is one they act on (a known profile key, or "ruim"),
  so "/ia perfil do servidor" and "/ia feedback do mapa?" fall through and
  are asked. corrigir stays greedy (a correction always reads the rest).
- AiProfile.isValid tells a real key from the PRECISO fallback, so /ia perfil
  blah no longer silently switches to PRECISO.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 03:54:01 +00:00
marcos 5f616d3d99 feat: /iap, reactions on answers, feedback and /ia corrigir 2026-08-06 03:43:19 +00:00
marcos a3f40d1408 fix: Ai review fixes — quit-race, token overflow, comment + nits
- Guard conversations.remember() on asker==null: a PlayerQuitEvent
  forgets the history (carry-forward #6), and re-adding here after the
  quit would resurrect it. lastAnswer stays regardless so /ia corrigir
  can still correct the last answer.
- Saturate the retry token ceiling: max_tokens near Integer.MAX_VALUE
  would overflow to a negative budget sent to the API.
- Tighten the constructor comment: aiUrl/aiWikiChars/aiMemoryExchanges/
  aiMemoryMinutes are baked; everything else (aiProfile, aiModel,
  aiMaxTokens, aiTemperature, aiInstructions, aiServerContext) is read
  live, not just aiProfile.
- Drop unused import java.util.List; add trailing newline.
2026-08-06 03:32:31 +00:00
marcos 5b13ec7713 feat: ground answers in the wiki, recipes, corrections and memory
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 03:19:54 +00:00
marcos 7940a8fc66 fix: guard Corrections against async read from compose
all() returns List.copyOf(entries), which iterates; add()/load()
structurally modify. Once Task 10 wires all() into runTaskAsynchronously
and Task 11 wires add() from the main-thread /ia corrigir command,
the race throws ConcurrentModificationException. Guarded by entries'
own monitor like Conversations: file parse and YAML save stay outside
the lock, only fast in-memory work is under it.
2026-08-06 03:12:27 +00:00
marcos 5cfa580997 feat: operator corrections injected into similar questions 2026-08-06 03:09:11 +00:00
marcos aee991bb7b feat: IA profiles, server context and a workable token ceiling
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 03:03:53 +00:00
marcos e0d7c289a8 feat: short per-player conversation memory
Guarded by a monitor: Ai calls this from runTaskAsynchronously and
history() prunes while it reads, so an unsynchronised map would throw
ConcurrentModification into a player's answer or corrupt itself on resize.

Expiry compares nanoTime differences rather than wall-clock instants, so
a zero window expires deterministically and an NTP step backwards cannot
leave entries stamped in the future and unexpirable. Player count is
capped LRU, since expiry only prunes players who ask again.
2026-08-05 16:23:57 +00:00
marcos c5d8f44bf2 fix: restrict API keys to printable ASCII, and harden response parsing
Fuzzing all 65,536 char values, not the 256 of the previous pass, shows
the JDK rejects 65,312 of them from a header value and every single one
echoes the value back. The previous deny-list covered 33. A key file
saved by Notepad or PowerShell Out-File carries a U+FEFF byte order
mark, which passed both filters and reached the quoting validator.

Ai.cleanKey and HttpFetcher.checkBearer now allow printable ASCII only,
an allow-list of the 94 characters a bearer token is made of, which
cannot drift out of what the JDK accepts. Demonstrated invariant:
cleanKey output ⊆ checkBearer accepts ⊆ JDK accepts, 0 violations.

Ai.call gains the same check and its failure log is now redacted; that
was the one path where the proof-of-concept leak surfaced.

MiniMax.message and answer now check JSON types before assuming them.
Gson throws unchecked on JSON that parses but has the wrong shape, and
both run outside post()'s try, so {"choices":["str"]} escaped to an
async Bukkit worker as a stack trace instead of the promised null.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 16:18:32 +00:00
marcos 88fe9c9695 fix: stop a malformed API key reaching a log line
HttpRequest.Builder.header() quotes the offending header value back in
its IllegalArgumentException. Fuzzed on temurin-25: all 32 control
characters it rejects echo the value, so a key carrying any of them ends
up in whatever log catches the throw. A key file with a comment on line
two survives trim() and is enough to trigger it.

Ai.cleanKey now takes the first non-blank line and drops control
characters, so a malformed key never forms. HttpFetcher.checkBearer
rejects one anyway before the request is built, with a message naming
only the position, so no future caller has to remember to redact. Its
reject set is a strict superset of the JDK's.

Also renames MiniMax.Msg to MiniMax.Turn: the package already has a
top-level Msg, the chat-formatting helper, which the record shadowed
inside MiniMax.java.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 16:05:22 +00:00
marcos 6e0167cff1 feat: MiniMax client with forced tool call for wiki term selection
Term selection is a forced tool call rather than free text: measured 5/5
against 2/7 for a free-text extraction call, because a tool argument is
structured output and survives the hidden reasoning eating the budget.
With tool_choice auto the model skipped the search on exactly the
questions it was most likely to get wrong.

Failures are reported through an optional warn consumer, as Wiki does,
and the endpoint is injectable for MiniMax's regional hosts.

Warnings are redacted of the key: a key with an embedded newline makes
the JDK throw invalid header value: "Bearer sk-...", quoting the whole
value back, and that lands in the generic call-failure path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 16:00:46 +00:00
marcos 62497556a5 fix: never resolve a recipe question to the wrong item
The substring fallback in materialFor returned plausible recipes for
items the player did not ask about. "Redstone Repeater" tied on length
and resolved to REDSTONE, handing someone asking about repeaters the
recipe for redstone dust; "Book and Quill" gave BOOK, "Minecart with
Chest" gave MINECART, "Rabbit's Foot" gave RABBIT, "Chestplate" gave
CHEST. That is the exact failure this feature exists to remove, and it
is worse than no answer because the model states it confidently.

Dropped it. Exact match plus a hand-checked alias table, else null. No
suffix rule either: "axe" is a suffix of "pickaxe", so tool and armour
families would fail the same way. Beds and wool now return null, which
is correct.

brewing() matched keys as bare substrings and hijacked real questions:
"salto" inside basalto, "cura" inside curar, "forca" inside reforcar.
Since describe() answers brewing first, each took over the whole answer.
Now gated on the question mentioning a potion, and matched on word
boundaries.

Table corrections: "Fogo do Dragao" is not an item, it is Bafo do Dragao
giving a lingering potion; Frasco de Agua, not Garrafa de Agua, which is
the empty bottle; Pe de Coelho; Fatia de Melancia Reluzente. Added the
in-game item names players actually type (Agilidade, Dano) and stripped
hyphens so Mestre-Tartaruga reaches the table.

describeShapeless no longer emits a dangling "Sem formato: ".

describeChoice could not be covered after all: constructing a
MaterialChoice initialises org.bukkit.Registry, which needs a server,
and the class is sealed so it cannot be faked. Verified in Task 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 15:54:40 +00:00
marcos dba64df917 feat: ground recipes on the server, translated via wiki langlinks
Crafting recipes come from Bukkit.recipeIterator(), which is authoritative
for this exact version. Player questions are Portuguese and Material names
are English, so the subject of the question is searched on the pt wiki and
prop=langlinks&lllang=en gives the English title, which uppercases onto the
enum constant.

Matching material names against the question directly does not work.
Measured over twenty realistic pt-BR questions it resolved 1 of 20, and that
one ("tridente" containing "trident") by coincidence rather than
translation. Through langlinks the same twenty resolve 17 of 20.

Brewing is not exposed by Bukkit at all: there is no brewing Recipe type,
PotionBrewer has no getter or iterator, and vanilla brewing is hardcoded in
PotionBrewing rather than registered as a recipe. Potions therefore come
from a hand-written pt-BR table. This is what actually answers the
fire-resistance question that motivated the feature; recipeIterator() alone
never could have.

Ingredients are read through getChoiceMap/getChoiceList. The deprecated
getIngredientMap/getIngredientList collapse a choice to one arbitrary stack,
printing "oak planks" where the recipe accepts any plank.

The design doc claimed recipeIterator() closed the potion case. It did not;
corrected to record what is true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 15:40:36 +00:00
marcos 0f3bdf209d fix: report wiki failures instead of grounding silently on nothing
Every failure mode returned null, which the caller cannot tell apart
from a term the wiki has no article for. A 403 would revert /ia to the
confidently wrong answers grounding exists to stop, against a clean log.

Also clamps maxChars so a config of 0 cannot switch grounding off for
good, restores the interrupt flag on disable, and distinguishes a
malformed response from an outage in the log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:25:45 +00:00
marcos 13fb87fb8b feat: fetch full articles from the Portuguese Minecraft Wiki 2026-08-05 15:17:01 +00:00
marcos 9ffcf816ea feat: add Fetcher seam with an identifying user agent 2026-08-05 15:11:11 +00:00
marcos 3d2150d2a6 docs: scope the foreign-script and surefire claims to what the code does
hasForeignScript detects by alphabet, so Latin-script leakage such as
the observed French "contiennent" is not caught. Say so in the javadoc
and admit the gap in the design doc rather than implying coverage.

failIfNoTests catches a misplaced or misnamed test class, not a
disabled one: an @Disabled class still reports as skipped and the
build stays green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 15:09:37 +00:00
marcos 4f427659bb refactor: extract AiText and detect foreign-script leakage
Moves sanitise out of the Bukkit-bound Ai class so it can be unit
tested, and adds hasForeignScript to catch the CJK words the model
intermittently drops into Portuguese answers.

Colour-code stripping now removes the code character too: replacing
only the section sign left "§c" reading as a stray "c" in chat.

Surefire now fails on an empty suite, so a misplaced or disabled test
class cannot pass as a green build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3jSvSTrG4qsniLSr6TpC6
2026-08-05 15:03:00 +00:00
marcos be1f31778a test: add JUnit 5 harness 2026-08-05 14:57:45 +00:00
marcos 2d867cefca docs: implementation plan for /ia grounding 2026-08-05 14:49:08 +00:00
marcos 3f9e80047c docs: design for grounding and feedback in the /ia module
Records what was measured rather than assumed: pt.minecraft.wiki as the
source, forced tool_choice for term selection (5/5 against 2/7 for
free-text extraction), full article text over exintro, and M2.7 over M3.

Also records two defects found while measuring — empty replies caused by
hidden reasoning eating max_tokens at the deployed value of 300, and
foreign-token leakage into Portuguese answers.
2026-08-05 14:45:03 +00:00
marcos 0b3be45d76 feat: pick MiniMax-M2.7, and strip markdown and emoji from replies
Benchmarked M2.7, M3 and M2 against the same four pt-BR prompts (a
Minecraft question, a general-knowledge one, an unanswerable one, and a
jailbreak attempt).

M2.7 wins on the axis that matters for chat. All three held the
no-server-access line under the jailbreak, but:

- M3 is a reasoning model, and its thinking counts against max_tokens.
  At the 300 chat needs, its Minecraft answer was cut off mid-word at
  39 characters while reporting 590 tokens used. Raising the budget
  enough to fix that costs more per question than the answer is worth.
- M2 answered the brewing question wrong, going straight from a water
  bottle to magma cream and skipping the nether wart.
- M2.7 answered correctly in 2.7-5.2s.

The benchmark also showed both markdown and emoji in every model's
output. Minecraft chat renders neither: `**negrito**` arrives as
literal asterisks, and emoji are empty boxes on Bedrock. The system
prompt now asks for plain text, and sanitise() strips emphasis, code
fences, headings and emoji regardless, since a prompt is a request and
not a guarantee.
2026-08-05 14:01:22 +00:00
marcos 0d84a606d4 feat: /ia — chat question-and-answer backed by MiniMax
Adds an `ia` module: `/ia <pergunta>` sends the question to an
OpenAI-compatible chat-completions endpoint (MiniMax by default) and
posts the reply to chat.

Access is gated by `canalhandia.ia`, declared `default: op` so the
operator has it out of the box and LuckPerms can grant it to anyone
else.

The model can only ever produce chat text:

- the reply goes to sendMessage and nowhere else — it is never passed
  to the command dispatcher;
- no `tools`/`tool_choice` are sent, so there is nothing for the model
  to call;
- the system prompt states it has no server, shell or command access;
- replies are sanitised — colour codes stripped so they cannot forge
  server messages, newlines folded so one answer is one chat entry,
  and leading slashes removed so nothing reads as a command to run.

The API key is deliberately not a config value, since config.yml is
committed. It is read from MINIMAX_API_KEY or from
plugins/Canalhandia/minimax.key, which is now gitignored.

Cost is bounded by a per-player cooldown and a server-wide daily cap,
both visible in /canalhandia status. The HTTP call runs off the main
thread; only the delivery hops back onto it.
2026-08-05 13:41:28 +00:00
marcos be6d34f456 feat: show who reacted, and stop curiosities flooding chat
Reactions only ever showed counts, so with several reactors nobody could
tell who had reacted. Naming everyone on its own line does not scale, so
names now appear in three progressively larger places: a hover tooltip per
button (Java only), a one-line closing summary naming the first
resumo-nomes (3) and collapsing the rest to "+N", and /reacoes for the full
breakdown sent privately.

Reactors' names are captured at react time so the summary survives them
logging off.

Two separate causes of chat flooding:
- every join fired its own curiosity, so a wave of joins produced one each.
  Automatic triggers are now rate-limited by intervalo-minimo-segundos
  (120); a typed /curiosidade still bypasses it.
- a curiosity cost two messages. The button row is now appended to the
  headline instead of being broadcast separately.

Also fixes "1 horas" — CuriosityFactory had its own duration formatting
that missed the earlier plural fix; it now uses Msg.duration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 21:38:28 +00:00
marcos 987ee13d74 feat: Bedrock support — typed commands and ASCII labels
Geyser cannot deliver a chat clickEvent to Bedrock, and most emoji render
as tofu boxes there, so Bedrock players could see the reaction row but
neither read nor use it.

Messages carrying buttons are now built twice and sent per player rather
than via Bukkit.broadcast. Each reaction gains a per-platform label plus a
typed shortcut:

    uau: { java: "[😮]", texto: "[UAU]", comando: "wow" }

Typed fallbacks for every clickable interaction: /reagir, /legal, /wow,
/top, /f, /palpite and /votar, all acting on the most recent message so no
message id is needed.

Bedrock detection uses Floodgate's UUID scheme (high 64 bits zero) rather
than the Floodgate API, keeping it an optional runtime dependency.
/canalhandia plataformas lists who is online and on which platform.

Reaction config moves from a flat key->label map to a section per
reaction; the old flat form is still accepted and gets an ASCII fallback
derived from the key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 21:31:24 +00:00
marcos 18ac5a5d17 fix: locate stats directory under Paper's dimensions layout
Rankings returned "sem dados" because World#getWorldFolder points at the
dimension directory in Paper's newer layout, so <worldFolder>/players/stats
does not exist. The real path is <worldContainer>/<level-name>/players/stats.
Try that first, fall back to the older locations, then climb out of the
dimension folder, and warn instead of silently returning nothing.

Also fixes "1 dias e 0 horas" — durations now agree in number and drop a
zero remainder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:52:38 +00:00
marcos 801b43610f feat!: rename to Canalhandia and add four chat games
Renames the plugin from Curiosidades to Canalhandia (the server name) now
that it covers more than curiosities, and adds:

- adivinha: the curiosity with the name hidden and clickable player names,
  revealing who guessed right. Curiosity sentences never contained the
  subject's name, so the same generator feeds both with no redaction step.
- luto: a clickable [F] under death messages with a closing count.
- enquete: polls with clickable options and a live boss-bar tally.
- ranking + marcos: leaderboards covering offline players, and one-time
  milestone announcements persisted to marcos.yml.

Fixes the reaction counter being invisible. Chat cannot be edited after
sending, so the button counts are necessarily frozen; previously the only
live surface was the boss bar and the buttons showed no number at all.
Counts now appear in the buttons at send time, on the boss bar, on the
reactor's action bar, and as a final tally line when the window closes.

Late clicks used to be dropped silently once the 90s window passed. The
last 8 reaction sets now stay live for reacao-validade-minutos (15).

Rankings read the stats JSON directly because Bukkit only exposes
statistics for online players.

Adds README.md documenting the design constraints that are easy to
mistake for bugs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:46:42 +00:00
marcos 7fd3fc157c feat: join-triggered by default, plus runtime admin controls
Default trigger is now player join rather than a timer, with a short
delay so the curiosity lands after the join message instead of racing
it. Modes entrada/intervalo/ambos/manual select the triggers.

Adds a full /curiosidade tree so behaviour is adjustable in-game
without editing config.yml: mode, interval, join delay, per-player
cooldown, no-repeat history, reaction window, reaction labels, and
per-category toggles. Every setter writes through to disk immediately
so changes survive a restart.

Facts are now tagged with a category so they can be filtered, and a
per-player cooldown plus recent-fact history stop repeats when someone
relogs.

Config changes are gated behind curiosidades.admin; reacting and
opting out stay default-true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:24:54 +00:00
marcos c528ed55c8 feat: curiosidades plugin for stat-based server announcements
Announces a random Portuguese fact about a random online player on a
timer, sourced from vanilla statistics, with clickable reactions.

Block/item/mob names are emitted as translatable components so each
client renders them in its own language instead of shipping a
translation table. Sentences are phrased to avoid number agreement with
the translated noun ("5.966 blocos de Pedra", not "5.966 Pedras").

Statistic constants are resolved by name with fallbacks because they get
renamed across Minecraft releases; a rename degrades one curiosity
rather than breaking the announcement.

Chat cannot be edited after sending, so live reaction tallies ride on a
boss bar while the in-chat buttons stay frozen at send time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ActvLGJApdxEAd2yfKPwqv
2026-08-04 20:18:05 +00:00