Summary
When a Durable Object created via idFromName()/getByName() runs its alarm() handler, ctx.id.name is undefined if the DO instance was evicted from memory before the alarm fired. Normal requests are unaffected because the caller re-derives the name-bearing ID on each call.
Root cause
The local AlarmScheduler persists only the hex actor ID, not the name:
-- src/workerd/server/alarm-scheduler.c++
CREATE TABLE IF NOT EXISTS _cf_ALARM (
actor_id TEXT PRIMARY KEY,
scheduled_time INTEGER
) WITHOUT ROWID;
The name is a one-way HMAC into the ID bytes, so it can't be recovered from the hex ID. When the alarm fires and the actor isn't resident, server.c++ reconstructs it via idFromString(), which produces a nameless ActorId:
// src/workerd/server/server.c++ (AlarmScheduler::GetActorFn)
Worker::Actor::Id id = idFactory->idFromString(kj::mv(idStr)); // name is always kj::none
That nameless ID is used to build the Worker::Actor, so ctx.id.name is empty. While the actor is still warm, the original name-bearing ID is reused and it works.
Reproduction
- getByName("foo") → set an alarm more than ~10s out (past the idle-eviction window), or restart the process.
- In alarm(), read this.ctx.id.name → undefined (expected "foo").
Suggested fix
Persist the name alongside actor_id in _cf_ALARM, thread it through AlarmScheduler/GetActorFn, and reconstruct via the existing idFromRaw(bytes, name) overload instead of idFromString().
Summary
When a Durable Object created via idFromName()/getByName() runs its alarm() handler, ctx.id.name is undefined if the DO instance was evicted from memory before the alarm fired. Normal requests are unaffected because the caller re-derives the name-bearing ID on each call.
Root cause
The local AlarmScheduler persists only the hex actor ID, not the name:
The name is a one-way HMAC into the ID bytes, so it can't be recovered from the hex ID. When the alarm fires and the actor isn't resident, server.c++ reconstructs it via idFromString(), which produces a nameless ActorId:
That nameless ID is used to build the Worker::Actor, so ctx.id.name is empty. While the actor is still warm, the original name-bearing ID is reused and it works.
Reproduction
Suggested fix
Persist the name alongside actor_id in _cf_ALARM, thread it through AlarmScheduler/GetActorFn, and reconstruct via the existing idFromRaw(bytes, name) overload instead of idFromString().