Skip to content

Fix: restore __ps_name storage hydrate for Agents facets (0.5.1)#381

Merged
threepointone merged 3 commits intomainfrom
fix-facet-hydrate
Apr 24, 2026
Merged

Fix: restore __ps_name storage hydrate for Agents facets (0.5.1)#381
threepointone merged 3 commits intomainfrom
fix-facet-hydrate

Conversation

@threepointone
Copy link
Copy Markdown
Collaborator

@threepointone threepointone commented Apr 24, 2026

Summary

0.5.0 moved the legacy __ps_name storage hydrate out of #ensureInitialized() and into alarm() only, with the reasoning being that ctx.id.name covers every other entry point for normal DOs addressed via idFromName()/getByName().

That reasoning missed Cloudflare Agents facets. Facet DOs are spawned via ctx.facets.get(facetKey) — not idFromName() — so their ctx.id.name is undefined. Agents bootstraps the facet's name by writing __ps_name to storage and calling this.__unsafe_ensureInitialized(), relying on PartyServer to read the storage record back:

// From cloudflare/agents packages/agents/src/index.ts _cf_initAsFacet
await Promise.all([
  this.ctx.storage.put("cf_agents_is_facet", true),
  this.ctx.storage.put("__ps_name", name),
  this.ctx.storage.put("cf_agents_parent_path", parentPath)
]);
await this.__unsafe_ensureInitialized();

With 0.5.0 this breaks: #_name stays undefined, onStart() throws on any this.name read, and subsequent fetcher.fetch(...) calls from _cf_forwardToFacet also throw.

Fix

Move the legacy hydrate from alarm() into #ensureInitialized(), still gated on !ctx.id.name && !#_name — the happy path (normal idFromName/getByName DOs) still pays zero storage reads since ctx.id.name is set and the gate short-circuits.

Name resolution priority: ctx.id.name > x-partykit-room header > legacy __ps_name storage record.

Changes

  • #ensureInitialized() does the conditional storage hydrate for any entry point that needs it (fetch, alarm, websocket handlers, __unsafe_ensureInitialized via RPC).
  • Server.fetch() pre-populates #_name from the x-partykit-room header before calling #ensureInitialized(), so onStart() can read this.name regardless of which source provided it. A final resolvability check after #ensureInitialized() throws with a useful error when no source could supply the name.
  • Server.alarm() is simplified — no duplicate hydrate call needed.
  • setName() @deprecated docblock is softened. It's still the right primitive for framework bootstrap of non-idFromName DOs (e.g. Agents facets); deprecating it outright was misleading.
  • Agents' current direct-storage-write pattern continues to work without changes on their side.

Test plan

  • New FacetLikeBootstrapServer DO + 2 tests covering the facet pattern: storage-write-then-ensureInitialized, plus cold-wake fetch recovery
  • New HeaderOnlyOnStartServer DO + test covering the newUniqueId + x-partykit-room header + onStart()-reads-this.name path (regression guard for the ordering bug in the first pass of this PR)
  • All 54 partyserver tests pass
  • Full repo test suite passes (partyserver, partysub, partywhen, partytracks, y-partyserver)
  • Types clean, lint clean, format clean
  • Follow-up: notify Cloudflare Agents team to bump their partyserver peer range to >=0.5.1 once this ships

Performance

Zero change for normal DOs. The conditional gate (!ctx.id.name && !#_name) short-circuits before any storage read whenever the name is already known — which is the happy path for every idFromName/getByName user. Only DOs that truly need the fallback (facets, pre-2026-03-15 alarms, newUniqueId + header callers where the name doesn't match storage) pay for one storage read on first entry.

0.5.0 moved the legacy __ps_name storage hydrate into `alarm()` only,
which broke Cloudflare Agents facets and any other framework that
writes __ps_name directly before calling __unsafe_ensureInitialized().

Facet DOs are spawned via `ctx.facets.get(...)` rather than
`idFromName()`, so their `ctx.id.name` is `undefined`. They were
relying on PartyServer's `#ensureInitialized()` calling
`#hydrateNameFromStorage()` to populate `this.name` before `onStart()`.

This PR:

- Moves the hydrate from `alarm()` into `#ensureInitialized()`, gated on
  `!ctx.id.name && !#_name` so the happy path (normal idFromName DOs)
  still pays zero storage reads.
- Simplifies `Server.fetch()` to delegate the hydrate to
  `#ensureInitialized()`. The x-partykit-room header remains as a
  last-resort fallback when neither ctx.id.name nor storage has a value.
- Simplifies `Server.alarm()` — no separate hydrate call needed.
- Softens `@deprecated` on `setName()` to clarify it's still the right
  primitive for framework bootstrap of non-idFromName DOs (e.g. facets).
- Adds FacetLikeBootstrapServer + tests covering the fetch-time and
  RPC-time hydrate paths.

Made-with: Cursor
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 24, 2026

🦋 Changeset detected

Latest commit: ed252d6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
partyserver Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 24, 2026

Open in StackBlitz

hono-party

npm i https://pkg.pr.new/cloudflare/partykit/hono-party@381

partyfn

npm i https://pkg.pr.new/cloudflare/partykit/partyfn@381

partyserver

npm i https://pkg.pr.new/cloudflare/partykit/partyserver@381

partysocket

npm i https://pkg.pr.new/cloudflare/partykit/partysocket@381

partysub

npm i https://pkg.pr.new/cloudflare/partykit/partysub@381

partysync

npm i https://pkg.pr.new/cloudflare/partykit/partysync@381

partytracks

npm i https://pkg.pr.new/cloudflare/partykit/partytracks@381

partywhen

npm i https://pkg.pr.new/cloudflare/partykit/partywhen@381

y-partyserver

npm i https://pkg.pr.new/cloudflare/partykit/y-partyserver@381

commit: ed252d6

The first pass of PR #381 ran #ensureInitialized() BEFORE applying the
x-partykit-room header fallback. For DOs with ctx.id.name undefined and
no __ps_name storage record, that meant onStart() ran against an unset
#_name and threw if it read this.name.

Restore the 0.5.0 ordering: pre-populate #_name from the header before
ensureInitialized runs, so onStart() sees the correct name regardless
of which source provided it. The resolvability throw is now a final
check after ensureInitialized.

Resolution priority is now: ctx.id.name > x-partykit-room header >
legacy __ps_name storage. Storage only wins when neither of the other
two supplies the name.

Adds HeaderOnlyOnStartServer + a regression test covering the
newUniqueId + x-partykit-room + onStart-reads-name path.

Made-with: Cursor
@threepointone threepointone merged commit 79d5c6b into main Apr 24, 2026
6 checks passed
@threepointone threepointone deleted the fix-facet-hydrate branch April 24, 2026 20:51
@github-actions github-actions Bot mentioned this pull request Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant