feat: trim old history images to alt text for vision models #32
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/history-image-trim"
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?
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
ImageDescriptionService+ theimage_alt_textssha256 cache, so each distinct image is described once ever — and descriptions are shared with blind mode.AgentRunner._buildImageDescribernow 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.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.AppSettings.toJsonalways emitshistoryImageLimit(null included) so the server's merge-on-save can clear it again.Deliberate choices
Verification
dart analyzecleandart analyzeclean; angela_app:flutter analyzeclean (only pre-existing infos)🤖 Generated with Claude Code
Coverage: apps/angela_server
Total: 50.9% (191 of 375)
Coverage: packages/angela_api
Total: 9.7% (47 of 485)
Coverage: packages/angela_core
Total: 29.6% (1907 of 6442)
🔮 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 analyzeonangela_core— clean (4 pre-existing infos ondatabase.dart/system_prompt_builder.dart, untouched by this PR).angela_coretest suite — 116 passed, 0 failed. No regressions.settings_handler.dart) — I read the full handler. The{...current.toJson(), ...body}shallow merge works because yourtoJsonalways emitshistoryImageLimit(null included). Your PR comment is exactly right, and I confirmed the asymmetry vsblindModeVisionModel's conditional emission is intentional and correct for this field's clear-on-empty semantics. Sound reasoning.runChat, timer, Über-Ich, peer) — each capturesdescriberSetupand plumbsmaxInlineImages: describerSetup?.maxInlineImages. ✅_buildChatPreviewGuarded:647,?.describediscarding the tuple) — I confirmed this method creates no Agent, so droppingmaxInlineImagesthere is correct. It only needs the describer to computeemitImages. Matches your PR note about prompt preview. ✅ Not a bug.✅ What I liked~
_buildImageDescriberrefactor 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~ ♪AppSettings.toJsonalways-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.app_settingsis a key-value table, so no migration is needed for the new field. Correctly recognized.💡 Little ideas (non-blocking)~
app_settings.dart—historyImageLimithas zero test coverage, and so does the new_buildImageDescriberbranch. CI reportsapp_settings.dartat 0.7% andagent_runner.dartat 18.8%;grepconfirms no test referenceshistoryImageLimitor 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)_buildImageDescriberpicks 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 focusedAppSettings.toJson → fromJsonround-trip test and one_buildImageDescribertest per branch would be lovely future-proofing. fufu~ you wouldn't want a regression here to slip through silently, would you? ♡app_settings_service.dart:75—_getNullableIntvs_getInt. Nice addition of the nullable variant. Minor: if a user manually types garbage into the "Images kept inline" field,int.tryParsereturns 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_core116 pass / 0 fail,dart analyzeclean, submodule integration verified at7c35b96Thanks for the thorough review — both suggestions addressed in
6ac8ba4:_buildImageDescriberinto a public pure functionresolveImageDescriberConfig(Assistant, AppSettings)so the branch matrix is unit-testable without spinning up clients. 16 new tests: thetoJsonalways-emit-null contract (the merge-on-save clear case),AppSettingsServiceKV 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.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