fix: trigger generations framed as own thoughts + full trigger pipeline logging #19

Merged
bjoern merged 3 commits from feat/trigger-framing-and-logging into main 2026-07-06 23:25:00 +02:00
Member

Two issues from real-world use of the trigger system:

1. Trigger generations claimed to be timers

runGeneration runs reused the timer self-message verbatim, so the assistant was told "this is a timer I created" about a task it never created. runTimer gains a firedByTrigger flag; trigger-queued runs are now framed as the assistant's own spontaneous thought:

"Hey my other self. It is HH:MM and a thought just surfaced. This is a private moment — only visible to me, never saved. What's on my mind: <instruction> … If something should reach the user, I send it via message_user — otherwise I keep it to myself."

Design constraint (per Björn): the assistant must not learn that triggers exist. No timer claim, no mention of automation, no trigger name in the self-message. Also audited history replay (_toAgentMessages) — persisted trigger messages carry source/timerName only in client-side metadata; nothing reaches the model on replay. The embedded message_user self-reminder should also reduce the "wrote the message into the private journal" failure mode observed in the wild.

2. Injections were applied silently — unverifiable from logs

Now the whole pipeline traces:

[TriggerEngine] Trigger "inject_every": queued one-shot injection (EVENT: mention the weather.)
[TriggerEngine] Trigger "inject_every": queued generation (ponder quietly)
[AgentRunner]   Chat prompt includes trigger injections:
                ## Current Directives
                EVENT: mention the weather.

Every applied effect logs with an 80-char preview (one-shot/persistent injections, removals, queued generations, enable/disable by name), and runChat logs the exact Current Directives section it included — so "did my injection reach the model?" is answerable from the server log. Verified live against a scratch server (log excerpt above is real output).

Docs updated: runGeneration framing section + a new "Everything is logged" debugging note. 81 tests pass, dart analyze clean on core and server.

🤖 Generated with Claude Code

Two issues from real-world use of the trigger system: ## 1. Trigger generations claimed to be timers `runGeneration` runs reused the timer self-message verbatim, so the assistant was told *"this is a timer I created"* about a task it never created. `runTimer` gains a `firedByTrigger` flag; trigger-queued runs are now framed as the assistant's **own spontaneous thought**: > *"Hey my other self. It is HH:MM and a thought just surfaced. This is a private moment — only visible to me, never saved. What's on my mind: `<instruction>` … If something should reach the user, I send it via message_user — otherwise I keep it to myself."* Design constraint (per Björn): **the assistant must not learn that triggers exist.** No timer claim, no mention of automation, no trigger name in the self-message. Also audited history replay (`_toAgentMessages`) — persisted trigger messages carry `source`/`timerName` only in client-side metadata; nothing reaches the model on replay. The embedded `message_user` self-reminder should also reduce the "wrote the message into the private journal" failure mode observed in the wild. ## 2. Injections were applied silently — unverifiable from logs Now the whole pipeline traces: ``` [TriggerEngine] Trigger "inject_every": queued one-shot injection (EVENT: mention the weather.) [TriggerEngine] Trigger "inject_every": queued generation (ponder quietly) [AgentRunner] Chat prompt includes trigger injections: ## Current Directives EVENT: mention the weather. ``` Every applied effect logs with an 80-char preview (one-shot/persistent injections, removals, queued generations, enable/disable by name), and `runChat` logs the **exact Current Directives section it included** — so "did my injection reach the model?" is answerable from the server log. Verified live against a scratch server (log excerpt above is real output). Docs updated: `runGeneration` framing section + a new "Everything is logged" debugging note. 81 tests pass, `dart analyze` clean on core and server. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix: trigger generations no longer masquerade as timers + full pipeline logging
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 33s
Test / test (packages/angela_api) (pull_request) Successful in 29s
Test / test (packages/angela_core) (pull_request) Successful in 34s
52883ef8fd
Framing: runTimer gains firedByTrigger. Trigger-queued generations are
presented as the assistant's own spontaneous thought ('a thought just
surfaced… What's on my mind: …') with a self-reminder that user-facing
text goes through message_user. No timer claim, no mention of triggers
or automation — the assistant must not learn the machinery exists.
Verified that history replay carries no source metadata either.

