feat(ia): add Judite & Narrador personas, per-player AI selection, persistent memory, and event reactivity #3
Reference in New Issue
Block a user
Delete Branch "feat/ia-personalities-and-memory"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
JUDITE(SAC/telemarketing) andNARRADOR(fantasy narrator) personas./ia persona <nome>with disk persistence inia-memoria.yml.[IA]tag with dynamic persona tag matching player preference (e.g.[Judite],[Zoeiro]).PlayerMemoryfor compressed sliding-window conversation history and key facts.lugares_jogadortool for location and death history grounding.🤖 AI Review · pragent pilot · glm-5.2:cloud ·
ea558130Adds Judite/Narrador personas, per-player persona selection persisted to ia-memoria.yml, a sliding-window PlayerMemory, a lugares_jogador tool, and persona-tagged chat output. Overall risk is medium: the new tool breaks the repo's stated private-notes-never-go-to-the-IA invariant by feeding arbitrary players' private notes to the third-party LLM, and a disk failure in recordTurn can swallow an already-computed answer. 1 high, 1 medium, 1 low.
🔋 AI usage
glm-5.2:cloud· engine: opencode · agent steps: 16 · duration: 158.3ssrc/main/java/dev/marcospaulo/canalhandia/Tools.java:149src/main/java/dev/marcospaulo/canalhandia/Ai.java:447src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java:303 inline comment(s) posted below.
@@ -432,2 +444,4 @@if (asker != null) {conversations.remember(askerId, question, clean);if (plugin.playerMemory() != null) {plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean);[MEDIUM] recordTurn (which does a synchronous YAML save to disk) runs before lastAnswer is set and before the answer is delivered; if save() throws IllegalStateException on a disk error, the already-paid-for answer is never sent to the player and /ia corrigir state is lost.
Fix: Move recordTurn after the answer is delivered and lastAnswer is set, or wrap it in a try/catch so memory persistence cannot swallow the chat reply.
🪙 ~792 tok (40% · attributed output)
@@ -135,0 +146,4 @@}String uuidStr = who.uuid();List<DeathLog.Entry> deaths = plugin.deathLog().forPlayer(uuidStr);List<Note> notes = plugin.notes().visibleTo(uuidStr, null, "");[HIGH] lugares_jogador calls notes.visibleTo(targetUuid, null, ""), which returns the target player's PRIVATE notes (visibleTo is PUBLICA || authorId==viewer), and that text is returned to the model whose context leaves the server for MiniMax — a direct violation of the Notes.publicSummary invariant that private notes must never reach the third-party AI.
Fix: Restrict to PUBLICA notes authored by the target player only, so no private text ever enters the tool response.
🪙 ~792 tok (40% · attributed output)
@@ -0,0 +27,4 @@}}assertTrue(foundLugares, "lugares_jogador must be defined in tools schema");}[LOW] ToolsTest only asserts the lugares_jogador schema and the unknown/malformed paths; there is no test exercising playerPlaces execution, which is the path that would have caught the private-note leak in the tool result.
Fix: Add a unit test that stubs Notes/DeathLog/OfflineStats and asserts playerPlaces returns only public notes and formats deaths/places correctly.
🪙 ~375 tok (19% · attributed output)
✅ Fixes applied in commit
2220f11Tools.java: Filteredlugares_jogadorto strictly includeNote.Scope.PUBLICAnotes authored by the target player only. Private notes are completely excluded from entering LLM context.Ai.java: MovedlastAnswerassignment before memory recording and wrappedplayerMemory.recordTurn()in atry-catchblock so disk persistence failures can never drop delivered player answers or/ia corrigirstate.ToolsTest.java: AddedformatPlayerPlacesunit tests asserting private note filtering, author filtering, empty fallback handling, and death formatting.All 331 tests passing.
🤖 AI Review · pragent pilot · glm-5.2:cloud ·
2220f11eAdds Judite/Narrador personas, per-player persona selection persisted to ia-memoria.yml, a sliding-window PlayerMemory, a lugares_jogador tool, and persona-tagged chat output. Tier: full (≈312 lines, 13 files, touches AI/persistence). No Java toolchain in sandbox so mvn/vet skipped; findings grounded in reading changed files and their callers. Risk is medium: PlayerMemory.save() does a synchronous full-file YAML write on the main server thread for every /ia answer (tick lag scaling with player count), the 'key facts' feature is inert because addFact is never called from production code, and the spec's own AiTagTest/EventTest acceptance criteria are unmet. 3 medium.
🔋 AI usage
glm-5.2:cloud· engine: opencode · agent steps: 36 · duration: 251.0ssrc/main/java/dev/marcospaulo/canalhandia/Ai.java:449src/main/java/dev/marcospaulo/canalhandia/PlayerMemory.java:184src/main/java/dev/marcospaulo/canalhandia/Ai.java:4803 inline comment(s) posted below.
@@ -433,1 +446,4 @@conversations.remember(askerId, question, clean);if (plugin.playerMemory() != null) {try {plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean);[MEDIUM] recordTurn() -> PlayerMemory.save() performs a synchronous full ia-memoria.yml write on the main server thread for every /ia answer (deliver runs via runTask), blocking the tick loop; cost scales with total players in the file.
Fix: Move the save off the main thread (async scheduler task, or a dirty-flag + periodic flush like Notes/DeathLog), or batch writes so a single answer doesn't rewrite every player's entry.
📎 ref: https://bukkit.fandom.com/wiki/Scheduler_Programming
🪙 ~4360 tok (32% · attributed output)
@@ -474,3 +479,2 @@* five separate IA replies.*/private Component style(String answer, String question, Settings settings, boolean bedrock, boolean firstLine) {private Component style(String answer, String question, Persona persona, Settings settings, boolean bedrock, boolean firstLine) {[MEDIUM] The dynamic persona tag rendering in style()/deliver() and the new persona-aware event hooks (Achievements, onJoinWelcome, death streak) have no test coverage; the spec's acceptance criteria name AiTagTest and EventTest but neither file exists.
Fix: Add tests asserting the rendered tag/hover use persona.displayTag()/tagColor() for each persona, and that saySomething(...,persona) composes with the passed persona rather than the global default.
🪙 ~4668 tok (34% · attributed output)
@@ -0,0 +181,4 @@save();}void addFact(UUID uuid, String name, String fact) {[MEDIUM] addFact() is never invoked from any production code path (only tests), so the 'Fatos conhecidos' feature is inert: /ia status never lists facts and formatContext() never injects any, despite spec/tasks.md marking the feature complete.
Fix: Either wire fact extraction (e.g. have the AI tool path or a post-answer step call addFact), or remove the facts storage/UI until the extraction path exists so the shipped feature matches the spec.
🪙 ~4572 tok (34% · attributed output)
✅ Review #81 Resolved in commit
998757dPlayerMemory.save()now dispatches full YAML file writes asynchronously via a dedicated single-thread I/O executor withflush()support, preventing server tick loop blocking.extractHeuristicFact) on conversational turns (e.g. "minha base", "estou construindo", "meu plano") and added/ia lembrar <fato>command for manual fact recording.AiTagTest.java(testing persona tag styling, Bedrock fallback, hover metadata) andEventTest.java(testing per-player persona event resolution and prompt composition).All 341 tests passing.
🤖 AI Review · pragent pilot · glm-5.2:cloud ·
998757d6Tier: full (≈312 lines, 13 files, touches AI/persistence). No Java toolchain in sandbox so mvn/javac skipped; findings grounded in reading changed files and callers (Canalhandia.onDisable, Notes.save, Note.visibleTo, Ai.askedToday). Adds Judite/Narrador personas, per-player persona selection persisted to ia-memoria.yml, a sliding-window PlayerMemory, a lugares_jogador tool, and persona-tagged chat output. Overall risk is medium. Prior reviews already covered the lugares_jogador privacy leak (now fixed to PUBLICA + author-scoped), the recordTurn exception swallowing, and a missing-tests note. This pass found two new issues: onDisable() never flushes PlayerMemory's async write executor, so persona/memory changes near shutdown are lost; and extractHeuristicFact indexes a trimmed/lowercased copy but slices the original-case/untrimmed string, garbling auto-extracted facts when the question has leading whitespace. 1 high, 1 medium.
🔋 AI usage
glm-5.2:cloud· engine: opencode · agent steps: 19 · duration: 179.1ssrc/main/java/dev/marcospaulo/canalhandia/Canalhandia.java:182src/main/java/dev/marcospaulo/canalhandia/PlayerMemory.java:2161 inline comment(s) posted below.
src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java:182— onDisable() never flushes PlayerMemory, whose saves run on a daemon single-thread executor; on any normal stop/restart the queued YAML writes for recent persona changes and recorded turns are dropped, silently losing the persistence this feature exists to provide. — fix: Call playerMemory.flush() (and ideally io.shutdown with a short await) inside onDisable before returning.