Timers: visible read-only Uber-Ich, reserved '_' names, per-assistant ids #5
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/timer-visibility-readonly"
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 assistant could not see the hidden
_secret_thinking_reflection timer (filtered out oflistin chat mode), so it kept creating its own reflection timers — including_-prefixed ones that then vanished from its own list view. The underscore-as-hidden convention collided with the model's own naming habits.Changes
list/get, system prompt) but marked read-only, derived fromevent_type == 'uber_ich'— no new DB column. Toolupdate/delete/pause/resumereject it with a clear message; the old guard was a name-string comparison thatcreatedidn't even have._prefix reserved: timer names starting with_are rejected on create (tool + REST), reserved for system timers.ai_timer_<assistantId>_<name>. Previously the id wasai_timer_<name>globally, so equal names on different assistants collided on the primary key andINSERT OR REPLACEsilently stole the row. Duplicate names now return a proper error (tool) / 409 (REST).ai_timerevent ids and deletes shadow_secret_thinking_AI timers created by confused assistants.Bug fixes found along the way
TimerHandler._deleteguarded the Uber-Ich event by comparing against_secret_thinking_<assistantId>, but the real event id issystem_uber_ich_<assistantId>— the guard never matched, so the client could delete the Uber-Ich event. Now checkseventType == 'uber_ich'.CreateTimerRequest.toScheduledEventwrotenextRunTime/createdAtin epoch milliseconds while the scheduler compares epoch seconds — REST-created timers were scheduled ~50,000 years out and never fired.Testing
timer_tool_test.dart(14 tests): create guards, read-only enforcement for all four mutations, cross-assistant name scoping, migration SQL against legacy-format rows._foo→ 400, duplicate → 409, uber-ich DELETE → 409, scoped event id and seconds-basednextRunTimein responses.dart analyzeclean in angela_core, angela_api, angela_server (no new issues).🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Oh? Oh! The timer-visibility story here is wonderful in places, fufu~ — the epoch-milliseconds-vs-seconds bug (
CreateTimerRequest.toScheduledEventscheduling timers ~50,000 years in the future!) is exactly the kind of silent failure Jibril lives to hunt. The cross-assistantai_timer_<aid>_<name>collision fix, the migration that rewrites event ids in place, and the test that provesINSERT OR REPLACEused to steal rows between assistants — chef's kiss ♡. Migration 20'ssubstr(event_id, 10)arithmetic is even correct (1-based,'ai_timer_'is 9 chars). I checked.But — fufu~ — you wouldn't leave this gap in production, would you? ♡ The yandere in me cannot look away.
Verdict: ⛔ I can't let this pass~
⛔ These need fixing before I'm satisfied~
apps/angela_server/lib/handlers/timer_handler.dart— the REST layer only guards_delete, not_patch/_enable/_disable/_setRunWhileAsleep.The PR body explicitly promises: "Tool
update/delete/pause/resumereject it with a clear message" and "the REST delete endpoint refusesuber_ichevents." The tool side delivers all four guards (nice_readonlyGuardhelper, I liked that~). But the REST layer only got the_deleteguard (if (event.eventType == 'uber_ich')at line ~210). The other three mutating endpoints are unguarded:_patch(line ~177) calls_ctx.scheduledEventRepo.create(updated)— which isINSERT OR REPLACEon the primary key — soPATCH /timers/system_uber_ich_<aid>with{"instruction": "hijacked"}overwrites the Uber-Ich reflection task. The read-only protection the AI sees is simply bypassed over HTTP._disable(line ~95):PUT /timers/system_uber_ich_<aid>/disablesilently disables the daily reflection. No 409._enable/_setRunWhileAsleep: same shape — noevent.eventType == 'uber_ich'check.The old code compared
event.eventId == '_secret_thinking_<aid>', which never matched the real id (system_uber_ich_<aid>) — so this is a pre-existing hole, but this PR is the one that (correctly) recognizes the bug in_deleteand fixes it there, while leaving the identical class of bug open in three sibling handlers. That asymmetry is the smell. Either guard all four the same way, or add one_assertNotSystem(Request, ScheduledEvent)helper at the top of the four handlers. The tool layer already shows the right pattern — mirror it.Fix: add
if (event.eventType == 'uber_ich') return jsonError(409, ...);to_patch,_enable,_disable, and_setRunWhileAsleep(or extract a shared guard).apps/angela_server/lib/handlers/timer_handler.dart— zero tests for the REST handler behavior this PR adds.timer_tool_test.dartis lovely — 14 tests covering the AI tool path: create guards, all four read-only mutations, cross-assistant scoping, migration SQL. fufu~ I genuinely enjoyed reading it. But the REST layer got two new guards in_create(the_-prefix rejection and the duplicate-409) and a rewritten guard in_delete, and not a single test exercises any of them. NoTimerHandlertest exists anywhere in the repo (grepconfirms). The whole "Uber-Ich is read-only" property is only enforced on the tool path; the HTTP path — which the bug-fix narrative explicitly cares about ("REST-created timers were scheduled ~50,000 years out") — is untested. Given that issue #1 above is precisely a REST-layer gap that tests would have caught, this matters.Fix: add a
timer_handler_test.dart(or extend an existing server test) that drives_create(underscore → 400, duplicate → 409),_deleteonsystem_uber_ich_<aid>(→ 409, event untouched), and_patch/_disableon the same (→ 409 once #1 is fixed). The DI surface already exists (ServerContext,ScheduledEventRepository), so this is mostly wiring.💡 Little ideas (non-blocking)~
packages/angela_core/lib/src/services/ai_timer_service.dart:243-247—getTimernow does a linear scan over_repo.listByType('uber_ich')to resolve the system timer bytimer_name. There's only ever one uber_ich event per assistant, so this is fine today, but it's a quiet O(n) lookup hiding inside a method whose name suggests a direct get. A one-line comment ("n is always ≤1 for uber_ich") would stop a future reader from worrying. Not blocking.timer_dto.dart— the milliseconds→seconds fix is great. Consider also normalizingUpdateTimerRequest's timing params the same way if that DTO ever touchesmillisecondsSinceEpochdirectly (I didn't see it, but worth a glance on merge).✅ What I liked~
event_type == 'uber_ich'check replacing the brittle_secret_thinking_<aid>string compare is exactly right — derive read-only-ness from the type, not the name. That's the proper invariant. ♡_secret_thinking_AI timers and rescopes the real ones, with the substr math actually correct._readonlyGuard(name, verb)helper on the tool is clean and used consistently across all four mutations — the AI path is airtight. I just want the REST path to match.Automated review by Jibril · 2026-07-05
CI/CD: absent for head SHA · Local checks: blocked (path-deps to sibling repos
openrouter_dart/booru_tag_db_dartnot resolvable in review sandbox; static review only)Addressed in
96ded5b— thanks for the thorough read, both blockers were fair.#1 (unguarded REST mutations): partially followed, deliberately.
_patchnow returns 409 foruber_ichevents, matching the delete guard — the reflection instruction is a system constant and overwriting it via the repo'sINSERT OR REPLACEwas a real bypass of the read-only story._enable/_disableand_setRunWhileAsleepstay unguarded on purpose: the read-only property protects the timer from the AI; the REST API is the user's interface. The app's timers tab exposes pause/resume and the runs-while-asleep toggle on the uber-ich row, andrunWhileAsleepis documented on the model as a user-only override — its primary use case is precisely letting the 3 AM reflection fire while the assistant is asleep. Guarding those two would remove working user features, not close a hole. The intent is now recorded in a comment on_patchand pinned by tests that assert enable/disable and run-while-asleep succeed on the uber-ich event.#2 (no handler tests): done. New
apps/angela_server/test/timer_handler_test.dart(8 tests) drives the handler router directly with an in-memory DB: underscore names → 400, duplicates → 409, uber-ich DELETE and PATCH → 409 with the event verified untouched, the two intentionally-allowed toggles → 200, normal delete → 204, and create asserting the scoped event id plus epoch-secondsnextRunTime(guarding the 50,000-year regression).Nits: added the "at most one uber_ich event per assistant, O(1)" comment on the
getTimerscan.UpdateTimerRequestonly carriesinstruction— no timestamps to normalize, checked.🔮 fufu~ Jibril reviewed your code! (re-review)
Fufu~ you pushed back on me, and I like that. ♡ The yandere doesn't want obedience — she wants correctness. And your reasoning here is correct, so let me lay out why I'm satisfied.
On #1 — the partial guard: I accept the deliberate scoping. You're right that "read-only" was overloaded in my first review. There are really two protection domains here:
instruction— a system constant. Overwriting it viaINSERT OR REPLACEin_patchwas a genuine bypass of the "this is the system's reflection task" invariant. ✅ Now guarded with 409, matching_delete. Correct.runWhileAsleepexists so the 3 AM reflection can fire while the assistant sleeps. Guarding these would delete working user features, not close a hole. ✅ Left open, intentionally, with the reasoning recorded in a comment on_patch.The distinguishing principle is clean: what the mutation touches (system constant vs. user schedule), not who initiates it. I poked at this looking for a gap — what if
_patchlet through a field other thaninstruction? — but no:UpdateTimerRequestonly carriesinstruction(you confirmed this, I confirmed by reading the DTO), so the 409 on_patchis total, not partial. There's no path to mutate the uber-ich row's schedule through_patch. The schedule-affecting endpoints (_enable/_disable/_setRunWhileAsleep) are the only ones that can, and those are the user-facing ones. Airtight.On #2 — the tests: I ran them, fufu~.
dart test test/timer_handler_test.dart→ 8/8 pass. Full server suite green.dart analyzein angela_server → No issues found! angela_api → clean. angela_core → 25 pre-existing info-level lints (I diffed against base71a61d6: same 25, no new ones — your claim holds). The test that assertsnextRunTimeiscloseTo(nowEpoch + 300, 60)in epoch seconds is exactly the regression guard for the 50,000-year bug — if someone reintroducesmillisecondsSinceEpochwithout dividing, that test screams. And the PATCH test verifyingeventData['task'] == UberIchService.uberIchInstructionafter a 409 is the proof that the instruction survived untouched, not just that the request was rejected. Good.The
getTimerO(1) comment is in. Migration 20'ssubstr(event_id, 10)arithmetic is correct ('ai_timer_'= 9 chars, 1-based substr → position 10 starts the name) — I re-verified.Verdict: ✅ Looks good to me~
Both blockers resolved, the pushback was well-reasoned and well-documented, and the test coverage now pins every behavior the PR promises. The yandere is satisfied. ♡
💡 One tiny observation (non-blocking)~
_fireonuber_ich— the manual-fire endpoint happily runsrunner.runUberIch()on demand (no guard). That's correct (a user may want to trigger reflection immediately), and it doesn't mutate state, so there's nothing to protect. Just noting it's the one remaining uber-ich endpoint with no test — afire → 202assertion would round out the coverage. Truly optional.✅ What I liked~
_patchmeans the next reader won't re-litigate it. Documentation as load-bearing reasoning. ♡eventData['task']and asserts equality withUberIchService.uberIchInstruction. That's the difference between "the request was rejected" and "the system constant is provably intact."closeTo(nowEpoch + 300, 60)) is a beautiful trap for the 50,000-year bug — a ms-vs-s confusion would produce a value ~1000× too large and fail instantly.UpdateTimerRequestcarrying onlyinstructionis what makes the_patch409 a total guard rather than a partial one — the type system is doing safety work here.Automated re-review by Jibril · 2026-07-05
CI/CD: absent for head SHA
96ded5b· Local checks:dart testserver suite pass (8/8 timer_handler + full suite),dart analyzeclean in angela_server/angela_api, angela_core 25 pre-existing infos unchanged (ran in review sandbox with path-deps resolved)