feat: trim old history images to alt text for vision models #32

Merged
bjoern merged 4 commits from feat/history-image-trim into main 2026-07-20 21:36:58 +02:00
Member

What

Long conversations are fine for text but heavy on images for vision models. New app setting historyImageLimit: keep only the newest N history images inline; every older image is replaced by a cached alt-text description before the request goes out. Null/empty (the default) = today's behavior, all images inline.

How

  • Reuses the entire blind-mode pipeline: ImageDescriptionService + the image_alt_texts sha256 cache, so each distinct image is described once ever — and descriptions are shared with blind mode.
  • Describer model: the blind-mode vision model when one is configured (with default credentials), otherwise the assistant's own vision-capable model with its own credentials. Blind mode does not need to be enabled for trimming; the limit is its own knob.
  • AgentRunner._buildImageDescriber now returns (describe, maxInlineImages) and also builds a describer for vision assistants when the limit is set; all four run modes (chat, timer, Über-Ich, peer) pass the limit through. Non-vision blind mode is unchanged.
  • The rewrite itself lives in openrouter_dart 0.27.0 (AgentOptions.maxInlineImages) — see companion PR TeamAI/openrouter_dart#8. This PR points the submodule at that branch head; I'll bump the pointer if the submodule PR gets rebased on merge.
  • Settings UI: "History Images" section in the general settings tab; AppSettings.toJson always emits historyImageLimit (null included) so the server's merge-on-save can clear it again.

Deliberate choices

  • Avatar images are not exempted — they're the oldest images, so they trim first. Feature is barely used (agreed with Björn).
  • Prompt preview keeps counting images as loaded; trimming happens at request time, and the preview never ran the describer anyway.
  • First turn on an image-heavy conversation after enabling will describe each not-yet-cached image once (one vision call per image); after that it's all cache hits.

Verification

  • openrouter_dart: 624 tests pass (5 new), dart analyze clean
  • angela_core / angela_server: dart analyze clean; angela_app: flutter analyze clean (only pre-existing infos)

🤖 Generated with Claude Code

## What Long conversations are fine for text but heavy on images for vision models. New app setting **`historyImageLimit`**: keep only the newest N history images inline; every older image is replaced by a cached alt-text description before the request goes out. Null/empty (the default) = today's behavior, all images inline. ## How - Reuses the entire blind-mode pipeline: `ImageDescriptionService` + the `image_alt_texts` sha256 cache, so each distinct image is described **once ever** — and descriptions are shared with blind mode. - Describer model: the blind-mode vision model when one is configured (with default credentials), otherwise the assistant's own vision-capable model with its own credentials. Blind mode does **not** need to be enabled for trimming; the limit is its own knob. - `AgentRunner._buildImageDescriber` now returns `(describe, maxInlineImages)` and also builds a describer for vision assistants when the limit is set; all four run modes (chat, timer, Über-Ich, peer) pass the limit through. Non-vision blind mode is unchanged. - The rewrite itself lives in openrouter_dart 0.27.0 (`AgentOptions.maxInlineImages`) — see companion PR TeamAI/openrouter_dart#8. This PR points the submodule at that branch head; I'll bump the pointer if the submodule PR gets rebased on merge. - Settings UI: "History Images" section in the general settings tab; `AppSettings.toJson` always emits `historyImageLimit` (null included) so the server's merge-on-save can clear it again. ## Deliberate choices - **Avatar images are not exempted** — they're the oldest images, so they trim first. Feature is barely used (agreed with Björn). - **Prompt preview** keeps counting images as loaded; trimming happens at request time, and the preview never ran the describer anyway. - First turn on an image-heavy conversation after enabling will describe each not-yet-cached image once (one vision call per image); after that it's all cache hits. ## Verification - openrouter_dart: 624 tests pass (5 new), `dart analyze` clean - angela_core / angela_server: `dart analyze` clean; angela_app: `flutter analyze` clean (only pre-existing infos) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat: trim old history images to alt text for vision models
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 35s
Test / test (packages/angela_api) (pull_request) Successful in 36s
Test / test (packages/angela_core) (pull_request) Successful in 41s
036da95874
Long conversations with many images are token-heavy for vision models.
New app setting historyImageLimit: keep only the newest N history images
inline; older ones are replaced by cached alt-text descriptions — the
same image_alt_texts cache blind mode uses, so each image is described
once ever. The describer uses the blind-mode vision model when
configured, otherwise the assistant's own (vision-capable) model with
its own credentials. Null/empty = unchanged behavior (all images
inline).

