Skip to content

Alarms on fresh 0.5.x DOs with pre-2026-03-15 compat date fail to recover this.name #390

Description

@threepointone

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

  1. 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).
  2. 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).
  3. Trigger the alarm-scheduling path (e.g. let a tool execute fail, or restart wrangler dev after the schedule was set).
  4. 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:

  1. 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.
  2. DOs addressed via idFromString() / newUniqueId(). Genuinely user error. Current message correctly addresses this case.
  3. 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:

  1. 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).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions