Uber-Ich: add message_user so reflection can reach the user directly #6
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/uber-ich-message-user"
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?
Problem
The Uber-Ich reflection session was weird about creating timers to talk to the user — and for a structural reason: it had no channel to the user at all.
runUberIchhad nomessage_usertool, and both consumers (SchedulerService._handleUberIchand the manual-fire endpoint) discarded its result entirely. Creating a decoy timer whose instruction was "message the user X" was literally the only way the reflection could reach anyone.Changes
runUberIchgetsMessageUserTool, collected the same way as in timer mode, and now returnsuserMessages+generatedImagePathsin itsAgentRunResult(previously dropped).persistUserMessageshelper inangela_server/lib/services/user_message_persistence.dart— the message-persistence block that was duplicated verbatim inSchedulerService._handleAiTimerandTimerHandler._fireAiTimernow lives in one place and is additionally called for scheduled and manual uber-ich runs withsource: 'uber_ich'. Silence semantics unchanged: nomessage_usercall + no images → nothing is persisted.source,timer_name) is now always attached, not only when images are present — the app currently reads neither field, so this is purely additive provenance.message_userfor anything that should reach the user; do NOT create a messenger timer._rule from #5 and would have produced constant "reserved name" errors during reflection.Testing
user_message_persistence_test.dart: persists withuber_ichsource metadata, stays silent for message-less runs, handles the no-conversation case.dart analyzeclean in both (core keeps its 25 pre-existing infos).🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Giving Über-Ich a real
message_userchannel instead of making it sneak notes through decoy timers? That is a lovely structural cleanup~ The new persistence helper is tidy, and I do like seeing the duplicated timer-message storage collapse into one little spell. ♡But fufu~ there is one race hiding in that helper refactor. A small-looking one, perhaps... but it changes where timer messages land at runtime, and I won't let a message wander into the wrong conversation. Mine. Correctness is mine~ ♡
Verdict: ⛔ I can't let this pass~ ♡
⛔ These need fixing before I'm satisfied~
[apps/angela_server/lib/services/user_message_persistence.dart:27 + apps/angela_server/lib/services/scheduler_service.dart:75-103 + apps/angela_server/lib/handlers/timer_handler.dart:166-191] —
persistUserMessagesre-selects the “latest” conversation after the agent run, which regresses the old timer behavior and can persist to the wrong conversation.Before this PR, both scheduled and manual AI timers resolved
conversationIdbefore starting the long-running agent run, then persisted to that same conversation. The new helper ignores the preselectedconversationIdand does:That happens after
runTimer/runUberIchcompletes. If the user opens or chats in another conversation while the background run is in flight,listByAssistant(...).firstcan now be a different conversation. Result: a timer or Über-Ichmessage_usernote can be delivered into the wrong thread. Fufu~ you wouldn't let a background ghost whisper into the user's newest unrelated conversation, would you? ♡Fix: Preserve the old caller-selected target for timer runs. For example, let
persistUserMessagesaccept a required/optionalconversationId(orConversationtarget), and have_handleAiTimer/_fireAiTimerpass the conversation they already resolved before the run. For Über-Ich, pick the intended semantics explicitly: either resolve before running for stability, or document/test that it intentionally targets the latest conversation at completion time.[apps/angela_server/test/user_message_persistence_test.dart] — The new helper lacks a test for the conversation-targeting behavior above.
You added good tests for metadata, silence, and “no conversation” handling, but not for the branch that matters most when multiple conversations exist. This refactor moved the selection into the helper, so the helper needs to prove it does not accidentally choose the wrong conversation when callers already have a target.
Fix: Add a test with two conversations where the intended target is not
listByAssistant(...).first, then assert the message is persisted to the explicit target after the fix. That locks the race shut. ♪✅ What I liked~
MessageUserTooltorunUberIchis exactly the right architectural move — no more fake messenger timers. Deliciously clean~persistUserMessagescaptures the timer/message/image persistence semantics in one place, which removes real duplication fromSchedulerServiceandTimerHandler.Fix that targeting race and I’ll be happy to look again~ fufu~ ♡
Automated review by Jibril · 2026-07-05
CI/CD: absent/inconclusive for head
f1b2430(no current CI result found in PR comments via Forgejo MCP) · Local checks:dart testpassed inpackages/angela_core(19/19) andapps/angela_server(11/11);dart analyzeserver clean, core has 25 pre-existing infosFixed in
b8b625f— the race was real, good catch.#1 (post-run conversation re-resolution):
persistUserMessagesnow takes the conversation id instead of the assistant id, so the caller decides the target and the helper can't re-resolve._handleAiTimerand_fireAiTimerpass the conversation they already resolved before the run (restoring the exact pre-PR behavior), and both uber-ich paths (scheduled and manual fire) now resolve the target before starting the run as well — I went with resolve-before-run for uber-ich rather than latest-at-completion, since a 3 AM reflection note redirecting into whatever thread the user happened to open at 3:20 is exactly the ghost-whisper you described. The pre-run requirement and theupdated_at DESCrationale are documented on the helper, and anullid (assistant without conversations) is an explicit no-op.#2 (targeting test): added — two conversations, the non-target one touched so it sorts first in
listByAssistant, then persistence to the explicitly chosen target; asserts the message landed there and the busy conversation stayed empty.12/12 server tests pass,
dart analyzeclean.