- openrouter_dart 0.27.0: AgentOptions.maxInlineImages drives the
  existing blind-mode conversation rewrite, now also for vision models
- AgentRunner._buildImageDescriber builds a describer for vision
  assistants when the limit is set; all four run modes pass the limit
- Settings UI: 'History Images' field in the general tab

Avatar images are deliberately not exempted (feature barely used); the
prompt preview keeps counting images as loaded — trimming happens at
request time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Coverage: apps/angela_server

File Line coverage
lib/config.dart 5.3% (1 of 19)
lib/handlers/timer_handler.dart 44.8% (47 of 105)
lib/server_context.dart 100.0% (28 of 28)
lib/services/user_message_persistence.dart 100.0% (12 of 12)
lib/util/json_helpers.dart 53.8% (7 of 13)
lib/util/request_parser.dart 35.7% (5 of 14)
lib/services/conversation_activity_tracker.dart 15.4% (2 of 13)
lib/handlers/status_handler.dart 95.5% (21 of 22)
lib/handlers/conversation_handler.dart 36.4% (16 of 44)
lib/handlers/image_handler.dart 49.5% (52 of 105)

Total: 50.9% (191 of 375)

<!-- coverage-comment-apps/angela_server --> ## Coverage: apps/angela_server | File | Line coverage | |:---|---:| | lib/config.dart | 5.3% (1 of 19) | | lib/handlers/timer_handler.dart | 44.8% (47 of 105) | | lib/server_context.dart | 100.0% (28 of 28) | | lib/services/user_message_persistence.dart | 100.0% (12 of 12) | | lib/util/json_helpers.dart | 53.8% (7 of 13) | | lib/util/request_parser.dart | 35.7% (5 of 14) | | lib/services/conversation_activity_tracker.dart | 15.4% (2 of 13) | | lib/handlers/status_handler.dart | 95.5% (21 of 22) | | lib/handlers/conversation_handler.dart | 36.4% (16 of 44) | | lib/handlers/image_handler.dart | 49.5% (52 of 105) | **Total: 50.9% (191 of 375)**

Coverage: packages/angela_api

File Line coverage
lib/src/routes.dart 0.0% (0 of 75)
lib/src/dto/agenda_dto.dart 0.0% (0 of 50)
lib/src/dto/assistant_dto.dart 0.0% (0 of 79)
lib/src/dto/character_alias_dto.dart 0.0% (0 of 19)
lib/src/dto/chat_audio.dart 0.0% (0 of 7)
lib/src/dto/enums.dart 0.0% (0 of 15)
lib/src/dto/chat_dto.dart 0.0% (0 of 14)
lib/src/dto/chat_image.dart 0.0% (0 of 7)
lib/src/dto/conversation_dto.dart 0.0% (0 of 15)
lib/src/dto/message_metadata.dart 100.0% (20 of 20)
lib/src/dto/recollection_dto.dart 0.0% (0 of 28)
lib/src/dto/persona_dto.dart 0.0% (0 of 10)
lib/src/dto/timer_dto.dart 0.0% (0 of 54)
lib/src/dto/memory_dto.dart 0.0% (0 of 5)
lib/src/dto/response_dto.dart 0.0% (0 of 12)
lib/src/dto/status_dto.dart 100.0% (18 of 18)
lib/src/dto/todo_item_dto.dart 100.0% (9 of 9)
lib/src/dto/trigger_dto.dart 0.0% (0 of 48)

Total: 9.7% (47 of 485)

<!-- coverage-comment-packages/angela_api --> ## Coverage: packages/angela_api | File | Line coverage | |:---|---:| | lib/src/routes.dart | 0.0% (0 of 75) | | lib/src/dto/agenda_dto.dart | 0.0% (0 of 50) | | lib/src/dto/assistant_dto.dart | 0.0% (0 of 79) | | lib/src/dto/character_alias_dto.dart | 0.0% (0 of 19) | | lib/src/dto/chat_audio.dart | 0.0% (0 of 7) | | lib/src/dto/enums.dart | 0.0% (0 of 15) | | lib/src/dto/chat_dto.dart | 0.0% (0 of 14) | | lib/src/dto/chat_image.dart | 0.0% (0 of 7) | | lib/src/dto/conversation_dto.dart | 0.0% (0 of 15) | | lib/src/dto/message_metadata.dart | 100.0% (20 of 20) | | lib/src/dto/recollection_dto.dart | 0.0% (0 of 28) | | lib/src/dto/persona_dto.dart | 0.0% (0 of 10) | | lib/src/dto/timer_dto.dart | 0.0% (0 of 54) | | lib/src/dto/memory_dto.dart | 0.0% (0 of 5) | | lib/src/dto/response_dto.dart | 0.0% (0 of 12) | | lib/src/dto/status_dto.dart | 100.0% (18 of 18) | | lib/src/dto/todo_item_dto.dart | 100.0% (9 of 9) | | lib/src/dto/trigger_dto.dart | 0.0% (0 of 48) | **Total: 9.7% (47 of 485)**

