TL;DR
A fresh Durable Object created under partyserver 0.5.x can hit Attempting to read .name on X, but this.ctx.id.name is not set when an alarm fires, even though the DO was correctly addressed via idFromName()/getByName(). The error message blames "legacy alarms scheduled before 2026-03-15" but the failure mode applies equally to brand-new alarms scheduled today, when the project's compatibility_date predates the runtime change that propagates ctx.id.name into alarm handlers.
The 0.5.x change to stop writing __ps_name to storage removed the safety net that would otherwise have caught this. Existing pre-0.5.x DOs are fine because their __ps_name records are still on disk; fresh DOs have nothing to fall back to.
Reproduction
- Create a new project with a
compatibility_date older than 2026-03-15 (e.g. 2026-01-28 — this is the date most existing examples in cloudflare/agents and templates currently use).
- Use a partyserver-derived class that schedules an alarm during normal operation. In
cloudflare/agents, Think.onChatRecovery schedules _chatRecoveryContinue via this.schedule(0, ...) whenever a tool execute is interrupted (see packages/think/src/think.ts:2779).
- Trigger the alarm-scheduling path (e.g. let a tool execute fail, or restart
wrangler dev after the schedule was set).
- After restart, the alarm fires on a fresh DO instance.
Result:
Error in Assistant:<unnamed> fetch: Error: Attempting to read .name on Assistant, but this.ctx.id.name is not set. PartyServer requires DOs to be addressed via idFromName()/getByName(). If this is a legacy alarm scheduled before 2026-03-15, reschedule it from a fetch handler to restore the name.
at Assistant.name (...partyserver.js)
at fn (...packages/agents/src/index.ts:1570:63) // this.mcp.restoreConnectionsFromStorage(this.name)
at Assistant._tryCatch (...packages/agents/src/index.ts:2250:20)
at ...packages/agents/src/index.ts:1569:22
at Assistant.Think.onStart (...packages/think/src/think.ts:583:7)
at ...partyserver.js
Why the error message is misleading for this case
The #nameOrThrow() throw at packages/partyserver/src/index.ts:671 handles three real cases but only describes two:
- ✅ Pre-0.5.x DOs with stale alarms.
__ps_name is in storage; the alarm fallback in #hydrateNameFromLegacyStorage reads it and this.name resolves. No error. Current message correctly addresses this case.
- ✅ DOs addressed via
idFromString() / newUniqueId(). Genuinely user error. Current message correctly addresses this case.
- ❌ Fresh 0.5.x DOs with
compatibility_date < 2026-03-15. No __ps_name exists (since 0.5.x stopped writing it). ctx.id.name is undefined in the alarm handler (because compat_date predates the runtime change that landed name propagation in alarms). User did everything "right" (used idFromName via routing). The "legacy alarm — reschedule from a fetch handler" advice doesn't fit: the alarm isn't legacy, and rescheduling from a fetch handler doesn't help if the underlying fallback record never existed.
This third case is the easiest one to fall into because every example in cloudflare/agents (and likely most templates / scaffolds) currently uses a pre-cutoff compatibility_date.
Root cause
Two interacting changes:
- The Cloudflare runtime change on 2026-03-15 made
ctx.id.name available inside alarm handlers (Durable Objects ID docs). This is gated by compatibility_date.
- partyserver 0.5.x stopped writing
__ps_name to storage on the assumption that the runtime fix made it unnecessary (changelog excerpt: "Server no longer writes the __ps_name record to storage. Existing records remain on disk for backward compatibility and are only read inside alarm() as a fallback for alarms that were scheduled before 2026-03-15").
For projects whose compatibility_date is on the recent side of the cutoff, the runtime path Just Works and no fallback is needed. For projects on the old side, the runtime doesn't propagate the name and there's no __ps_name record to fall back to, so this.name throws inside onStart() on alarm wake.
Proposed fixes
In rough priority order:
1. Restore a one-time __ps_name write on first fetch() (recommended)
Inside fetch(), after #ensureInitialized(), if ctx.id.name is set and no __ps_name exists yet, write it. Idempotent — fires only once per DO lifetime; after the first write, the record stays. Cost: one extra storage put on the very first fetch a DO ever sees. After that, alarms always have a fallback regardless of compat_date or runtime version.
This is strictly safer than the 0.5.x baseline. The perf delta vs. the pre-0.5.x version is one storage put per DO lifetime, not per request — negligible in absolute terms. The price of NOT writing it is silent breakage on every project with an older compat_date that schedules any alarm.
Sketch:
// packages/partyserver/src/index.ts, inside fetch() after #ensureInitialized()
if (this.ctx.id.name && !this.#_name) {
// Best-effort defensive bootstrap. Idempotent — we only write if
// the legacy record isn't already present. Restores the safety
// net that pre-0.5.x partyserver provided automatically, so
// alarms can recover the name regardless of compat_date.
const existing = await this.ctx.storage.get(NAME_STORAGE_KEY);
if (!existing) {
await this.ctx.storage.put(NAME_STORAGE_KEY, this.ctx.id.name);
}
}
(Or do the existence check + put inside #hydrateNameFromLegacyStorage symmetrically — wherever fits the style.)
2. Tailor the throw message to distinguish the three cases
#nameOrThrow() at packages/partyserver/src/index.ts:668-674 could check what's actually present and tailor the message. Something like:
const ctxName = this.ctx.id.name;
if (ctxName !== undefined) return ctxName;
if (this.#_name) return this.#_name;
// Distinguish the three real cases.
const hasLegacyRecord = /* check #_legacyRecordWasPresent flag set during hydrate */;
if (hasLegacyRecord) {
// Pre-0.5.x DO with a stale alarm — rare, current message is right.
throw new Error(`...legacy alarm scheduled before 2026-03-15...`);
}
throw new Error(
`Attempting to read .name on ${this.#ParentClass.name}, but this.ctx.id.name is not set ` +
`and no name was provided via fetch headers or storage. Likely causes:\n` +
` 1. Your compatibility_date is older than 2026-03-15. Newer Cloudflare runtimes propagate ` +
` ctx.id.name into alarm handlers automatically; older ones don't. Bump compatibility_date.\n` +
` 2. The DO was addressed via idFromString() or newUniqueId(). PartyServer requires name-based ` +
` addressing — use idFromName() / getByName() / routePartykitRequest().\n` +
` 3. A native DO RPC method was called without going through __unsafe_ensureInitialized() first.`
);
This is a polish on top of the real fix. With option 1 in place, this message gets seen much less often, but when it is seen it's actionable.
3. Optional: add a startup-time warning
Inside #ensureInitialized(), if we're on the slow path AND no legacy record exists, console.warn once per DO with a one-line summary of the likely cause. Probably overkill given option 1 makes the failure mode go away entirely; including for completeness.
What I'd avoid
Reverting the 0.5.x change wholesale and writing __ps_name on every fetch would re-add a storage put per request. That was the right thing to drop. The fix here is a one-time-per-DO-lifetime write, not per-request.
Affected ecosystem
cloudflare/agents (Agent extends Server from partyserver). All examples in that repo currently use compatibility_date: 2026-01-28 or 2026-03-06, both pre-cutoff. Originally surfaced via examples/agents-as-tools — Think schedules _chatRecoveryContinue on tool-execute interruption, and that alarm wakes the DO post-restart, hitting this exact failure.
- Anyone consuming
partysync, partysub, y-partyserver, hono-party — same migration applies (see their respective changelogs in this repo).
- Anyone whose project copied compat_date from older docs/templates.
Workaround for now
Two manual steps in the affected project:
- Bump
compatibility_date in wrangler.jsonc to a date after 2026-03-15 (I used 2026-04-15 in cloudflare/agents's examples/agents-as-tools/wrangler.jsonc).
- Wipe
.wrangler/state/ to clear any orphaned alarm scheduled under the old runtime.
Both are one-time and only address the immediate user; the partyserver-side fix is what closes the silent-breakage hazard for everyone.
Context
Out of scope
- Changing the
cloudflare/agents examples' compat_dates. That's a separate, additive cleanup that should happen too, but it doesn't fix the partyserver gap for any project not under our control.
- Debugging Think's
_chatRecoveryContinue scheduling — that's downstream and works fine if this.name is recoverable.
TL;DR
A fresh Durable Object created under partyserver 0.5.x can hit
Attempting to read .name on X, but this.ctx.id.name is not setwhen an alarm fires, even though the DO was correctly addressed viaidFromName()/getByName(). The error message blames "legacy alarms scheduled before 2026-03-15" but the failure mode applies equally to brand-new alarms scheduled today, when the project'scompatibility_datepredates the runtime change that propagatesctx.id.nameinto alarm handlers.The 0.5.x change to stop writing
__ps_nameto storage removed the safety net that would otherwise have caught this. Existing pre-0.5.x DOs are fine because their__ps_namerecords are still on disk; fresh DOs have nothing to fall back to.Reproduction
compatibility_dateolder than2026-03-15(e.g.2026-01-28— this is the date most existing examples incloudflare/agentsand templates currently use).cloudflare/agents,Think.onChatRecoveryschedules_chatRecoveryContinueviathis.schedule(0, ...)whenever a tool execute is interrupted (seepackages/think/src/think.ts:2779).wrangler devafter the schedule was set).Result:
Why the error message is misleading for this case
The
#nameOrThrow()throw atpackages/partyserver/src/index.ts:671handles three real cases but only describes two:__ps_nameis in storage; the alarm fallback in#hydrateNameFromLegacyStoragereads it andthis.nameresolves. No error. Current message correctly addresses this case.idFromString()/newUniqueId(). Genuinely user error. Current message correctly addresses this case.compatibility_date< 2026-03-15. No__ps_nameexists (since 0.5.x stopped writing it).ctx.id.nameisundefinedin the alarm handler (because compat_date predates the runtime change that landed name propagation in alarms). User did everything "right" (usedidFromNamevia routing). The "legacy alarm — reschedule from a fetch handler" advice doesn't fit: the alarm isn't legacy, and rescheduling from a fetch handler doesn't help if the underlying fallback record never existed.This third case is the easiest one to fall into because every example in
cloudflare/agents(and likely most templates / scaffolds) currently uses a pre-cutoffcompatibility_date.Root cause
Two interacting changes:
ctx.id.nameavailable inside alarm handlers (Durable Objects ID docs). This is gated bycompatibility_date.__ps_nameto storage on the assumption that the runtime fix made it unnecessary (changelog excerpt: "Server no longer writes the__ps_namerecord to storage. Existing records remain on disk for backward compatibility and are only read insidealarm()as a fallback for alarms that were scheduled before 2026-03-15").For projects whose
compatibility_dateis on the recent side of the cutoff, the runtime path Just Works and no fallback is needed. For projects on the old side, the runtime doesn't propagate the name and there's no__ps_namerecord to fall back to, sothis.namethrows insideonStart()on alarm wake.Proposed fixes
In rough priority order:
1. Restore a one-time
__ps_namewrite on firstfetch()(recommended)Inside
fetch(), after#ensureInitialized(), ifctx.id.nameis set and no__ps_nameexists yet, write it. Idempotent — fires only once per DO lifetime; after the first write, the record stays. Cost: one extra storageputon the very first fetch a DO ever sees. After that, alarms always have a fallback regardless of compat_date or runtime version.This is strictly safer than the 0.5.x baseline. The perf delta vs. the pre-0.5.x version is one storage put per DO lifetime, not per request — negligible in absolute terms. The price of NOT writing it is silent breakage on every project with an older compat_date that schedules any alarm.
Sketch:
(Or do the existence check + put inside
#hydrateNameFromLegacyStoragesymmetrically — wherever fits the style.)2. Tailor the throw message to distinguish the three cases
#nameOrThrow()atpackages/partyserver/src/index.ts:668-674could check what's actually present and tailor the message. Something like:This is a polish on top of the real fix. With option 1 in place, this message gets seen much less often, but when it is seen it's actionable.
3. Optional: add a startup-time warning
Inside
#ensureInitialized(), if we're on the slow path AND no legacy record exists,console.warnonce per DO with a one-line summary of the likely cause. Probably overkill given option 1 makes the failure mode go away entirely; including for completeness.What I'd avoid
Reverting the 0.5.x change wholesale and writing
__ps_nameon every fetch would re-add a storage put per request. That was the right thing to drop. The fix here is a one-time-per-DO-lifetime write, not per-request.Affected ecosystem
cloudflare/agents(Agent extends Serverfrom partyserver). All examples in that repo currently usecompatibility_date: 2026-01-28or2026-03-06, both pre-cutoff. Originally surfaced viaexamples/agents-as-tools— Think schedules_chatRecoveryContinueon tool-execute interruption, and that alarm wakes the DO post-restart, hitting this exact failure.partysync,partysub,y-partyserver,hono-party— same migration applies (see their respective changelogs in this repo).Workaround for now
Two manual steps in the affected project:
compatibility_dateinwrangler.jsoncto a date after2026-03-15(I used2026-04-15incloudflare/agents'sexamples/agents-as-tools/wrangler.jsonc)..wrangler/state/to clear any orphaned alarm scheduled under the old runtime.Both are one-time and only address the immediate user; the partyserver-side fix is what closes the silent-breakage hazard for everyone.
Context
cloudflare/agentsexamples/agents-as-tools(helpers-as-sub-agents pattern). The Researcher facet's events are streamed back to the parent over DO RPC; tool-execute interruptions trigger Think's chat-recovery flow, which schedules the alarm that eventually exposes this.packages/partyserver/src/index.ts:671packages/partyserver/src/index.ts:548-574(#hydrateNameFromLegacyStorage)packages/partyserver/CHANGELOG.mdOut of scope
cloudflare/agentsexamples' compat_dates. That's a separate, additive cleanup that should happen too, but it doesn't fix the partyserver gap for any project not under our control._chatRecoveryContinuescheduling — that's downstream and works fine ifthis.nameis recoverable.