feat(ia): add Judite & Narrador personas, per-player AI selection, persistent memory, and event reactivity #3

Merged
masi merged 3 commits from feat/ia-personalities-and-memory into main 2026-08-18 22:52:03 +00:00
Contributor

Summary

  • Add JUDITE (SAC/telemarketing) and NARRADOR (fantasy narrator) personas.
  • Support per-player AI selection via /ia persona <nome> with disk persistence in ia-memoria.yml.
  • Replace [IA] tag with dynamic persona tag matching player preference (e.g. [Judite], [Zoeiro]).
  • Implement PlayerMemory for compressed sliding-window conversation history and key facts.
  • Add lugares_jogador tool for location and death history grounding.
  • Expand event commentary for joins, death streaks, and achievement unlocks.
  • 329 unit tests passing (100%).
## Summary - Add `JUDITE` (SAC/telemarketing) and `NARRADOR` (fantasy narrator) personas. - Support per-player AI selection via `/ia persona <nome>` with disk persistence in `ia-memoria.yml`. - Replace `[IA]` tag with dynamic persona tag matching player preference (e.g. `[Judite]`, `[Zoeiro]`). - Implement `PlayerMemory` for compressed sliding-window conversation history and key facts. - Add `lugares_jogador` tool for location and death history grounding. - Expand event commentary for joins, death streaks, and achievement unlocks. - 329 unit tests passing (100%).
masi added 1 commit 2026-08-18 22:07:25 +00:00
masi added the AI-REVIEWAI-USAGE labels 2026-08-18 22:07:32 +00:00
pragent-bot bot reviewed 2026-08-18 22:10:13 +00:00
pragent-bot bot left a comment