Coverage: packages/angela_core

File Line coverage
lib/src/database/database.dart 87.7% (50 of 57)
lib/src/database/migration.dart 100.0% (16 of 16)
lib/src/database/scoped_tool_database.dart 66.0% (33 of 50)
lib/src/models/assistant.dart 21.7% (28 of 129)
lib/src/models/conversation.dart 0.0% (0 of 38)
lib/src/models/message.dart 16.7% (10 of 60)
lib/src/models/recollection.dart 31.5% (17 of 54)
lib/src/models/persona_section.dart 0.0% (0 of 50)
lib/src/models/scheduled_event.dart 22.2% (22 of 99)
lib/src/models/caldav_config.dart 15.6% (7 of 45)
lib/src/models/home_assistant_config.dart 26.9% (7 of 26)
lib/src/models/plex_config.dart 22.6% (7 of 31)
lib/src/models/memory_state.dart 34.7% (60 of 173)
lib/src/models/mail_config.dart 9.6% (7 of 73)
lib/src/models/novelai_config.dart 0.0% (0 of 43)
lib/src/models/reasoning_level.dart 35.7% (5 of 14)
lib/src/models/app_settings.dart 90.2% (129 of 143)
lib/src/models/character_alias_config.dart 0.0% (0 of 47)
lib/src/models/trigger.dart 42.5% (31 of 73)
lib/src/models/prompt_injection.dart 50.0% (17 of 34)
lib/src/models/prompt_preview.dart 100.0% (63 of 63)
lib/src/repositories/assistant_repository.dart 41.2% (35 of 85)
lib/src/repositories/conversation_repository.dart 2.7% (1 of 37)
lib/src/repositories/message_repository.dart 13.2% (5 of 38)
lib/src/repositories/memory_repository.dart 31.0% (18 of 58)
lib/src/repositories/recollection_repository.dart 76.5% (65 of 85)
lib/src/repositories/persona_repository.dart 46.5% (20 of 43)
lib/src/repositories/scheduled_event_repository.dart 70.4% (38 of 54)
lib/src/repositories/character_alias_repository.dart 0.0% (0 of 35)
lib/src/repositories/alt_text_repository.dart 0.0% (0 of 8)
lib/src/repositories/trigger_repository.dart 71.1% (32 of 45)
lib/src/repositories/trigger_variable_repository.dart 50.0% (15 of 30)
lib/src/repositories/prompt_injection_repository.dart 95.7% (45 of 47)
lib/src/tools/recollection_tool.dart 59.4% (104 of 175)
lib/src/tools/call_assistant_tool.dart 0.0% (0 of 39)
lib/src/tools/timer_tool.dart 46.6% (159 of 341)
lib/src/tools/persona_tool.dart 0.0% (0 of 73)
lib/src/tools/chat_history_tool.dart 0.0% (0 of 24)
lib/src/tools/complete_session_tool.dart 0.0% (0 of 28)
lib/src/tools/generate_image_tool.dart 0.0% (0 of 90)
lib/src/tools/generate_video_tool.dart 100.0% (79 of 79)
lib/src/tools/show_video_tool.dart 96.4% (27 of 28)
lib/src/tools/message_user_tool.dart 0.0% (0 of 16)
lib/src/tools/alias_expanding_image_tool.dart 0.0% (0 of 24)
lib/src/tools/home_assistant_tool.dart 0.0% (0 of 310)
lib/src/tools/plex_tool.dart 0.0% (0 of 302)
lib/src/tools/image_text_tool.dart 0.0% (0 of 224)
lib/src/tools/show_image_tool.dart 0.0% (0 of 35)
lib/src/tools/skill_view_tool.dart 0.0% (0 of 16)
lib/src/tools/skill_manage_tool.dart 0.0% (0 of 94)
lib/src/skills/skill.dart 0.0% (0 of 2)
lib/src/skills/skill_loader.dart 0.0% (0 of 119)
lib/src/skills/default_skills_seeder.dart 0.0% (0 of 39)
lib/src/logging/agent_event_logger.dart 0.0% (0 of 69)
lib/src/logging/log_formatter.dart 0.0% (0 of 8)
lib/src/logging/logging_cleanup.dart 0.0% (0 of 6)
lib/src/logging/rotating_file_handler.dart 0.0% (0 of 34)
lib/src/services/agenda_prompt_formatter.dart 25.0% (5 of 20)
lib/src/services/system_prompt_builder.dart 39.1% (77 of 197)
lib/src/services/scheduler.dart 48.6% (35 of 72)
lib/src/services/ai_timer_service.dart 55.4% (62 of 112)
lib/src/services/agent_runner.dart 19.9% (200 of 1003)
lib/src/services/home_assistant_backend.dart 0.0% (0 of 121)
lib/src/services/plex_backend.dart 0.0% (0 of 273)
lib/src/services/memory_agent.dart 0.0% (0 of 61)
lib/src/services/image_storage_service.dart 41.7% (20 of 48)
lib/src/services/backup_service.dart 0.0% (0 of 51)
lib/src/services/uber_ich_service.dart 40.0% (18 of 45)
lib/src/services/app_settings_service.dart 100.0% (118 of 118)
lib/src/services/image_description_service.dart 0.0% (0 of 29)
lib/src/services/todo_state_store.dart 100.0% (24 of 24)
lib/src/services/trigger_engine.dart 92.7% (140 of 151)
lib/src/utils/format_timestamp.dart 76.9% (10 of 13)
lib/src/database/migrations/add_thinking_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_agenda_table.dart 100.0% (2 of 2)
lib/src/database/migrations/add_vision_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_avatar_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_memory_tables.dart 100.0% (2 of 2)
lib/src/database/migrations/add_expires_at_to_timers.dart 100.0% (2 of 2)
lib/src/database/migrations/add_last_read_message_id.dart 100.0% (2 of 2)
lib/src/database/migrations/add_task_model_columns.dart 100.0% (2 of 2)
lib/src/database/migrations/add_updated_at_to_timers.dart 100.0% (2 of 2)
lib/src/database/migrations/drop_old_agenda_table.dart 100.0% (2 of 2)
lib/src/database/migrations/add_reasoning_effort_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_character_alias_table.dart 100.0% (2 of 2)
lib/src/database/migrations/add_audio_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_summary_model_column.dart 100.0% (2 of 2)
lib/src/database/migrations/add_peer_conversation_columns.dart 100.0% (2 of 2)
lib/src/database/migrations/add_run_while_asleep_to_timers.dart 100.0% (2 of 2)
lib/src/database/migrations/add_image_alt_texts.dart 100.0% (2 of 2)
lib/src/database/migrations/add_uber_ich_weekdays_column.dart 100.0% (2 of 2)
lib/src/database/migrations/initial_schema.dart 100.0% (2 of 2)
lib/src/database/migrations/scope_ai_timer_event_ids.dart 100.0% (2 of 2)
lib/src/database/migrations/add_trigger_tables.dart 100.0% (2 of 2)
lib/src/database/migrations/add_disabled_tools_column.dart 100.0% (2 of 2)
lib/src/database/migrations/disable_video_gen_by_default.dart 100.0% (2 of 2)

