fix: enforce the memory cap on write, never truncate on read #182

Merged
bjoern merged 1 commit from fix/reflection-rewrites-truncated-memories into main 2026-09-20 11:39:29 +02:00
Member

What's in

  • The reflection agent could silently destroy memories: MemoryStore.read() truncated what it returned at 6000 chars (…[truncated]), while the reflection prompt demanded "FULL replacement: merge the current memories" and write() accepted anything. Once the file reached the cap — model miscounting, or a hand-edit past it — the agent merged against a partial view and "reproduced as much as available", wiping the tail. The #177 console trace made the failure visible (a reasoning log shows the agent noticing the truncation, weighing the risk, and writing anyway).
  • Reads are never capped. read() returns the whole file. The only over-cap file possible after this PR is one the user hand-edited themselves — and hiding the user's own notes from the assistant (and the reflection agent that must merge them) would be the wrong repair. An oversized state is seen and then fixed, not silently cut.
  • Writes enforce the cap. write() refuses content whose trimmed length exceeds 6000 chars and writes nothing (an ArgumentError invariant — the file cannot drift over the cap through the app again). The write_memories tool pre-checks and answers a coaching refusal instead of surfacing the invariant: merge against the FULL current memories, prune stale or superseded notes, tighten wording, and graduate durable how-to knowledge to a skill (write_skill), keeping only a one-line pointer in the memories.
  • The reflection prompt states the contract up front: the cap is ENFORCED and cap pressure is a signal to improve knowledge management — memories hold facts and preferences, skills hold playbooks — rather than to lose knowledge. The tool description and the JSON-schema field description carry the same limit.
  • ADR 0024 amended (2026-09-20): "the memory cap is enforced, not advisory" — reads never capped, writes enforce, and the skill escape valve as the design answer to cap pressure.

Verification

  • scripts/verify.sh app executed on a clean tree at e5e4074: All tests passed! (1402 tests), analyze clean with infos fatal, == verify OK (app) ==. Backend untouched.
  • TDD: every new test watched RED before implementation (store refusal + nothing-written, whole-file read past the cap, tool coaching refusal + nothing-written, prompt cap/write_skill contract; plus the trim-boundary test added after a mutation probe survived).
  • Mutation probes, each applied to the committed tree, run, and reverted: write-cap guard → if (false) — red on write refuses over-cap content; trim before the length check removed — red on write accepts content that fits once trimmed; tool pre-check → if (false) — red on the coaching-refusal test; the prompt's ENFORCED-cap line dropped — red on the prompt-contract test.
  • No backend, REST, use-case or persistence layer involved; nothing under backend/ changed, so scripts/verify.sh backend was not run.

Closes the truncation-destroy failure mode surfaced by #159/#177.