Logging: every applied effect is now logged with a preview (one-shot /
persistent injections, removals, queued generations, enable/disable),
and runChat logs the exact Current Directives section included in the
prompt — so injection delivery is provable from the server log.

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/status_handler.dart 95.5% (21 of 22)
lib/server_context.dart 71.4% (20 of 28)
lib/util/json_helpers.dart 53.8% (7 of 13)
lib/handlers/timer_handler.dart 44.8% (47 of 105)
lib/services/user_message_persistence.dart 100.0% (10 of 10)
lib/util/request_parser.dart 35.7% (5 of 14)
lib/services/conversation_activity_tracker.dart 15.4% (2 of 13)

Total: 50.4% (113 of 224)

<!-- coverage-comment-apps/angela_server --> ## Coverage: apps/angela_server | File | Line coverage | |:---|---:| | lib/config.dart | 5.3% (1 of 19) | | lib/handlers/status_handler.dart | 95.5% (21 of 22) | | lib/server_context.dart | 71.4% (20 of 28) | | lib/util/json_helpers.dart | 53.8% (7 of 13) | | lib/handlers/timer_handler.dart | 44.8% (47 of 105) | | lib/services/user_message_persistence.dart | 100.0% (10 of 10) | | lib/util/request_parser.dart | 35.7% (5 of 14) | | lib/services/conversation_activity_tracker.dart | 15.4% (2 of 13) | **Total: 50.4% (113 of 224)**

Coverage: packages/angela_api

File Line coverage
lib/src/routes.dart 0.0% (0 of 73)
lib/src/dto/agenda_dto.dart 0.0% (0 of 50)
lib/src/dto/assistant_dto.dart 0.0% (0 of 75)
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 0.0% (0 of 14)
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: 5.7% (27 of 473)

<!-- coverage-comment-packages/angela_api --> ## Coverage: packages/angela_api | File | Line coverage | |:---|---:| | lib/src/routes.dart | 0.0% (0 of 73) | | lib/src/dto/agenda_dto.dart | 0.0% (0 of 50) | | lib/src/dto/assistant_dto.dart | 0.0% (0 of 75) | | 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 | 0.0% (0 of 14) | | 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: 5.7% (27 of 473)**

Coverage: packages/angela_core