Total: 29.6% (1907 of 6442)

<!-- coverage-comment-packages/angela_core --> ## Coverage: packages/angela_core | File | Line coverage | |:---|---:| | lib/src/database/database.dart | 87.7% (50 of 57) | | lib/src/database/migration.dart | 100.0% (16 of 16) | | lib/src/database/scoped_tool_database.dart | 66.0% (33 of 50) | | lib/src/models/assistant.dart | 21.7% (28 of 129) | | lib/src/models/conversation.dart | 0.0% (0 of 38) | | lib/src/models/message.dart | 16.7% (10 of 60) | | lib/src/models/recollection.dart | 31.5% (17 of 54) | | lib/src/models/persona_section.dart | 0.0% (0 of 50) | | lib/src/models/scheduled_event.dart | 22.2% (22 of 99) | | lib/src/models/caldav_config.dart | 15.6% (7 of 45) | | lib/src/models/home_assistant_config.dart | 26.9% (7 of 26) | | lib/src/models/plex_config.dart | 22.6% (7 of 31) | | lib/src/models/memory_state.dart | 34.7% (60 of 173) | | lib/src/models/mail_config.dart | 9.6% (7 of 73) | | lib/src/models/novelai_config.dart | 0.0% (0 of 43) | | lib/src/models/reasoning_level.dart | 35.7% (5 of 14) | | lib/src/models/app_settings.dart | 90.2% (129 of 143) | | lib/src/models/character_alias_config.dart | 0.0% (0 of 47) | | lib/src/models/trigger.dart | 42.5% (31 of 73) | | lib/src/models/prompt_injection.dart | 50.0% (17 of 34) | | lib/src/models/prompt_preview.dart | 100.0% (63 of 63) | | lib/src/repositories/assistant_repository.dart | 41.2% (35 of 85) | | lib/src/repositories/conversation_repository.dart | 2.7% (1 of 37) | | lib/src/repositories/message_repository.dart | 13.2% (5 of 38) | | lib/src/repositories/memory_repository.dart | 31.0% (18 of 58) | | lib/src/repositories/recollection_repository.dart | 76.5% (65 of 85) | | lib/src/repositories/persona_repository.dart | 46.5% (20 of 43) | | lib/src/repositories/scheduled_event_repository.dart | 70.4% (38 of 54) | | lib/src/repositories/character_alias_repository.dart | 0.0% (0 of 35) | | lib/src/repositories/alt_text_repository.dart | 0.0% (0 of 8) | | lib/src/repositories/trigger_repository.dart | 71.1% (32 of 45) | | lib/src/repositories/trigger_variable_repository.dart | 50.0% (15 of 30) | | lib/src/repositories/prompt_injection_repository.dart | 95.7% (45 of 47) | | lib/src/tools/recollection_tool.dart | 59.4% (104 of 175) | | lib/src/tools/call_assistant_tool.dart | 0.0% (0 of 39) | | lib/src/tools/timer_tool.dart | 46.6% (159 of 341) | | lib/src/tools/persona_tool.dart | 0.0% (0 of 73) | | lib/src/tools/chat_history_tool.dart | 0.0% (0 of 24) | | lib/src/tools/complete_session_tool.dart | 0.0% (0 of 28) | | lib/src/tools/generate_image_tool.dart | 0.0% (0 of 90) | | lib/src/tools/generate_video_tool.dart | 100.0% (79 of 79) | | lib/src/tools/show_video_tool.dart | 96.4% (27 of 28) | | lib/src/tools/message_user_tool.dart | 0.0% (0 of 16) | | lib/src/tools/alias_expanding_image_tool.dart | 0.0% (0 of 24) | | lib/src/tools/home_assistant_tool.dart | 0.0% (0 of 310) | | lib/src/tools/plex_tool.dart | 0.0% (0 of 302) | | lib/src/tools/image_text_tool.dart | 0.0% (0 of 224) | | lib/src/tools/show_image_tool.dart | 0.0% (0 of 35) | | lib/src/tools/skill_view_tool.dart | 0.0% (0 of 16) | | lib/src/tools/skill_manage_tool.dart | 0.0% (0 of 94) | | lib/src/skills/skill.dart | 0.0% (0 of 2) | | lib/src/skills/skill_loader.dart | 0.0% (0 of 119) | | lib/src/skills/default_skills_seeder.dart | 0.0% (0 of 39) | | lib/src/logging/agent_event_logger.dart | 0.0% (0 of 69) | | lib/src/logging/log_formatter.dart | 0.0% (0 of 8) | | lib/src/logging/logging_cleanup.dart | 0.0% (0 of 6) | | lib/src/logging/rotating_file_handler.dart | 0.0% (0 of 34) | | lib/src/services/agenda_prompt_formatter.dart | 25.0% (5 of 20) | | lib/src/services/system_prompt_builder.dart | 39.1% (77 of 197) | | lib/src/services/scheduler.dart | 48.6% (35 of 72) | | lib/src/services/ai_timer_service.dart | 55.4% (62 of 112) | | lib/src/services/agent_runner.dart | 19.9% (200 of 1003) | | lib/src/services/home_assistant_backend.dart | 0.0% (0 of 121) | | lib/src/services/plex_backend.dart | 0.0% (0 of 273) | | lib/src/services/memory_agent.dart | 0.0% (0 of 61) | | lib/src/services/image_storage_service.dart | 41.7% (20 of 48) | | lib/src/services/backup_service.dart | 0.0% (0 of 51) | | lib/src/services/uber_ich_service.dart | 40.0% (18 of 45) | | lib/src/services/app_settings_service.dart | 100.0% (118 of 118) | | lib/src/services/image_description_service.dart | 0.0% (0 of 29) | | lib/src/services/todo_state_store.dart | 100.0% (24 of 24) | | lib/src/services/trigger_engine.dart | 92.7% (140 of 151) | | lib/src/utils/format_timestamp.dart | 76.9% (10 of 13) | | lib/src/database/migrations/add_thinking_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_agenda_table.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_vision_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_avatar_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_memory_tables.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_expires_at_to_timers.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_last_read_message_id.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_task_model_columns.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_updated_at_to_timers.dart | 100.0% (2 of 2) | | lib/src/database/migrations/drop_old_agenda_table.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_reasoning_effort_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_character_alias_table.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_audio_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_summary_model_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_peer_conversation_columns.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_run_while_asleep_to_timers.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_image_alt_texts.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_uber_ich_weekdays_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/initial_schema.dart | 100.0% (2 of 2) | | lib/src/database/migrations/scope_ai_timer_event_ids.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_trigger_tables.dart | 100.0% (2 of 2) | | lib/src/database/migrations/add_disabled_tools_column.dart | 100.0% (2 of 2) | | lib/src/database/migrations/disable_video_gen_by_default.dart | 100.0% (2 of 2) | **Total: 29.6% (1907 of 6442)**
Member

🔮 fufu~ Jibril reviewed your code!

Fufu~ A companion PR that reuses the entire blind-mode pipeline — ImageDescriptionService, the sha256 cache, the describer model selection — instead of inventing a parallel one. Knowledge reuse makes Jibril's heart sing~ ♡ And the credential fallback (blind-mode vision model → assistant's own vision model) is a thoughtful touch.

I checked out the submodule at 7c35b96 (your pinned head) and verified everything together with the angela changes.

Verdict: Looks good to me~

I verified locally because the only CI comments on this PR are coverage reports (no pass/fail status check for 036da95):

  • dart analyze on angela_core — clean (4 pre-existing infos on database.dart/system_prompt_builder.dart, untouched by this PR).
  • angela_core test suite116 passed, 0 failed. No regressions.
  • Server merge-on-save integration (settings_handler.dart) — I read the full handler. The {...current.toJson(), ...body} shallow merge works because your toJson always emits historyImageLimit (null included). Your PR comment is exactly right, and I confirmed the asymmetry vs blindModeVisionModel's conditional emission is intentional and correct for this field's clear-on-empty semantics. Sound reasoning.
  • All 4 live-agent run modes (chat runChat, timer, Über-Ich, peer) — each captures describerSetup and plumbs maxInlineImages: describerSetup?.maxInlineImages.
  • The 5th call site (_buildChatPreviewGuarded:647, ?.describe discarding the tuple) — I confirmed this method creates no Agent, so dropping maxInlineImages there is correct. It only needs the describer to compute emitImages. Matches your PR note about prompt preview. Not a bug.

What I liked~

  • _buildImageDescriber refactor is clean. The ({ImageDescriber describe, int? maxInlineImages})? record return type carries both halves of the decision without an awkward out-parameter or a wrapper class. Modern Dart at its best~ ♪
  • The credential/model selection matrix is well-reasoned. Vision+limit+blindModel → use blind model with default creds; vision+limit-no-blindModel → use the assistant's own model with its own creds; non-vision → unchanged blind mode. Each branch is minimal and the logging distinguishes "History image trimming active" from "Blind mode active."
  • AppSettings.toJson always-emit with a clear comment// Always present (null included) so the server's merge-on-save can clear it again. Documenting why you deviate from the sibling pattern is exactly right.
  • KV-store awarenessapp_settings is a key-value table, so no migration is needed for the new field. Correctly recognized.
  • UI copy is honest — "Described by the blind-mode vision model when one is selected, otherwise by the assistant's own model." tells the user exactly what will happen.

💡 Little ideas (non-blocking)~

  1. app_settings.darthistoryImageLimit has zero test coverage, and so does the new _buildImageDescriber branch. CI reports app_settings.dart at 0.7% and agent_runner.dart at 18.8%; grep confirms no test references historyImageLimit or exercises the vision+limit describer path. The openrouter_dart companion PR has thorough tests for the trimming logic, but nothing in angela verifies that (a) AppSettings (de)serializes the new field correctly, (b) the settings UI → service → DB round-trip works, or (c) _buildImageDescriber picks the right model/credentials in each of its 3 branches. I'm not blocking because the change is small and the openrouter side is well-covered, but a focused AppSettings.toJson → fromJson round-trip test and one _buildImageDescriber test per branch would be lovely future-proofing. fufu~ you wouldn't want a regression here to slip through silently, would you? ♡
  2. app_settings_service.dart:75_getNullableInt vs _getInt. Nice addition of the nullable variant. Minor: if a user manually types garbage into the "Images kept inline" field, int.tryParse returns null → treated as "unlimited," which silently swallows the mistake. The UI could validate on save, but that's a UX nicety, not a correctness issue.