🤖 AI Review · pragent pilot · glm-5.2:cloud · ea558130

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: 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

  • model: glm-5.2:cloud · engine: opencode · agent steps: 16 · duration: 158.3s
  • tokens: 749914 in · 1959 out · 0 reasoning · cache 0 read / 0 write → 751873 total
  • est. cost: $0.00 (on-network glm-5.2:cloud via headroom — no per-token charge)
  • scope: whole-repo checkout at head sha (agent can read any file + run linters, not just the diff) — input tokens include files read beyond the diff
  • per-comment tokens are attributed (one model pass produces all findings; output split by each finding's body weight)
severity location ≈out tok %
HIGH src/main/java/dev/marcospaulo/canalhandia/Tools.java:149 792 40%
MEDIUM src/main/java/dev/marcospaulo/canalhandia/Ai.java:447 792 40%
LOW src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java:30 375 19%

3 inline comment(s) posted below.

🤖 **AI Review** · pragent pilot · glm-5.2:cloud · `ea558130` 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: 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 - model: `glm-5.2:cloud` · engine: opencode · agent steps: 16 · duration: 158.3s - tokens: 749914 in · 1959 out · 0 reasoning · cache 0 read / 0 write → 751873 total - est. cost: $0.00 (on-network glm-5.2:cloud via headroom — no per-token charge) - scope: whole-repo checkout at head sha (agent can read any file + run linters, not just the diff) — input tokens include files read beyond the diff - per-comment tokens are *attributed* (one model pass produces all findings; output split by each finding's body weight) | severity | location | ≈out tok | % | |---|---|---:|---:| | HIGH | `src/main/java/dev/marcospaulo/canalhandia/Tools.java:149` | 792 | 40% | | MEDIUM | `src/main/java/dev/marcospaulo/canalhandia/Ai.java:447` | 792 | 40% | | LOW | `src/test/java/dev/marcospaulo/canalhandia/ToolsTest.java:30` | 375 | 19% | _3 inline comment(s) posted below._ <!-- pragent:sha=ea55813019ff1f9cf0e19cd3197aa55330a167e9 -->
@@ -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.

if (plugin.playerMemory() != null) {
                try {
                    plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean);
                } catch (RuntimeException ignored) {
                    plugin.getLogger().warning("Falha ao gravar memória da IA para " + asker.getName());
                }
            }

🪙 ~792 tok (40% · attributed output)

**[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. ```java if (plugin.playerMemory() != null) { try { plugin.playerMemory().recordTurn(askerId, asker.getName(), question, clean); } catch (RuntimeException ignored) { plugin.getLogger().warning("Falha ao gravar memória da IA para " + asker.getName()); } } ``` 🪙 ~792 tok (40% · attributed output)
masi marked this conversation as resolved
@@ -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.

List<Note> notes = new java.util.ArrayList<>();
        for (Note n : plugin.notes().visibleTo(uuidStr, null, "")) {
            if (n.scope() == Note.Scope.PUBLICA
                    && n.authorId() != null && n.authorId().equals(uuidStr)) {
                notes.add(n);
            }
        }

🪙 ~792 tok (40% · attributed output)

**[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. ```java List<Note> notes = new java.util.ArrayList<>(); for (Note n : plugin.notes().visibleTo(uuidStr, null, "")) { if (n.scope() == Note.Scope.PUBLICA && n.authorId() != null && n.authorId().equals(uuidStr)) { notes.add(n); } } ``` 🪙 ~792 tok (40% · attributed output)
masi marked this conversation as resolved
@@ -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)

**[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)
masi marked this conversation as resolved
masi added 1 commit 2026-08-18 22:15:05 +00:00
Author
Contributor

Fixes applied in commit 2220f11

  1. [HIGH] Notes Privacy in Tools.java: Filtered lugares_jogador to strictly include Note.Scope.PUBLICA notes authored by the target player only. Private notes are completely excluded from entering LLM context.
  2. [MEDIUM] Persistence Resilience in Ai.java: Moved lastAnswer assignment before memory recording and wrapped playerMemory.recordTurn() in a try-catch block so disk persistence failures can never drop delivered player answers or /ia corrigir state.
  3. [LOW] Test Coverage in ToolsTest.java: Added formatPlayerPlaces unit tests asserting private note filtering, author filtering, empty fallback handling, and death formatting.

All 331 tests passing.

### ✅ Fixes applied in commit `2220f11` 1. **[HIGH] Notes Privacy in `Tools.java`:** Filtered `lugares_jogador` to strictly include `Note.Scope.PUBLICA` notes authored by the target player only. Private notes are completely excluded from entering LLM context. 2. **[MEDIUM] Persistence Resilience in `Ai.java`:** Moved `lastAnswer` assignment before memory recording and wrapped `playerMemory.recordTurn()` in a `try-catch` block so disk persistence failures can never drop delivered player answers or `/ia corrigir` state. 3. **[LOW] Test Coverage in `ToolsTest.java`:** Added `formatPlayerPlaces` unit tests asserting private note filtering, author filtering, empty fallback handling, and death formatting. All 331 tests passing.
pragent-bot bot reviewed 2026-08-18 22:19:20 +00:00
pragent-bot bot left a comment

🤖 AI Review · pragent pilot · glm-5.2:cloud · 2220f11e

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. 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

  • model: glm-5.2:cloud · engine: opencode · agent steps: 36 · duration: 251.0s
  • tokens: 1746493 in · 13600 out · 0 reasoning · cache 0 read / 0 write → 1760093 total
  • est. cost: $0.00 (on-network glm-5.2:cloud via headroom — no per-token charge)
  • scope: whole-repo checkout at head sha (agent can read any file + run linters, not just the diff) — input tokens include files read beyond the diff
  • per-comment tokens are attributed (one model pass produces all findings; output split by each finding's body weight)
severity location ≈out tok %
MEDIUM src/main/java/dev/marcospaulo/canalhandia/Ai.java:449 4360 32%
MEDIUM src/main/java/dev/marcospaulo/canalhandia/PlayerMemory.java:184 4572 34%
MEDIUM src/main/java/dev/marcospaulo/canalhandia/Ai.java:480 4668 34%

3 inline comment(s) posted below.

🤖 **AI Review** · pragent pilot · glm-5.2:cloud · `2220f11e` 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. 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 - model: `glm-5.2:cloud` · engine: opencode · agent steps: 36 · duration: 251.0s - tokens: 1746493 in · 13600 out · 0 reasoning · cache 0 read / 0 write → 1760093 total - est. cost: $0.00 (on-network glm-5.2:cloud via headroom — no per-token charge) - scope: whole-repo checkout at head sha (agent can read any file + run linters, not just the diff) — input tokens include files read beyond the diff - per-comment tokens are *attributed* (one model pass produces all findings; output split by each finding's body weight) | severity | location | ≈out tok | % | |---|---|---:|---:| | MEDIUM | `src/main/java/dev/marcospaulo/canalhandia/Ai.java:449` | 4360 | 32% | | MEDIUM | `src/main/java/dev/marcospaulo/canalhandia/PlayerMemory.java:184` | 4572 | 34% | | MEDIUM | `src/main/java/dev/marcospaulo/canalhandia/Ai.java:480` | 4668 | 34% | _3 inline comment(s) posted below._ <!-- pragent:sha=2220f11e6495a0715665663f1377491e122a8cdd -->
@@ -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)

**[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)
masi marked this conversation as resolved
@@ -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)

**[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)
masi marked this conversation as resolved
@@ -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)

**[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)
masi marked this conversation as resolved
masi added 1 commit 2026-08-18 22:42:51 +00:00
Author
Contributor

Review #81 Resolved in commit 998757d

  1. [MEDIUM] Asynchronous Memory Persistence: PlayerMemory.save() now dispatches full YAML file writes asynchronously via a dedicated single-thread I/O executor with flush() support, preventing server tick loop blocking.
  2. [MEDIUM] Fact Extraction & Command Wiring: Added heuristic automatic fact extraction (extractHeuristicFact) on conversational turns (e.g. "minha base", "estou construindo", "meu plano") and added /ia lembrar <fato> command for manual fact recording.
  3. [MEDIUM] Test Suite Expansion: Created AiTagTest.java (testing persona tag styling, Bedrock fallback, hover metadata) and EventTest.java (testing per-player persona event resolution and prompt composition).

All 341 tests passing.

### ✅ Review #81 Resolved in commit `998757d` 1. **[MEDIUM] Asynchronous Memory Persistence:** `PlayerMemory.save()` now dispatches full YAML file writes asynchronously via a dedicated single-thread I/O executor with `flush()` support, preventing server tick loop blocking. 2. **[MEDIUM] Fact Extraction & Command Wiring:** Added heuristic automatic fact extraction (`extractHeuristicFact`) on conversational turns (e.g. "minha base", "estou construindo", "meu plano") and added `/ia lembrar <fato>` command for manual fact recording. 3. **[MEDIUM] Test Suite Expansion:** Created `AiTagTest.java` (testing persona tag styling, Bedrock fallback, hover metadata) and `EventTest.java` (testing per-player persona event resolution and prompt composition). All 341 tests passing.
pragent-bot bot reviewed 2026-08-18 22:45:53 +00:00
pragent-bot bot left a comment

🤖 AI Review · pragent pilot · glm-5.2:cloud · 998757d6

Tier: 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

  • model: glm-5.2:cloud · engine: opencode · agent steps: 19 · duration: 179.1s
  • tokens: 880127 in · 9659 out · 0 reasoning · cache 0 read / 0 write → 889786 total
  • est. cost: $0.00 (on-network glm-5.2:cloud via headroom — no per-token charge)
  • scope: whole-repo checkout at head sha (agent can read any file + run linters, not just the diff) — input tokens include files read beyond the diff
  • per-comment tokens are attributed (one model pass produces all findings; output split by each finding's body weight)
severity location ≈out tok %
HIGH src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java:182 4313 45%
MEDIUM src/main/java/dev/marcospaulo/canalhandia/PlayerMemory.java:216 5346 55%

1 inline comment(s) posted below.

  • [HIGH] 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.
🤖 **AI Review** · pragent pilot · glm-5.2:cloud · `998757d6` Tier: 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 - model: `glm-5.2:cloud` · engine: opencode · agent steps: 19 · duration: 179.1s - tokens: 880127 in · 9659 out · 0 reasoning · cache 0 read / 0 write → 889786 total - est. cost: $0.00 (on-network glm-5.2:cloud via headroom — no per-token charge) - scope: whole-repo checkout at head sha (agent can read any file + run linters, not just the diff) — input tokens include files read beyond the diff - per-comment tokens are *attributed* (one model pass produces all findings; output split by each finding's body weight) | severity | location | ≈out tok | % | |---|---|---:|---:| | HIGH | `src/main/java/dev/marcospaulo/canalhandia/Canalhandia.java:182` | 4313 | 45% | | MEDIUM | `src/main/java/dev/marcospaulo/canalhandia/PlayerMemory.java:216` | 5346 | 55% | _1 inline comment(s) posted below._ - **[HIGH]** `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. <!-- pragent:sha=998757d6108a1993e90555f87eb49f47a14a2a2f -->
masi merged commit 01e6c28fef into main 2026-08-18 22:52:03 +00:00
masi deleted branch feat/ia-personalities-and-memory 2026-08-18 22:52:10 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gitea_admin/canalhandia#3