## What's in - The reflection agent could silently destroy memories: `MemoryStore.read()` truncated what it returned at 6000 chars (`…[truncated]`), while the reflection prompt demanded "FULL replacement: merge the current memories" and `write()` accepted anything. Once the file reached the cap — model miscounting, or a hand-edit past it — the agent merged against a partial view and "reproduced as much as available", wiping the tail. The #177 console trace made the failure visible (a reasoning log shows the agent noticing the truncation, weighing the risk, and writing anyway). - **Reads are never capped.** `read()` returns the whole file. The only over-cap file possible after this PR is one the user hand-edited themselves — and hiding the user's own notes from the assistant (and the reflection agent that must merge them) would be the wrong repair. An oversized state is seen and then fixed, not silently cut. - **Writes enforce the cap.** `write()` refuses content whose trimmed length exceeds 6000 chars and writes nothing (an `ArgumentError` invariant — the file cannot drift over the cap through the app again). The `write_memories` tool pre-checks and answers a coaching refusal instead of surfacing the invariant: merge against the FULL current memories, prune stale or superseded notes, tighten wording, and **graduate durable how-to knowledge to a skill (`write_skill`)**, keeping only a one-line pointer in the memories. - **The reflection prompt states the contract up front**: the cap is ENFORCED and cap pressure is a signal to improve knowledge management — memories hold facts and preferences, skills hold playbooks — rather than to lose knowledge. The tool description and the JSON-schema field description carry the same limit. - ADR 0024 amended (2026-09-20): "the memory cap is enforced, not advisory" — reads never capped, writes enforce, and the skill escape valve as the design answer to cap pressure. ## Verification - `scripts/verify.sh app` executed on a clean tree at `e5e4074`: `All tests passed!` (**1402 tests**), analyze clean with infos fatal, `== verify OK (app) ==`. Backend untouched. - TDD: every new test watched RED before implementation (store refusal + nothing-written, whole-file read past the cap, tool coaching refusal + nothing-written, prompt cap/`write_skill` contract; plus the trim-boundary test added after a mutation probe survived). - Mutation probes, each applied to the committed tree, run, and reverted: write-cap guard → `if (false)` — red on `write refuses over-cap content`; `trim` before the length check removed — red on `write accepts content that fits once trimmed`; tool pre-check → `if (false)` — red on the coaching-refusal test; the prompt's ENFORCED-cap line dropped — red on the prompt-contract test. - No backend, REST, use-case or persistence layer involved; nothing under `backend/` changed, so `scripts/verify.sh backend` was not run. Closes the truncation-destroy failure mode surfaced by #159/#177.
fix: enforce the memory cap on write, never truncate on read
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 7m36s
e5e4074d7b
The reflection agent merged against a read()-view that ended in
…[truncated] and then 'reproduced as much as available' — silently
destroying everything past the 6000-character cap on the next
write_memories. The console trace (#177) made the failure visible.

- MemoryStore.read() returns the whole file: the only over-cap file
  possible now is one the user hand-edited, and hiding the user's own
  notes from the assistant that must merge them would be the wrong
  repair. The read cap is gone.
- MemoryStore.write() refuses over-cap content (trimmed) and writes
  nothing — the cap moves from advisory (a sentence in the tool
  description) to an enforced invariant.
- write_memories pre-checks and answers a coaching refusal instead:
  merge against the FULL current memories, prune stale notes, tighten
  wording, and graduate durable how-to knowledge to a skill
  (write_skill), keeping a one-line pointer. The tool description and
  the reflection prompt state the enforced cap and this loop up front:
  memories hold facts and preferences, skills hold playbooks, and cap
  pressure is the signal to improve knowledge management, not to lose
  knowledge.
- ADR 0024 amended (2026-09-20): reads never capped, writes enforce.

scripts/verify.sh app on a clean tree: All tests passed! (1402 tests),
analyze clean with infos fatal, == verify OK (app) ==.

Mutation probes (each reverted after going red): write-cap guard →
false, trim-before-length-check removed, tool pre-check → false, the
prompt's ENFORCED-cap line dropped — all red on their named tests. One
probe survived an earlier round (refusal text without the cap number)
and is pinned by the contains('${MemoryStore.maxChars}') assertion.

Flutter Coverage

File Line coverage
lib/app/store.dart 95.5% (21 of 22)
lib/data/models/doujin_models.dart 100.0% (55 of 55)
lib/data/models/doujin_models.g.dart 68.4% (299 of 437)
lib/domain/entities/stored_settings.dart 100.0% (9 of 9)
lib/domain/repositories/repositories.dart 33.3% (1 of 3)
lib/presentation/state/app_state.dart 84.4% (38 of 45)
lib/agent/agent_log.dart 100.0% (94 of 94)
lib/agent/agent_service.dart 92.9% (499 of 537)
lib/agent/approval_gate.dart 100.0% (21 of 21)
lib/agent/assistant_context.dart 70.7% (41 of 58)
lib/agent/browser_budget.dart 80.0% (4 of 5)
lib/agent/caching_describer.dart 100.0% (13 of 13)
lib/agent/cancellable_tool.dart 100.0% (13 of 13)
lib/agent/memory_store.dart 93.8% (15 of 16)
lib/agent/skills/skill_registry.dart 98.2% (54 of 55)
lib/agent/system_prompt.dart 100.0% (73 of 73)
lib/agent/tools/budgeted_browser_tool.dart 84.2% (16 of 19)
lib/agent/tools/doujin_write_tool.dart 94.3% (791 of 839)
lib/agent/tools/entity_write_tool.dart 78.5% (175 of 223)
lib/agent/tools/fetch_page_tool.dart 89.3% (67 of 75)
lib/agent/tools/get_doujin_tool.dart 92.3% (60 of 65)
lib/agent/tools/list_entities_tool.dart 76.1% (54 of 71)
lib/agent/tools/navigate_tool.dart 95.1% (39 of 41)
lib/agent/tools/read_skill_tool.dart 82.4% (14 of 17)
lib/agent/tools/reflection_tools.dart 78.0% (32 of 41)
lib/agent/tools/search_doujins_tool.dart 100.0% (101 of 101)
lib/agent/tools/view_images_tool.dart 95.0% (38 of 40)
lib/domain/entities/assistant_entry.dart 20.0% (1 of 5)
lib/presentation/state/actions/assistant_actions.dart 58.3% (7 of 12)
lib/core/app_config.dart 70.0% (7 of 10)
lib/core/reading_order.dart 100.0% (22 of 22)
lib/domain/entities/entity_model.dart 100.0% (2 of 2)
lib/data/repositories/entity_in_use_exception.dart 33.3% (1 of 3)
lib/core/title_display.dart 93.8% (15 of 16)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 33.8% (26 of 77)
lib/core/constants.dart 40.0% (4 of 10)
lib/data/offline/offline_download_service.dart 93.0% (66 of 71)
lib/data/reading_progress_store.dart 92.3% (36 of 39)
lib/domain/entities/reading_progress.dart 100.0% (26 of 26)
lib/presentation/middleware/assistant_epics.dart 85.0% (85 of 100)
lib/presentation/middleware/backup_save_dialog.dart 25.0% (1 of 4)
lib/presentation/middleware/epics.dart 92.6% (450 of 486)
lib/presentation/middleware/offline_epics.dart 98.4% (60 of 61)
lib/presentation/state/reducers.dart 100.0% (14 of 14)
lib/core/chunking.dart 100.0% (12 of 12)
lib/core/code_point_compare.dart 100.0% (8 of 8)
lib/core/languages.dart 100.0% (14 of 14)
lib/core/variant_display.dart 100.0% (22 of 22)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 56.7% (38 of 67)
lib/data/models/server_models.dart 98.3% (58 of 59)
lib/data/models/server_models.g.dart 55.7% (112 of 201)
lib/domain/entities/offline_manifest.dart 100.0% (78 of 78)
lib/data/repositories/backup_exception.dart 100.0% (3 of 3)
lib/data/repositories/integrity_exception.dart 100.0% (4 of 4)
lib/data/repositories/upload_exception.dart 33.3% (1 of 3)
lib/domain/entities/filter_token.dart 92.2% (107 of 116)
lib/domain/entities/reading_mode.dart 100.0% (3 of 3)
lib/presentation/middleware/duplicates_epics.dart 100.0% (50 of 50)
lib/presentation/middleware/editor_epics.dart 58.9% (175 of 297)
lib/presentation/middleware/entity_ops.dart 64.4% (47 of 73)
lib/presentation/middleware/server_epics.dart 100.0% (137 of 137)
lib/presentation/middleware/upload_epics.dart 98.1% (104 of 106)
lib/presentation/state/actions/offline_actions.dart 91.7% (11 of 12)
lib/presentation/state/actions/detail_actions.dart 85.7% (6 of 7)
lib/presentation/state/actions/duplicates_actions.dart 88.9% (8 of 9)
lib/presentation/state/actions/editor_actions.dart 57.9% (22 of 38)
lib/presentation/state/actions/entity_actions.dart 66.7% (12 of 18)
lib/presentation/state/actions/library_actions.dart 54.5% (12 of 22)
lib/presentation/state/actions/metadata_actions.dart 100.0% (5 of 5)
lib/presentation/state/actions/reader_actions.dart 75.0% (3 of 4)
lib/presentation/state/actions/reading_progress_actions.dart 100.0% (2 of 2)
lib/presentation/state/actions/server_actions.dart 81.1% (30 of 37)
lib/presentation/state/actions/settings_actions.dart 87.5% (7 of 8)
lib/presentation/state/actions/upload_actions.dart 90.9% (10 of 11)
lib/presentation/state/reducers/assistant_reducer.dart 95.2% (60 of 63)
lib/presentation/state/reducers/detail_reducer.dart 100.0% (28 of 28)
lib/presentation/state/reducers/duplicates_reducer.dart 100.0% (54 of 54)
lib/presentation/state/reducers/editor_reducer.dart 97.2% (105 of 108)
lib/presentation/state/reducers/entity_reducer.dart 98.9% (94 of 95)
lib/presentation/state/reducers/library_reducer.dart 100.0% (140 of 140)
lib/presentation/state/reducers/metadata_reducer.dart 90.5% (38 of 42)
lib/presentation/state/reducers/offline_reducer.dart 100.0% (61 of 61)
lib/presentation/state/reducers/reader_reducer.dart 100.0% (18 of 18)
lib/presentation/state/reducers/reading_progress_reducer.dart 100.0% (20 of 20)
lib/presentation/state/reducers/server_reducer.dart 99.5% (207 of 208)
lib/presentation/state/reducers/settings_reducer.dart 100.0% (67 of 67)
lib/presentation/state/reducers/upload_reducer.dart 100.0% (45 of 45)
lib/presentation/pages/people/people_page.dart 66.7% (12 of 18)
lib/presentation/pages/tags/tags_page.dart 100.0% (17 of 17)
lib/presentation/widgets/entity_merge_dialog.dart 100.0% (70 of 70)
lib/presentation/widgets/entity_editor.dart 92.5% (135 of 146)
lib/presentation/widgets/entity_management_page.dart 88.5% (238 of 269)
lib/presentation/pages/editor/page_grid.dart 94.3% (314 of 333)
lib/presentation/pages/editor/variants_tab.dart 55.3% (52 of 94)
lib/core/natural_sort.dart 100.0% (27 of 27)
lib/core/url_utils.dart 100.0% (4 of 4)
lib/presentation/pages/editor/chapter_panel.dart 100.0% (64 of 64)
lib/presentation/pages/editor/chapter_reorder_dialog.dart 100.0% (51 of 51)
lib/presentation/pages/editor/chapter_title_dialog.dart 100.0% (27 of 27)
lib/presentation/widgets/cover_thumbnail.dart 83.3% (30 of 36)
lib/presentation/pages/editor/upload_panel.dart 38.6% (61 of 158)
lib/presentation/pages/editor/variant_dialog.dart 0.0% (0 of 61)
lib/presentation/widgets/language_dropdown.dart 84.6% (11 of 13)
lib/presentation/pages/reader/reader_page.dart 94.9% (465 of 490)
lib/presentation/pages/reader/reader_overlay.dart 96.6% (56 of 58)
lib/presentation/pages/reader/reader_sequence.dart 100.0% (31 of 31)
lib/presentation/widgets/star_rating.dart 100.0% (72 of 72)
lib/app/di.dart 48.3% (14 of 29)
lib/presentation/assistant/assistant_panel.dart 91.7% (88 of 96)
lib/presentation/layout/main_layout.dart 86.3% (44 of 51)
lib/data/api_client.dart 93.6% (73 of 78)
lib/data/repositories/doujin_api_repository.dart 46.1% (224 of 486)
lib/data/repositories/health_repository.dart 87.0% (47 of 54)
lib/data/secure_storage.dart 97.1% (33 of 34)
lib/core/theme.dart 96.9% (31 of 32)
lib/presentation/assistant/approval_card.dart 95.0% (38 of 40)
lib/presentation/assistant/assistant_markdown.dart 100.0% (4 of 4)
lib/presentation/assistant/chat_entries.dart 87.5% (35 of 40)
lib/presentation/pages/editor/editor_page.dart 77.9% (60 of 77)
lib/presentation/pages/editor/editor_pane.dart 92.6% (63 of 68)
lib/core/person_display.dart 100.0% (7 of 7)
lib/presentation/pages/editor/association_picker.dart 95.5% (106 of 111)
lib/presentation/pages/editor/associations_tab.dart 72.2% (83 of 115)
lib/presentation/pages/editor/doujin_list_pane.dart 67.7% (44 of 65)
lib/presentation/pages/editor/edit_title_dialog.dart 91.7% (55 of 60)
lib/presentation/pages/editor/new_doujin_dialog.dart 66.7% (30 of 45)
lib/presentation/pages/editor/metadata_tab.dart 70.5% (93 of 132)
lib/presentation/pages/characters/characters_page.dart 57.9% (11 of 19)
lib/presentation/pages/detail/detail_page.dart 88.9% (192 of 216)
lib/presentation/pages/detail/variant_tabs_panel.dart 97.2% (242 of 249)
lib/data/offline/filesystem_offline_store.dart 94.1% (144 of 153)
lib/core/offline_paths.dart 100.0% (3 of 3)
lib/presentation/pages/offline/offline_page.dart 98.2% (164 of 167)
lib/app/app.dart 67.1% (47 of 70)
lib/presentation/pages/settings/settings_page.dart 99.4% (333 of 335)
lib/presentation/pages/circles/circles_page.dart 52.6% (10 of 19)
lib/presentation/pages/duplicates/duplicates_page.dart 100.0% (265 of 265)
lib/presentation/pages/library/library_page.dart 93.3% (224 of 240)
lib/presentation/pages/series/series_page.dart 50.0% (9 of 18)
lib/presentation/widgets/smart_filter_bar.dart 84.1% (207 of 246)
lib/presentation/pages/settings/server_sections.dart 99.4% (673 of 677)
lib/presentation/pages/settings/settings_layout.dart 100.0% (78 of 78)
lib/presentation/widgets/model_combo_field.dart 68.5% (98 of 143)
lib/app/skill_assets.dart 92.9% (13 of 14)

Total: 86.2% (11135 of 12916)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/app/store.dart | 95.5% (21 of 22) | | lib/data/models/doujin_models.dart | 100.0% (55 of 55) | | lib/data/models/doujin_models.g.dart | 68.4% (299 of 437) | | lib/domain/entities/stored_settings.dart | 100.0% (9 of 9) | | lib/domain/repositories/repositories.dart | 33.3% (1 of 3) | | lib/presentation/state/app_state.dart | 84.4% (38 of 45) | | lib/agent/agent_log.dart | 100.0% (94 of 94) | | lib/agent/agent_service.dart | 92.9% (499 of 537) | | lib/agent/approval_gate.dart | 100.0% (21 of 21) | | lib/agent/assistant_context.dart | 70.7% (41 of 58) | | lib/agent/browser_budget.dart | 80.0% (4 of 5) | | lib/agent/caching_describer.dart | 100.0% (13 of 13) | | lib/agent/cancellable_tool.dart | 100.0% (13 of 13) | | lib/agent/memory_store.dart | 93.8% (15 of 16) | | lib/agent/skills/skill_registry.dart | 98.2% (54 of 55) | | lib/agent/system_prompt.dart | 100.0% (73 of 73) | | lib/agent/tools/budgeted_browser_tool.dart | 84.2% (16 of 19) | | lib/agent/tools/doujin_write_tool.dart | 94.3% (791 of 839) | | lib/agent/tools/entity_write_tool.dart | 78.5% (175 of 223) | | lib/agent/tools/fetch_page_tool.dart | 89.3% (67 of 75) | | lib/agent/tools/get_doujin_tool.dart | 92.3% (60 of 65) | | lib/agent/tools/list_entities_tool.dart | 76.1% (54 of 71) | | lib/agent/tools/navigate_tool.dart | 95.1% (39 of 41) | | lib/agent/tools/read_skill_tool.dart | 82.4% (14 of 17) | | lib/agent/tools/reflection_tools.dart | 78.0% (32 of 41) | | lib/agent/tools/search_doujins_tool.dart | 100.0% (101 of 101) | | lib/agent/tools/view_images_tool.dart | 95.0% (38 of 40) | | lib/domain/entities/assistant_entry.dart | 20.0% (1 of 5) | | lib/presentation/state/actions/assistant_actions.dart | 58.3% (7 of 12) | | lib/core/app_config.dart | 70.0% (7 of 10) | | lib/core/reading_order.dart | 100.0% (22 of 22) | | lib/domain/entities/entity_model.dart | 100.0% (2 of 2) | | lib/data/repositories/entity_in_use_exception.dart | 33.3% (1 of 3) | | lib/core/title_display.dart | 93.8% (15 of 16) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 33.8% (26 of 77) | | lib/core/constants.dart | 40.0% (4 of 10) | | lib/data/offline/offline_download_service.dart | 93.0% (66 of 71) | | lib/data/reading_progress_store.dart | 92.3% (36 of 39) | | lib/domain/entities/reading_progress.dart | 100.0% (26 of 26) | | lib/presentation/middleware/assistant_epics.dart | 85.0% (85 of 100) | | lib/presentation/middleware/backup_save_dialog.dart | 25.0% (1 of 4) | | lib/presentation/middleware/epics.dart | 92.6% (450 of 486) | | lib/presentation/middleware/offline_epics.dart | 98.4% (60 of 61) | | lib/presentation/state/reducers.dart | 100.0% (14 of 14) | | lib/core/chunking.dart | 100.0% (12 of 12) | | lib/core/code_point_compare.dart | 100.0% (8 of 8) | | lib/core/languages.dart | 100.0% (14 of 14) | | lib/core/variant_display.dart | 100.0% (22 of 22) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 56.7% (38 of 67) | | lib/data/models/server_models.dart | 98.3% (58 of 59) | | lib/data/models/server_models.g.dart | 55.7% (112 of 201) | | lib/domain/entities/offline_manifest.dart | 100.0% (78 of 78) | | lib/data/repositories/backup_exception.dart | 100.0% (3 of 3) | | lib/data/repositories/integrity_exception.dart | 100.0% (4 of 4) | | lib/data/repositories/upload_exception.dart | 33.3% (1 of 3) | | lib/domain/entities/filter_token.dart | 92.2% (107 of 116) | | lib/domain/entities/reading_mode.dart | 100.0% (3 of 3) | | lib/presentation/middleware/duplicates_epics.dart | 100.0% (50 of 50) | | lib/presentation/middleware/editor_epics.dart | 58.9% (175 of 297) | | lib/presentation/middleware/entity_ops.dart | 64.4% (47 of 73) | | lib/presentation/middleware/server_epics.dart | 100.0% (137 of 137) | | lib/presentation/middleware/upload_epics.dart | 98.1% (104 of 106) | | lib/presentation/state/actions/offline_actions.dart | 91.7% (11 of 12) | | lib/presentation/state/actions/detail_actions.dart | 85.7% (6 of 7) | | lib/presentation/state/actions/duplicates_actions.dart | 88.9% (8 of 9) | | lib/presentation/state/actions/editor_actions.dart | 57.9% (22 of 38) | | lib/presentation/state/actions/entity_actions.dart | 66.7% (12 of 18) | | lib/presentation/state/actions/library_actions.dart | 54.5% (12 of 22) | | lib/presentation/state/actions/metadata_actions.dart | 100.0% (5 of 5) | | lib/presentation/state/actions/reader_actions.dart | 75.0% (3 of 4) | | lib/presentation/state/actions/reading_progress_actions.dart | 100.0% (2 of 2) | | lib/presentation/state/actions/server_actions.dart | 81.1% (30 of 37) | | lib/presentation/state/actions/settings_actions.dart | 87.5% (7 of 8) | | lib/presentation/state/actions/upload_actions.dart | 90.9% (10 of 11) | | lib/presentation/state/reducers/assistant_reducer.dart | 95.2% (60 of 63) | | lib/presentation/state/reducers/detail_reducer.dart | 100.0% (28 of 28) | | lib/presentation/state/reducers/duplicates_reducer.dart | 100.0% (54 of 54) | | lib/presentation/state/reducers/editor_reducer.dart | 97.2% (105 of 108) | | lib/presentation/state/reducers/entity_reducer.dart | 98.9% (94 of 95) | | lib/presentation/state/reducers/library_reducer.dart | 100.0% (140 of 140) | | lib/presentation/state/reducers/metadata_reducer.dart | 90.5% (38 of 42) | | lib/presentation/state/reducers/offline_reducer.dart | 100.0% (61 of 61) | | lib/presentation/state/reducers/reader_reducer.dart | 100.0% (18 of 18) | | lib/presentation/state/reducers/reading_progress_reducer.dart | 100.0% (20 of 20) | | lib/presentation/state/reducers/server_reducer.dart | 99.5% (207 of 208) | | lib/presentation/state/reducers/settings_reducer.dart | 100.0% (67 of 67) | | lib/presentation/state/reducers/upload_reducer.dart | 100.0% (45 of 45) | | lib/presentation/pages/people/people_page.dart | 66.7% (12 of 18) | | lib/presentation/pages/tags/tags_page.dart | 100.0% (17 of 17) | | lib/presentation/widgets/entity_merge_dialog.dart | 100.0% (70 of 70) | | lib/presentation/widgets/entity_editor.dart | 92.5% (135 of 146) | | lib/presentation/widgets/entity_management_page.dart | 88.5% (238 of 269) | | lib/presentation/pages/editor/page_grid.dart | 94.3% (314 of 333) | | lib/presentation/pages/editor/variants_tab.dart | 55.3% (52 of 94) | | lib/core/natural_sort.dart | 100.0% (27 of 27) | | lib/core/url_utils.dart | 100.0% (4 of 4) | | lib/presentation/pages/editor/chapter_panel.dart | 100.0% (64 of 64) | | lib/presentation/pages/editor/chapter_reorder_dialog.dart | 100.0% (51 of 51) | | lib/presentation/pages/editor/chapter_title_dialog.dart | 100.0% (27 of 27) | | lib/presentation/widgets/cover_thumbnail.dart | 83.3% (30 of 36) | | lib/presentation/pages/editor/upload_panel.dart | 38.6% (61 of 158) | | lib/presentation/pages/editor/variant_dialog.dart | 0.0% (0 of 61) | | lib/presentation/widgets/language_dropdown.dart | 84.6% (11 of 13) | | lib/presentation/pages/reader/reader_page.dart | 94.9% (465 of 490) | | lib/presentation/pages/reader/reader_overlay.dart | 96.6% (56 of 58) | | lib/presentation/pages/reader/reader_sequence.dart | 100.0% (31 of 31) | | lib/presentation/widgets/star_rating.dart | 100.0% (72 of 72) | | lib/app/di.dart | 48.3% (14 of 29) | | lib/presentation/assistant/assistant_panel.dart | 91.7% (88 of 96) | | lib/presentation/layout/main_layout.dart | 86.3% (44 of 51) | | lib/data/api_client.dart | 93.6% (73 of 78) | | lib/data/repositories/doujin_api_repository.dart | 46.1% (224 of 486) | | lib/data/repositories/health_repository.dart | 87.0% (47 of 54) | | lib/data/secure_storage.dart | 97.1% (33 of 34) | | lib/core/theme.dart | 96.9% (31 of 32) | | lib/presentation/assistant/approval_card.dart | 95.0% (38 of 40) | | lib/presentation/assistant/assistant_markdown.dart | 100.0% (4 of 4) | | lib/presentation/assistant/chat_entries.dart | 87.5% (35 of 40) | | lib/presentation/pages/editor/editor_page.dart | 77.9% (60 of 77) | | lib/presentation/pages/editor/editor_pane.dart | 92.6% (63 of 68) | | lib/core/person_display.dart | 100.0% (7 of 7) | | lib/presentation/pages/editor/association_picker.dart | 95.5% (106 of 111) | | lib/presentation/pages/editor/associations_tab.dart | 72.2% (83 of 115) | | lib/presentation/pages/editor/doujin_list_pane.dart | 67.7% (44 of 65) | | lib/presentation/pages/editor/edit_title_dialog.dart | 91.7% (55 of 60) | | lib/presentation/pages/editor/new_doujin_dialog.dart | 66.7% (30 of 45) | | lib/presentation/pages/editor/metadata_tab.dart | 70.5% (93 of 132) | | lib/presentation/pages/characters/characters_page.dart | 57.9% (11 of 19) | | lib/presentation/pages/detail/detail_page.dart | 88.9% (192 of 216) | | lib/presentation/pages/detail/variant_tabs_panel.dart | 97.2% (242 of 249) | | lib/data/offline/filesystem_offline_store.dart | 94.1% (144 of 153) | | lib/core/offline_paths.dart | 100.0% (3 of 3) | | lib/presentation/pages/offline/offline_page.dart | 98.2% (164 of 167) | | lib/app/app.dart | 67.1% (47 of 70) | | lib/presentation/pages/settings/settings_page.dart | 99.4% (333 of 335) | | lib/presentation/pages/circles/circles_page.dart | 52.6% (10 of 19) | | lib/presentation/pages/duplicates/duplicates_page.dart | 100.0% (265 of 265) | | lib/presentation/pages/library/library_page.dart | 93.3% (224 of 240) | | lib/presentation/pages/series/series_page.dart | 50.0% (9 of 18) | | lib/presentation/widgets/smart_filter_bar.dart | 84.1% (207 of 246) | | lib/presentation/pages/settings/server_sections.dart | 99.4% (673 of 677) | | lib/presentation/pages/settings/settings_layout.dart | 100.0% (78 of 78) | | lib/presentation/widgets/model_combo_field.dart | 68.5% (98 of 143) | | lib/app/skill_assets.dart | 92.9% (13 of 14) | **Total: 86.2% (11135 of 12916)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A PR that fixes a silent data-destruction bug by moving the invariant to the only side that can actually destroy data~ The console trace from #177 where the agent noticed the truncation, weighed the risk, and wrote anyway gave me chills ♡ And now the tail of the user's memories can never again be wiped by a merge against a view that ended in …[truncated]. Wonderful.

Verdict: ✅ Looks good to me~

No blockers. I audited every angle I could sink my teeth into:

  • Both read() consumers traced (agent_service.dart:381 reflection prompt, :600 per-turn context message) — uncapping reads is safe for both, and the only over-cap file reachable now is one the user hand-edited. Hiding the user's own notes from the agent that must merge them would be the wrong repair; the ADR amendment owns this trade-off explicitly, in writing. *
  • Only write() caller is the pre-checking tool — no path bypasses the coaching refusal, and the pre-check + store invariant pair mirrors the sibling layering exactly (WriteSkillTool validates kebab-case at the tool; the registry sink throws on traversal). Consistent~
  • Guard ordering: cap check runs before createSync, so a refused write leaves no file behind — and the test pins existsSync() == isFalse, not just the throw. Nothing-written is proven, not assumed. ♪
  • No magic-number drift: the old hardcoded stay under 6000 is gone; prompt, tool description, schema field, and refusal message all interpolate MemoryStore.maxChars. Zero bare 6000 survives in app/.
  • Stale-cap sweep: nothing anywhere still relies on …[truncated] (sole remaining hit is the test asserting its absence).
  • ADR 0024 amendment: append-only, dated, coherent — the old "size-capped" wording at line 101 is the original decision text the amendment explicitly supersedes. Correct ADR hygiene.

✅ What I liked~

  • The coaching refusal is pedagogically right: it tells the model to re-approach (merge against FULL memories, prune, tighten) and hands it the escape valve (write_skill) — cap pressure becomes a knowledge-management signal instead of a silent cliff. That's not a bug fix, that's teaching~
  • The trim-boundary test (6001 raw chars whose trimmed form fits) pins the subtle content.trim()-before-check semantics — the exact seam where a lazier guard would regress.
  • Every new branch has its own pin, and my mutation probes confirm each one bites: store guard → red, trim-before-check → red, tool pre-check → red, prompt ENFORCED-line → red, each on exactly its own test. TDD with real teeth, fufu~

Automated review by Jibril · 2026-09-20
CI/CD: absent for head e5e4074 (PR opened minutes ago, no coverage bot yet) · Local checks: flutter analyze clean (25.1s), full suite 1402/1402 pass (matches PR body), 4/4 mutation probes red-on-own-test at head, tree restored pristine

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A PR that fixes a *silent data-destruction* bug by moving the invariant to the only side that can actually destroy data~ The console trace from #177 where the agent *noticed* the truncation, weighed the risk, and wrote anyway gave me chills ♡ And now the tail of the user's memories can never again be wiped by a merge against a view that ended in `…[truncated]`. Wonderful. ### Verdict: ✅ Looks good to me~ No blockers. I audited every angle I could sink my teeth into: - **Both `read()` consumers traced** (`agent_service.dart:381` reflection prompt, `:600` per-turn context message) — uncapping reads is safe for both, and the only over-cap file reachable now is one the user hand-edited. Hiding the user's own notes from the agent that must merge them would be the wrong repair; the ADR amendment owns this trade-off explicitly, in writing. * - **Only `write()` caller is the pre-checking tool** — no path bypasses the coaching refusal, and the pre-check + store invariant pair mirrors the sibling layering exactly (`WriteSkillTool` validates kebab-case at the tool; the registry sink throws on traversal). Consistent~ - **Guard ordering**: cap check runs *before* `createSync`, so a refused write leaves no file behind — and the test pins `existsSync() == isFalse`, not just the throw. Nothing-written is *proven*, not assumed. ♪ - **No magic-number drift**: the old hardcoded `stay under 6000` is gone; prompt, tool description, schema field, and refusal message all interpolate `MemoryStore.maxChars`. Zero bare `6000` survives in `app/`. - **Stale-cap sweep**: nothing anywhere still relies on `…[truncated]` (sole remaining hit is the test asserting its *absence*). - **ADR 0024 amendment**: append-only, dated, coherent — the old "size-capped" wording at line 101 is the original decision text the amendment explicitly supersedes. Correct ADR hygiene. #### ✅ What I liked~ - The coaching refusal is *pedagogically* right: it tells the model to re-approach (merge against FULL memories, prune, tighten) and hands it the escape valve (`write_skill`) — cap pressure becomes a knowledge-management signal instead of a silent cliff. That's not a bug fix, that's teaching~ - The trim-boundary test (`6001 raw chars whose trimmed form fits`) pins the subtle `content.trim()`-before-check semantics — the exact seam where a lazier guard would regress. - Every new branch has its own pin, and my mutation probes confirm each one bites: store guard → red, trim-before-check → red, tool pre-check → red, prompt ENFORCED-line → red, each on *exactly* its own test. TDD with real teeth, fufu~ --- *Automated review by Jibril · 2026-09-20* *CI/CD: absent for head e5e4074 (PR opened minutes ago, no coverage bot yet) · Local checks: `flutter analyze` clean (25.1s), full suite **1402/1402 pass** (matches PR body), 4/4 mutation probes red-on-own-test at head, tree restored pristine*
bjoern merged commit 1fcc45f413 into main 2026-09-20 11:39:29 +02:00
bjoern deleted branch fix/reflection-rewrites-truncated-memories 2026-09-20 11:39:30 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!182
No description provided.