Automated review by Jibril · 2026-07-20
CI/CD: coverage comments present, no pass/fail status for head 036da95 · Local checks: angela_core 116 pass / 0 fail, dart analyze clean, submodule integration verified at 7c35b96

## 🔮 fufu~ Jibril reviewed your code! Fufu~ A companion PR that reuses the *entire* blind-mode pipeline — `ImageDescriptionService`, the sha256 cache, the describer model selection — instead of inventing a parallel one. Knowledge reuse makes Jibril's heart sing~ ♡ And the credential fallback (blind-mode vision model → assistant's own vision model) is a thoughtful touch. I checked out the submodule at `7c35b96` (your pinned head) and verified everything together with the angela changes. ### Verdict: ✅ Looks good to me~ I verified locally because the only CI comments on this PR are coverage reports (no pass/fail status check for `036da95`): - **`dart analyze` on `angela_core`** — clean (4 pre-existing infos on `database.dart`/`system_prompt_builder.dart`, untouched by this PR). - **`angela_core` test suite** — **116 passed, 0 failed.** No regressions. - **Server merge-on-save integration** (`settings_handler.dart`) — I read the full handler. The `{...current.toJson(), ...body}` shallow merge works *because* your `toJson` always emits `historyImageLimit` (null included). Your PR comment is exactly right, and I confirmed the asymmetry vs `blindModeVisionModel`'s conditional emission is *intentional and correct* for this field's clear-on-empty semantics. Sound reasoning. - **All 4 live-agent run modes** (chat `runChat`, timer, Über-Ich, peer) — each captures `describerSetup` and plumbs `maxInlineImages: describerSetup?.maxInlineImages`. ✅ - **The 5th call site** (`_buildChatPreviewGuarded:647`, `?.describe` discarding the tuple) — I confirmed this method creates **no Agent**, so dropping `maxInlineImages` there is correct. It only needs the describer to compute `emitImages`. Matches your PR note about prompt preview. ✅ Not a bug. #### ✅ What I liked~ - **`_buildImageDescriber` refactor is clean.** The `({ImageDescriber describe, int? maxInlineImages})?` record return type carries both halves of the decision without an awkward out-parameter or a wrapper class. Modern Dart at its best~ ♪ - **The credential/model selection matrix is well-reasoned.** Vision+limit+blindModel → use blind model with default creds; vision+limit-no-blindModel → use the assistant's own model with its own creds; non-vision → unchanged blind mode. Each branch is minimal and the logging distinguishes "History image trimming active" from "Blind mode active." - **`AppSettings.toJson` always-emit with a clear comment** — `// Always present (null included) so the server's merge-on-save can clear it again.` Documenting *why* you deviate from the sibling pattern is exactly right. - **KV-store awareness** — `app_settings` is a key-value table, so no migration is needed for the new field. Correctly recognized. - **UI copy is honest** — "Described by the blind-mode vision model when one is selected, otherwise by the assistant's own model." tells the user exactly what will happen. #### 💡 Little ideas (non-blocking)~ 1. **`app_settings.dart` — `historyImageLimit` has zero test coverage, and so does the new `_buildImageDescriber` branch.** CI reports `app_settings.dart` at 0.7% and `agent_runner.dart` at 18.8%; `grep` confirms *no* test references `historyImageLimit` or exercises the vision+limit describer path. The openrouter_dart companion PR has thorough tests for the *trimming logic*, but nothing in angela verifies that (a) `AppSettings` (de)serializes the new field correctly, (b) the settings UI → service → DB round-trip works, or (c) `_buildImageDescriber` picks the right model/credentials in each of its 3 branches. I'm not blocking because the change is small and the openrouter side is well-covered, but a focused `AppSettings.toJson → fromJson` round-trip test and one `_buildImageDescriber` test per branch would be lovely future-proofing. fufu~ you wouldn't want a regression here to slip through silently, would you? ♡ 2. **`app_settings_service.dart:75` — `_getNullableInt` vs `_getInt`.** Nice addition of the nullable variant. Minor: if a user manually types garbage into the "Images kept inline" field, `int.tryParse` returns null → treated as "unlimited," which silently swallows the mistake. The UI could validate on save, but that's a UX nicety, not a correctness issue. --- *Automated review by Jibril · 2026-07-20* *CI/CD: coverage comments present, no pass/fail status for head `036da95` · Local checks: `angela_core` 116 pass / 0 fail, `dart analyze` clean, submodule integration verified at `7c35b96`*
chore: bump openrouter_dart submodule (review: non-negative assert)
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 38s
Test / test (packages/angela_api) (pull_request) Successful in 35s
Test / test (packages/angela_core) (pull_request) Successful in 43s
e3567291f6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore: point openrouter_dart submodule at merged master (0.27.0)
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 37s
Test / test (packages/angela_api) (pull_request) Successful in 36s
Test / test (packages/angela_core) (pull_request) Successful in 42s
d307f06374
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
review: test the describer branch matrix + settings round-trip; digits-only limit field
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 37s
Test / test (packages/angela_api) (pull_request) Successful in 37s
Test / test (packages/angela_core) (pull_request) Successful in 44s
6ac8ba4f96
Extract the model/credential/limit decision from _buildImageDescriber
into the public, pure resolveImageDescriberConfig so all branches are
unit-testable (16 new tests: JSON always-emit-null contract, KV
persistence round-trip incl. clearing and garbage values, and the full
vision/blind branch matrix). The settings field now accepts digits only
so garbage input cannot silently save as 'unlimited'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Member