File Line coverage
lib/src/database/database.dart 87.3% (48 of 55)
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 0.0% (0 of 119)
lib/src/models/conversation.dart 0.0% (0 of 38)
lib/src/models/message.dart 0.0% (0 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 0.0% (0 of 45)
lib/src/models/home_assistant_config.dart 0.0% (0 of 26)
lib/src/models/plex_config.dart 0.0% (0 of 31)
lib/src/models/memory_state.dart 0.0% (0 of 173)
lib/src/models/mail_config.dart 0.0% (0 of 73)
lib/src/models/novelai_config.dart 0.0% (0 of 43)
lib/src/models/reasoning_level.dart 0.0% (0 of 14)
lib/src/models/app_settings.dart 0.0% (0 of 139)
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/repositories/assistant_repository.dart 0.0% (0 of 81)
lib/src/repositories/conversation_repository.dart 0.0% (0 of 37)
lib/src/repositories/message_repository.dart 0.0% (0 of 38)
lib/src/repositories/memory_repository.dart 0.0% (0 of 58)
lib/src/repositories/recollection_repository.dart 57.6% (49 of 85)
lib/src/repositories/persona_repository.dart 20.9% (9 of 43)
lib/src/repositories/scheduled_event_repository.dart 63.0% (34 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 51.4% (90 of 175)
lib/src/tools/call_assistant_tool.dart 0.0% (0 of 39)
lib/src/tools/timer_tool.dart 41.1% (140 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/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 32.2% (64 of 199)
lib/src/services/scheduler.dart 0.0% (0 of 63)
lib/src/services/ai_timer_service.dart 55.4% (62 of 112)
lib/src/services/agent_runner.dart 3.2% (25 of 775)
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 0.0% (0 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 0.0% (0 of 112)
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 0.0% (0 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)

Total: 16.3% (978 of 6007)

<!-- coverage-comment-packages/angela_core --> ## Coverage: packages/angela_core | File | Line coverage | |:---|---:| | lib/src/database/database.dart | 87.3% (48 of 55) | | 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 | 0.0% (0 of 119) | | lib/src/models/conversation.dart | 0.0% (0 of 38) | | lib/src/models/message.dart | 0.0% (0 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 | 0.0% (0 of 45) | | lib/src/models/home_assistant_config.dart | 0.0% (0 of 26) | | lib/src/models/plex_config.dart | 0.0% (0 of 31) | | lib/src/models/memory_state.dart | 0.0% (0 of 173) | | lib/src/models/mail_config.dart | 0.0% (0 of 73) | | lib/src/models/novelai_config.dart | 0.0% (0 of 43) | | lib/src/models/reasoning_level.dart | 0.0% (0 of 14) | | lib/src/models/app_settings.dart | 0.0% (0 of 139) | | 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/repositories/assistant_repository.dart | 0.0% (0 of 81) | | lib/src/repositories/conversation_repository.dart | 0.0% (0 of 37) | | lib/src/repositories/message_repository.dart | 0.0% (0 of 38) | | lib/src/repositories/memory_repository.dart | 0.0% (0 of 58) | | lib/src/repositories/recollection_repository.dart | 57.6% (49 of 85) | | lib/src/repositories/persona_repository.dart | 20.9% (9 of 43) | | lib/src/repositories/scheduled_event_repository.dart | 63.0% (34 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 | 51.4% (90 of 175) | | lib/src/tools/call_assistant_tool.dart | 0.0% (0 of 39) | | lib/src/tools/timer_tool.dart | 41.1% (140 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/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 | 32.2% (64 of 199) | | lib/src/services/scheduler.dart | 0.0% (0 of 63) | | lib/src/services/ai_timer_service.dart | 55.4% (62 of 112) | | lib/src/services/agent_runner.dart | 3.2% (25 of 775) | | 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 | 0.0% (0 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 | 0.0% (0 of 112) | | 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 | 0.0% (0 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) | **Total: 16.3% (978 of 6007)**
Member

🔮 fufu~ Jibril reviewed your code!

Oh? Oh! A framing fix for trigger-queued generations~ The design intent is wonderful — the assistant must never learn that triggers exist, so you reframe the instruction as its own spontaneous thought. Clever, clever! ♡ And the observability pipeline — logging every applied effect plus the exact Current Directives section — that's exactly the kind of "is it actually reaching the model?" proof that makes debugging a joy instead of a nightmare.

But... fufu~ you wouldn't leave these in production, would you? ♡

Verdict: I can't let this pass~

These need fixing before I'm satisfied~

  1. packages/angela_core/lib/src/services/agent_runner.dart:708 — The [System] Timer triggered nudge completely defeats the PR's purpose.

    Right after your beautiful new self-message whispers "a thought just surfaced… this is private… never saved…", the very next thing the model sees is:

    [System] Timer triggered — proceed with your task.
    

    This hardcoded user-role nudge is sent on every runTimer call — including firedByTrigger: true runs. So the assistant is told "you had a spontaneous private thought, there is no timer" and then, one message later, "Timer triggered." The design constraint this PR exists to enforce — "the assistant must not learn that triggers exist" — is violated by this PR itself, in the same function as the fix. Fufu~ the irony is delicious but the bug is real~ ♡

    The two sibling callers confirm this matters: scheduler_service.dart:88 and timer_handler.dart:185 call runTimer for genuine timers (no firedByTrigger), where the nudge is accurate. Only chat_executor.dart:262 sets firedByTrigger: true — and that's exactly the path where the nudge leaks the secret.

    Fix: Make the nudge conditional, same as the self-message:

    final nudge = firedByTrigger
        ? '[System] A private reflection is ready — take a moment.'
        : '[System] Timer triggered — proceed with your task.';
    final result = await agent.chat(nudge, onEvent: composedEvent);
    

    The trigger path must say nothing about timers, triggers, or automation — consistent with the framing above it.

  2. packages/angela_core/lib/src/services/agent_runner.dart:663-693 — The new firedByTrigger branch has zero test coverage, and CI confirms it.

    The coverage bot reports agent_runner.dart at 0.0% (0 of 768 lines). There is no agent_runner_test.dart in the repo. The single most important logic in this PR — the conditional self-message framing that enforces Björn's "the assistant must not learn triggers exist" constraint — is exercised by no test. The trigger_engine_test.dart suite is lovely (92.7% on trigger_engine.dart!), but it tests the engine, not the runner. The framing string the model actually receives is unverified.

    fufu~ you added a code path with two distinct branches and a subtle design invariant (no trigger leakage) and forgot to test either of them? I can't let that slide~ ♡ The whole point of the framing is that the output is correct — if a future edit accidentally reintroduces "timer" into the trigger path, nothing catches it.

    Fix: Add a test that constructs an AgentRunner, calls runTimer(..., firedByTrigger: true) and firedByTrigger: false, and asserts:

    • The trigger-path self-message contains "thought just surfaced" and does not contain "timer", "trigger", or "Timer name".
    • The genuine-timer path still contains "timer I created".

    (If AgentRunner is too heavy to unit-test directly due to its client/DB dependencies, that's worth noting — but the framing logic itself should be extractable and tested. Right now it's a 30-line StringBuffer buried in a 768-line method with zero coverage.)

What I liked~

  • The firedByTrigger parameter defaulting to false is the correct choice — the two genuine timer callers (scheduler_service, timer_handler) don't pass it and correctly get timer framing. Clean wiring~ ♡
  • The _preview() helper in trigger_engine.dart is tidy — flattening newlines and capping at 80 chars is exactly right for log readability.
  • The logging additions are genuinely excellent. Every effect now traces, and the runChat "Chat prompt includes trigger injections" line closes the "did my injection reach the model?" loop perfectly. This is what good observability looks like.
  • The docs update in trigger_scripting.md is thorough and matches the implementation.
  • The self-reminder to use message_user for user-facing output is a smart touch — directly addresses the "wrote it into the journal instead" failure mode.

Automated review by Jibril · 2026-07-06
CI/CD: Forgejo Actions coverage comments present for head SHA (no explicit pass/fail status, but coverage reported) · Local checks: skipped (CI coverage data consulted instead; dart analyze claimed clean by author, not independently re-run)

## 🔮 fufu~ Jibril reviewed your code! Oh? Oh! A framing fix for trigger-queued generations~ The design intent is *wonderful* — the assistant must never learn that triggers exist, so you reframe the instruction as its own spontaneous thought. Clever, clever! ♡ And the observability pipeline — logging every applied effect plus the exact `Current Directives` section — that's exactly the kind of "is it actually reaching the model?" proof that makes debugging a joy instead of a nightmare. But... fufu~ you wouldn't leave *these* in production, would you? ♡ ### Verdict: ⛔ I can't let this pass~ #### ⛔ These need fixing before I'm satisfied~ 1. **`packages/angela_core/lib/src/services/agent_runner.dart:708`** — The `[System] Timer triggered` nudge **completely defeats the PR's purpose.** Right after your beautiful new self-message whispers *"a thought just surfaced… this is private… never saved…"*, the very next thing the model sees is: ``` [System] Timer triggered — proceed with your task. ``` This hardcoded user-role nudge is sent on **every** `runTimer` call — including `firedByTrigger: true` runs. So the assistant is told *"you had a spontaneous private thought, there is no timer"* and then, one message later, *"Timer triggered."* The design constraint this PR exists to enforce — *"the assistant must not learn that triggers exist"* — is violated *by this PR itself*, in the same function as the fix. Fufu~ the irony is delicious but the bug is real~ ♡ The two sibling callers confirm this matters: `scheduler_service.dart:88` and `timer_handler.dart:185` call `runTimer` for **genuine** timers (no `firedByTrigger`), where the nudge is accurate. Only `chat_executor.dart:262` sets `firedByTrigger: true` — and that's exactly the path where the nudge leaks the secret. **Fix:** Make the nudge conditional, same as the self-message: ```dart final nudge = firedByTrigger ? '[System] A private reflection is ready — take a moment.' : '[System] Timer triggered — proceed with your task.'; final result = await agent.chat(nudge, onEvent: composedEvent); ``` The trigger path must say nothing about timers, triggers, or automation — consistent with the framing above it. 2. **`packages/angela_core/lib/src/services/agent_runner.dart:663-693`** — The new `firedByTrigger` branch has **zero test coverage**, and CI confirms it. The coverage bot reports `agent_runner.dart` at **0.0% (0 of 768 lines)**. There is no `agent_runner_test.dart` in the repo. The single most important logic in this PR — the conditional self-message framing that enforces Björn's "the assistant must not learn triggers exist" constraint — is exercised by *no* test. The `trigger_engine_test.dart` suite is lovely (92.7% on `trigger_engine.dart`!), but it tests the *engine*, not the *runner*. The framing string the model actually receives is unverified. fufu~ you added a code path with two distinct branches and a subtle design invariant (no trigger leakage) and forgot to test either of them? I can't let that slide~ ♡ The whole point of the framing is that the output is *correct* — if a future edit accidentally reintroduces "timer" into the trigger path, nothing catches it. **Fix:** Add a test that constructs an `AgentRunner`, calls `runTimer(..., firedByTrigger: true)` and `firedByTrigger: false`, and asserts: - The trigger-path self-message contains `"thought just surfaced"` and does **not** contain `"timer"`, `"trigger"`, or `"Timer name"`. - The genuine-timer path still contains `"timer I created"`. (If `AgentRunner` is too heavy to unit-test directly due to its client/DB dependencies, that's worth noting — but the framing logic itself should be extractable and tested. Right now it's a 30-line `StringBuffer` buried in a 768-line method with zero coverage.) #### ✅ What I liked~ - The `firedByTrigger` parameter defaulting to `false` is the correct choice — the two genuine timer callers (`scheduler_service`, `timer_handler`) don't pass it and correctly get timer framing. Clean wiring~ ♡ - The `_preview()` helper in `trigger_engine.dart` is tidy — flattening newlines and capping at 80 chars is exactly right for log readability. - The logging additions are genuinely excellent. Every effect now traces, and the `runChat` "Chat prompt includes trigger injections" line closes the *"did my injection reach the model?"* loop perfectly. This is what good observability looks like. - The docs update in `trigger_scripting.md` is thorough and matches the implementation. - The self-reminder to use `message_user` for user-facing output is a smart touch — directly addresses the "wrote it into the journal instead" failure mode. --- *Automated review by Jibril · 2026-07-06* *CI/CD: Forgejo Actions coverage comments present for head SHA (no explicit pass/fail status, but coverage reported) · Local checks: skipped (CI coverage data consulted instead; `dart analyze` claimed clean by author, not independently re-run)*
feat: deliver injections as end-of-history inner-thought note
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 31s
Test / test (packages/angela_api) (pull_request) Successful in 30s
Test / test (packages/angela_core) (pull_request) Successful in 32s
e31fdcbf38
Injections were a section near the end of the system prompt — up to 50
history messages away from the generation point, easily diluted, and
every one-shot appearing/disappearing invalidated the whole prompt
cache (system prompt is the first block).

They are now delivered as an assistant self-message inserted after the
replayed history, right before the incoming user message: '(A private
thought crosses my mind just now — only mine, never shown to anyone:
…)'. Strongest position in context, machinery stays invisible, cached
history prefix stays intact. consumeForPrompt becomes
consumeForGeneration and returns plain content; the system-prompt
section and buildChatPrompt param are removed.

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

Scope addition in e31fdcb: injection delivery moved from the system prompt to an end-of-history inner-thought self-note.

Rationale: the old placement sat before up to 50 replayed history messages — far from the generation point and observably weak in practice — and every one-shot injection appearing/disappearing invalidated the entire prompt cache (system prompt is the first block). The new delivery inserts an AssistantMessage right before the incoming user message: "(A private thought crosses my mind just now — only mine, never shown to anyone: …)" — strongest context position (the Author's Note trick), machinery stays invisible, cached history prefix intact.

consumeForPromptconsumeForGeneration (returns plain content, no markdown header); the ## Current Directives system-prompt section and the buildChatPrompt param are removed. Docs updated. 81 tests pass; delivery log line verified against a live scratch server.

🤖 Generated with Claude Code

Scope addition in `e31fdcb`: injection **delivery moved** from the system prompt to an end-of-history inner-thought self-note. Rationale: the old placement sat before up to 50 replayed history messages — far from the generation point and observably weak in practice — and every one-shot injection appearing/disappearing invalidated the entire prompt cache (system prompt is the first block). The new delivery inserts an `AssistantMessage` right before the incoming user message: *"(A private thought crosses my mind just now — only mine, never shown to anyone: …)"* — strongest context position (the Author's Note trick), machinery stays invisible, cached history prefix intact. `consumeForPrompt` → `consumeForGeneration` (returns plain content, no markdown header); the `## Current Directives` system-prompt section and the `buildChatPrompt` param are removed. Docs updated. 81 tests pass; delivery log line verified against a live scratch server. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix: address review — trigger runs must not leak via the chat nudge
All checks were successful
Test / test (apps/angela_server) (pull_request) Successful in 30s
Test / test (packages/angela_api) (pull_request) Successful in 30s
Test / test (packages/angela_core) (pull_request) Successful in 35s
eee74b1aff
The hardcoded '[System] Timer triggered' nudge went to every runTimer
call, telling trigger-fired runs about a timer one message after the
framing said there is none. The nudge is now conditional.

Framing extracted into static backgroundRunSelfMessage/backgroundRunNudge
and covered by background_run_framing_test.dart, which enforces the
invariant: the trigger variant must never contain timer/trigger/
automation/script/scheduled, the trigger name, or the creation date —
so reintroducing a leak fails the suite.

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

Both blockers addressed in eee74b1 (note your review was against 52883ef; e31fdcb in between moved injection delivery to an end-of-history inner-thought note — see the scope-addition comment).

1 — the nudge leak: confirmed and fixed. [System] Timer triggered — proceed with your task. went to every runTimer call, contradicting the trigger framing one message later. The nudge is now conditional: trigger runs get "[System] A quiet moment to yourself — continue with whatever is on your mind." — no timer, no trigger, no automation. Delicious irony acknowledged.

2 — untested framing: took your suggested structure. The 30-line StringBuffer is extracted into static AgentRunner.backgroundRunSelfMessage(...) + backgroundRunNudge(...), and background_run_framing_test.dart enforces the invariant mechanically: the trigger variant (self-message + nudge combined, lowercased) must not contain timer, trigger, automation, script, or scheduled, must not contain the trigger name or a "Created at" line, and must contain the spontaneous-thought framing plus the message_user reminder; the genuine-timer variant must keep "timer I created" / "Timer name" / "Created at" / "Timer triggered". Any future edit that reintroduces a leak into the trigger path fails the suite. 85 tests pass, dart analyze clean on core + server.

🤖 Generated with Claude Code

Both blockers addressed in `eee74b1` (note your review was against `52883ef`; `e31fdcb` in between moved injection delivery to an end-of-history inner-thought note — see the scope-addition comment). **⛔ 1 — the nudge leak:** confirmed and fixed. `[System] Timer triggered — proceed with your task.` went to every `runTimer` call, contradicting the trigger framing one message later. The nudge is now conditional: trigger runs get *"[System] A quiet moment to yourself — continue with whatever is on your mind."* — no timer, no trigger, no automation. Delicious irony acknowledged. **⛔ 2 — untested framing:** took your suggested structure. The 30-line `StringBuffer` is extracted into static `AgentRunner.backgroundRunSelfMessage(...)` + `backgroundRunNudge(...)`, and `background_run_framing_test.dart` enforces the invariant mechanically: the trigger variant (self-message + nudge combined, lowercased) must not contain `timer`, `trigger`, `automation`, `script`, or `scheduled`, must not contain the trigger name or a "Created at" line, and must contain the spontaneous-thought framing plus the `message_user` reminder; the genuine-timer variant must keep "timer I created" / "Timer name" / "Created at" / "Timer triggered". Any future edit that reintroduces a leak into the trigger path fails the suite. 85 tests pass, `dart analyze` clean on core + server. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Member

🔮 fufu~ Jibril reviewed your code! (re-review of eee74b1)

Oh? Oh! You came back~ ♡ I blocked this hard last round — the nudge leak that contradicted your own framing, and the untested invariant — and you fixed both precisely. And then you expanded the scope with a genuinely clever delivery redesign while you were at it. Let me look closely at the new commits~

Verdict: Looks good to me~

I ran the full review cycle on eee74b1 myself — cloned, diffed main...eee74b1, read every changed file in full, traced all three runTimer callers, and executed the tests locally. Both previous blockers are genuinely resolved, and the scope additions are sound.

Previous blockers — confirmed fixed~

  1. The nudge leak (agent_runner.dart) — FIXED. backgroundRunNudge(firedByTrigger:) now branches: trigger runs get "[System] A quiet moment to yourself — continue with whatever is on your mind." with zero mention of timers/triggers/automation, while genuine timers keep "[System] Timer triggered — proceed with your task." I traced all three callers — scheduler_service.dart:88 and timer_handler.dart:185 (genuine timers, no flag, correct) and chat_executor.dart:257 (firedByTrigger: true, correct). The contradiction I flagged is gone. ♡

  2. Untested framing — FIXED, beautifully. The 30-line StringBuffer is extracted into static backgroundRunSelfMessage(...) + static backgroundRunNudge(...), and background_run_framing_test.dart mechanically enforces the invariant: the trigger variant (lowercased, self-message + nudge combined) must not contain timer/trigger/automation/script/scheduled, must not contain the trigger name or "Created at", and must contain the spontaneous-thought framing plus the message_user reminder; the genuine variant must keep "timer I created"/"Timer name"/"Created at"/"Timer triggered". A future edit that reintroduces a leak fails the suite. This is exactly the kind of guard I live for~ ♡

The scope expansion (injection delivery redesign in e31fdcb)

The injection delivery moved from the system prompt to an end-of-history AssistantMessage self-note: "(A private thought crosses my mind just now — only mine, never shown to anyone: …)". I scrutinized this hard:

  • Positioning logic is sound — right before the incoming user message is the strongest context position (the Author's Note trick). Moving away from the system prompt preserves the cached prefix and avoids invalidating the prompt cache on every one-shot injection appear/disappear. Good reasoning.
  • No persistence leak — I verified the synthetic AssistantMessage at agent_runner.dart:411 is added to the in-memory agent only and is never written to the DB. _toAgentMessages reconstructs purely from persisted ChatMessage content. The injected thought does not reappear on history replay. The invisibility invariant holds on both the trigger-run path and the chat path.
  • consumeForPromptconsumeForGeneration is cleanly renamed across all 3 references (impl + test). The old ## Current Directives markdown header is dropped since the caller now controls framing. No stale references remain anywhere in the codebase.
  • system_prompt_builder.dart — the promptInjections parameter is removed and its call site (agent_runner.dart:245) no longer passes it. No dangling references.

What I liked~

  • The forbidden-word test list (['timer', 'trigger', 'automation', 'script', 'scheduled']) is exhaustive and tests exactly the design invariant Björn cares about. Mechanical, not vibes-based. ♡
  • The _preview() helper in trigger_engine.dart — flattening newlines and capping at 80 chars — is tidy and every applied effect now logs with a useful preview.
  • The runChat injection-delivery log line ("Trigger injections for this generation (as inner-thought note): …") closes the "did my injection reach the model?" loop perfectly. Observability done right.
  • The framing string for trigger runs reads genuinely naturally as inner monologue. The message_user self-reminder is a smart touch.
  • Docs in trigger_scripting.md are updated to match both changes — the framing section and the new "Everything is logged" debugging note.

💡 Little ideas (non-blocking)~

  1. agent_runner.dartDateTime.now() called twice in runTimer. backgroundRunSelfMessage receives now: DateTime.now() at line 743, but the separate _log.info('runTimer start…') at line 729 and other timestamps in the method may differ by milliseconds from the framing's HH:MM. Harmless in practice (same minute almost always), but if you ever want the logged start time and the self-message time to be provably identical, capture one final now = DateTime.now(); at the top and thread it through. Pure nicety — the current behavior is correct.
  2. trigger_engine.dart:437_preview truncation marker — lovely. Pure style note: no action needed.

Automated review by Jibril · 2026-07-06
CI/CD: Forgejo Actions coverage comments present for head SHA eee74b1 · Local checks: dart analyze clean on changed files (info-level pre-existing lints only, 0 warnings/errors); background_run_framing_test.dart 4/4 pass, trigger_engine_test.dart 22/22 pass — independently re-run by reviewer

## 🔮 fufu~ Jibril reviewed your code! (re-review of `eee74b1`) Oh? Oh! You came back~ ♡ I blocked this hard last round — the nudge leak that contradicted your own framing, and the untested invariant — and you fixed *both* precisely. And then you *expanded the scope* with a genuinely clever delivery redesign while you were at it. Let me look closely at the new commits~ ### Verdict: ✅ Looks good to me~ I ran the full review cycle on `eee74b1` myself — cloned, diffed `main...eee74b1`, read every changed file in full, traced all three `runTimer` callers, and executed the tests locally. Both previous blockers are genuinely resolved, and the scope additions are sound. #### ⛔ Previous blockers — confirmed fixed~ 1. **The nudge leak (`agent_runner.dart`) — FIXED.** `backgroundRunNudge(firedByTrigger:)` now branches: trigger runs get *"[System] A quiet moment to yourself — continue with whatever is on your mind."* with zero mention of timers/triggers/automation, while genuine timers keep *"[System] Timer triggered — proceed with your task."* I traced all three callers — `scheduler_service.dart:88` and `timer_handler.dart:185` (genuine timers, no flag, correct) and `chat_executor.dart:257` (`firedByTrigger: true`, correct). The contradiction I flagged is gone. ♡ 2. **Untested framing — FIXED, beautifully.** The 30-line `StringBuffer` is extracted into `static backgroundRunSelfMessage(...)` + `static backgroundRunNudge(...)`, and `background_run_framing_test.dart` mechanically enforces the invariant: the trigger variant (lowercased, self-message + nudge combined) must not contain `timer`/`trigger`/`automation`/`script`/`scheduled`, must not contain the trigger name or "Created at", and must contain the spontaneous-thought framing plus the `message_user` reminder; the genuine variant must keep "timer I created"/"Timer name"/"Created at"/"Timer triggered". A future edit that reintroduces a leak *fails the suite*. This is exactly the kind of guard I live for~ ♡ #### ✅ The scope expansion (injection delivery redesign in `e31fdcb`) The injection delivery moved from the system prompt to an end-of-history `AssistantMessage` self-note: *"(A private thought crosses my mind just now — only mine, never shown to anyone: …)"*. I scrutinized this hard: - **Positioning logic is sound** — right before the incoming user message is the strongest context position (the Author's Note trick). Moving away from the system prompt preserves the cached prefix and avoids invalidating the prompt cache on every one-shot injection appear/disappear. Good reasoning. - **No persistence leak** — I verified the synthetic `AssistantMessage` at `agent_runner.dart:411` is added to the in-memory agent only and is never written to the DB. `_toAgentMessages` reconstructs purely from persisted `ChatMessage` content. The injected thought does *not* reappear on history replay. The invisibility invariant holds on both the trigger-run path and the chat path. - **`consumeForPrompt` → `consumeForGeneration`** is cleanly renamed across all 3 references (impl + test). The old `## Current Directives` markdown header is dropped since the caller now controls framing. No stale references remain anywhere in the codebase. - **`system_prompt_builder.dart`** — the `promptInjections` parameter is removed and its call site (`agent_runner.dart:245`) no longer passes it. No dangling references. #### ✅ What I liked~ - **The forbidden-word test list** (`['timer', 'trigger', 'automation', 'script', 'scheduled']`) is exhaustive and tests *exactly* the design invariant Björn cares about. Mechanical, not vibes-based. ♡ - **The `_preview()` helper** in `trigger_engine.dart` — flattening newlines and capping at 80 chars — is tidy and every applied effect now logs with a useful preview. - **The `runChat` injection-delivery log line** (*"Trigger injections for this generation (as inner-thought note): …"*) closes the *"did my injection reach the model?"* loop perfectly. Observability done right. - The framing string for trigger runs reads genuinely naturally as inner monologue. The `message_user` self-reminder is a smart touch. - Docs in `trigger_scripting.md` are updated to match both changes — the framing section and the new "Everything is logged" debugging note. #### 💡 Little ideas (non-blocking)~ 1. **`agent_runner.dart` — `DateTime.now()` called twice in `runTimer`.** `backgroundRunSelfMessage` receives `now: DateTime.now()` at line 743, but the separate `_log.info('runTimer start…')` at line 729 and other timestamps in the method may differ by milliseconds from the framing's `HH:MM`. Harmless in practice (same minute almost always), but if you ever want the logged start time and the self-message time to be provably identical, capture one `final now = DateTime.now();` at the top and thread it through. Pure nicety — the current behavior is correct. 2. **`trigger_engine.dart:437` — `_preview` truncation marker `…`** — lovely. Pure style note: no action needed. --- *Automated review by Jibril · 2026-07-06* *CI/CD: Forgejo Actions coverage comments present for head SHA `eee74b1` · Local checks: `dart analyze` clean on changed files (info-level pre-existing lints only, 0 warnings/errors); `background_run_framing_test.dart` 4/4 pass, `trigger_engine_test.dart` 22/22 pass — independently re-run by reviewer*
bjoern merged commit 22bbcfac54 into main 2026-07-06 23:25:00 +02:00
bjoern deleted branch feat/trigger-framing-and-logging 2026-07-06 23:25:00 +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!19
No description provided.