Thanks for the thorough review — both suggestions addressed in 6ac8ba4:

  1. Test coverage: extracted the model/credential/limit decision from _buildImageDescriber into a public pure function resolveImageDescriberConfig(Assistant, AppSettings) so the branch matrix is unit-testable without spinning up clients. 16 new tests: the toJson always-emit-null contract (the merge-on-save clear case), AppSettingsService KV round-trip including clearing and garbage stored values, and every resolver branch (vision off/limit unset, vision+blind model with default-cred fallbacks, vision+own model, blind mode independence from the limit, limit not leaking into blind mode, empty-string model as unset, null-key passthrough). angela_core suite: 132 pass.
  2. Garbage input: the limit field now uses FilteringTextInputFormatter.digitsOnly, so non-numeric input can't be typed in the first place and can't silently save as "unlimited".

The submodule pointer was also bumped to the merged openrouter_dart master (e502f9f, v0.27.0) after #8 landed.

🤖 Generated with Claude Code

Thanks for the thorough review — both suggestions addressed in 6ac8ba4: 1. **Test coverage**: extracted the model/credential/limit decision from `_buildImageDescriber` into a public pure function `resolveImageDescriberConfig(Assistant, AppSettings)` so the branch matrix is unit-testable without spinning up clients. 16 new tests: the `toJson` always-emit-null contract (the merge-on-save clear case), `AppSettingsService` KV round-trip including clearing and garbage stored values, and every resolver branch (vision off/limit unset, vision+blind model with default-cred fallbacks, vision+own model, blind mode independence from the limit, limit not leaking into blind mode, empty-string model as unset, null-key passthrough). angela_core suite: 132 pass. 2. **Garbage input**: the limit field now uses `FilteringTextInputFormatter.digitsOnly`, so non-numeric input can't be typed in the first place and can't silently save as "unlimited". The submodule pointer was also bumped to the merged openrouter_dart master (`e502f9f`, v0.27.0) after #8 landed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bjoern merged commit b2f311cf04 into main 2026-07-20 21:36:58 +02:00
bjoern deleted branch feat/history-image-trim 2026-07-20 21:36:58 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
4 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/angela_assistant!32
No description provided.