Skip to content

Document facet bootstrap pattern, no runtime change#386

Merged
threepointone merged 1 commit into
mainfrom
document-facet-bootstrap
Apr 26, 2026
Merged

Document facet bootstrap pattern, no runtime change#386
threepointone merged 1 commit into
mainfrom
document-facet-bootstrap

Conversation

@threepointone
Copy link
Copy Markdown
Collaborator

Summary

Adds documentation and end-to-end test coverage for using PartyServer with Durable Object Facets. No runtime behavior change — the Server class (name getter, setName(), #hydrateNameFromLegacyStorage, #ensureInitialized) is byte-identical to 0.5.2.

What this fixes (and what it doesn't)

Facets spawned via ctx.facets.get(name, factory) without an explicit id in FacetStartupOptions inherit the parent DO's ctx.id — including ctx.id.name. PartyServer's name getter reads ctx.id.name straight through, so on an implicit-id facet this.name returns the parent's name rather than the facet's logical name. This is faithful to the workerd contract but it's almost never what framework authors expect.

The fix is at the call site, not in PartyServer: pass id: someBoundDoNamespace.idFromName(facetName) (or the equivalent via ctx.exports[BoundClassName].idFromName(facetName)) to ctx.facets.get(...). The facet then gets its own native ctx.id.name === facetName and PartyServer's name getter does the right thing automatically. No setName() is required, no __ps_name storage record is written, and cold-wake recovery happens for free because the factory re-runs and idFromName is deterministic.

What ships

  • README.md — rewrote the .name description; added a new "Using PartyServer with Durable Object Facets" section that walks through the recommended pattern with a code example, calls out the implicit-id footgun, and warns that plain-string id values are not a substitute for idFromName(facetName) (workerd treats string ids as idFromString-like, so the resulting facet has no ctx.id.name).
  • setName() docstring — clarified that facets are NOT a setName() use case; pointed readers at the explicit-id pattern. The setName() ctx.id.name mismatch throw is preserved as a typo guard for the idFromName happy path.
  • #hydrateNameFromLegacyStorage docstring — corrected to no longer claim that Cloudflare Agents facets use this fallback.
  • End-to-end test coverageFacetParent / FacetChild fixtures plus a describe("facets …") block in index.test.ts that covers:
    • implicit-id flow (pins the runtime contract: this.name returns the parent's name; documentation-via-test so future readers aren't surprised)
    • explicit-id flow via env binding (env.SomeNs.idFromName)
    • explicit-id flow via ctx.exports[BoundClass].idFromName (no env knowledge required — the recommended pattern for framework code)
    • plain-string id (negative case: produces a facet with ctx.id.name === undefined, this.name throws)
    • cold-wake recovery on the explicit-id path (no storage record involved; deterministic factory re-run carries the name)
  • Sibling packages (hono-party, partysub, partysync, partywhen, y-partyserver) — devDependency bumped from partyserver: ^0.5.1^0.5.2 for consistency.
  • CHANGELOG.md — minor formatting cleanup.
  • Changeset.changeset/facet-name-resolution.md, patch bump.

Why docs/tests instead of a runtime fix

The previous draft of this work added a runtime override mechanism in Server (getter precedence flip, hydrate gate change, removed setName guard). That worked but introduced behavior changes for non-facet users (silent override on setName() mismatch, ~1ms storage GET per cold wake on every DO). With the discovery that FacetStartupOptions.id is the documented Cloudflare API for "give this facet its own identity," the override mechanism is unnecessary. We're going with the runtime contract instead of around it, which:

  • preserves byte-identical behavior for all existing users;
  • is forward-compatible with future workerd facet capabilities tied to ctx.id;
  • doesn't require sibling frameworks to adopt new partyserver patterns.

The Cloudflare Agents repo is migrating its _cf_initAsFacet to pass id to ctx.facets.get() in a separate PR; this PR is the partyserver-side documentation that makes that pattern discoverable for everyone else.

Test plan

  • npm run check:test passes (60/60, including 3 new facet tests against the real ctx.facets.get() API)
  • npm run check:type passes (all 41 projects)
  • npm run check:lint passes
  • End-to-end validation against the actual consumer (cloudflare/agents) using a local-tarball install: full workers test project (1006 tests) passes
  • Reviewer to spot-check the README facet section for clarity

Made with Cursor

Adds README + setName() docstring guidance pointing facet users at the
explicit FacetStartupOptions.id pattern, plus an end-to-end test fixture
(FacetParent / FacetChild) that pins both the implicit-id workerd
contract and the recommended explicit-id behavior. Per the Cloudflare
facet docs, passing `id: someBoundDoNamespace.idFromName(facetName)` to
ctx.facets.get() gives the facet its own ctx.id.name, so partyserver's
existing name getter does the right thing without any setName/__ps_name
override mechanism. The runtime is byte-identical to 0.5.2.

Also bumps sibling packages' devDependency on partyserver from ^0.5.1
to ^0.5.2 for consistency, and tidies CHANGELOG formatting.

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

changeset-bot Bot commented Apr 26, 2026

🦋 Changeset detected

Latest commit: 7b150f8

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 26, 2026

Open in StackBlitz

hono-party

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

partyfn

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

partyserver

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

partysocket

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

partysub

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

partysync

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

partytracks

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

partywhen

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

y-partyserver

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

commit: 7b150f8

@threepointone threepointone merged commit 8a3bc02 into main Apr 26, 2026
6 checks passed
@threepointone threepointone deleted the document-facet-bootstrap branch April 26, 2026 00:26
@github-actions github-actions Bot mentioned this pull request Apr 26, 2026
threepointone added a commit to cloudflare/agents that referenced this pull request Apr 26, 2026
* chore(deps): bump partyserver to ^0.5.3 and roll up other dep updates

Bumps partyserver from ^0.4.1 to ^0.5.3 across the workspace (root and
all examples / experimental / packages / sites that referenced it),
plus a sweep of other dependency updates that landed during this
maintenance pass:

- @cloudflare/vite-plugin: 1.33.0 -> 1.33.2
- @cloudflare/vitest-pool-workers: 0.14.8 -> 0.15.0
- @cloudflare/workers-types: 4.20260420.1 -> 4.20260425.1
- @openai/agents: 0.8.4 -> 0.8.5
- @vitest/{browser,browser-playwright,runner}: 4.1.4 -> 4.1.5
- nx: 22.6.5 -> 22.7.0
- (plus consumer-side bumps in examples/* and experimental/*)

The partyserver bump is the load-bearing one: 0.5.3 ships docs and
test coverage for using PartyServer with Durable Object Facets via
explicit FacetStartupOptions.id, which the next commit consumes in
the agents framework itself. Runtime behavior of partyserver is
unchanged from 0.5.2, so this commit alone is non-breaking.

No code changes; just package.json + package-lock.json.

Made-with: Cursor

* Migrate facet bootstrap to explicit FacetStartupOptions.id

Drops the __ps_name storage write and setName() bootstrap from
_cf_initAsFacet in favor of the documented Cloudflare facet API:
pass id: parentNs.idFromName(name) to ctx.facets.get() so the facet
gets its own ctx.id.name. PartyServer's existing 0.5.x name getter
then resolves this.name correctly without any override mechanism,
storage write, or cold-wake hydrate cost.

Background: facets spawned without an explicit id inherit the parent
DO's ctx.id, so on a facet ctx.id.name was the parent's name, and
this.name silently misreported. The fix lives at the call site, not
in partyserver. See #1385 for the full investigation
and cloudflare/partykit#386 for the partyserver-side documentation.

Changes:

- _cf_resolveSubAgent: pass id: parentNs.idFromName(name) where
  parentNs = ctx.exports[this.constructor.name]. The parent agent
  is always bound (it's the entry-point DO), so its namespace is
  always available via ctx.exports. The id is opaque + a name;
  nothing routes through the namespace at runtime for facets.

- _cf_initAsFacet: drops the __ps_name storage write entirely;
  drops the setName(name) call. Just persists cf_agents_is_facet
  and cf_agents_parent_path, then __unsafe_ensureInitialized() to
  fire onStart(). Adds a defensive `this.name !== name` guard so
  any future bug in the parent's id construction surfaces with a
  clear error instead of silently mis-identifying the facet.

- alarm() docstring: updated to reflect that this.name resolves
  from ctx.id.name on the happy path (including for facets).

- New error-path coverage: if the parent class isn't bound as a DO
  namespace, throw a descriptive error pointing at wrangler.jsonc.
  If this.constructor.name looks minified (heuristic regex), append
  a hint about preserving class names (e.g. esbuild keepNames).

- New tests: TestUnboundParentAgent and TestMinifiedNameParentAgent
  fixtures (parent classes exported under a different name from
  their declaration) exercise both error paths. Existing test "should
  set this.name to the facet name" already covers the happy path.

- MCP test cleanup: removes vestigial setName("default") + onStart()
  call pairs from create-oauth-provider, oauth2-mcp-client, and
  wait-connections-e2e tests. These were originally needed for
  partyserver 0.4.x bootstrap but became actual ctx.id.name
  mismatches under partyserver 0.5.x's strict guard. Renamed setName
  args to match idFromName names where the test addresses by name.

All 1008 tests pass (up from 1006 — the 2 new error-path tests).
The full workers test project, all sub-agent tests, alarms tests,
and MCP tests are green against the published partyserver 0.5.3.

Closes #1385

Made-with: Cursor

* Add changeset for facet bootstrap migration (minor)

Made-with: Cursor

* changeset: patch instead of minor

Made-with: Cursor

* Update agents-facet-explicit-id.md

* docs: parent-class requirements + freshen test comment

Adds a parent-class requirements note to docs/sub-agents.md describing
the implicit contract that the parent must be bound as a DO namespace
and have its class name preserved by the bundler. This makes the new
unbound-parent / minified-name error path discoverable in the docs.

Also clarifies the hibernation note (this.name is now carried by
ctx.id, not hydrated from storage), and fixes a stale comment in the
abort-and-re-access test that still referenced `__ps_name` (now only
the agent-specific keys are written by _cf_initAsFacet).

Made-with: Cursor

* fix(types): tighten FacetCapableCtx.exports to express runtime contract

Per code review feedback on PR #1393: the previous type
`Record<string, DurableObjectClass>` was too narrow and didn't
explain why `ctx.exports[parentClassName].idFromName(...)` works
for facet-only classes that aren't bound in
`durable_objects.bindings` (e.g. `OuterSubAgent` spawning
`InnerSubAgent` in the test fixtures).

The workerd runtime contract is: any class registered via
`migrations.new_sqlite_classes` (or `new_classes`) — bindings or
not — is exposed in ctx.exports as both a `DurableObjectClass` AND
a `DurableObjectNamespace`. The intersection is what makes the
explicit-id facet bootstrap work for nested sub-agents.

Updates the FacetCapableCtx.exports type to
`Record<string, (DurableObjectClass & DurableObjectNamespace) | undefined>`
with a docstring explaining the runtime semantics, and drops the
now-redundant `as unknown as DurableObjectNamespace | undefined`
cast at the call site in `_cf_resolveSubAgent`.

Empirically verified: 47/47 sub-agent tests still pass, all 75
typecheck projects green.

Made-with: Cursor

* refactor(types): use DurableObjectFacets from workers-types

Drops our hand-rolled inline facets shape in favor of
DurableObjectFacets from @cloudflare/workers-types now that the bumped
peer (^4.20260425.1) ships it natively. Also adds a top-of-interface
docstring explaining why FacetCapableCtx exists at all (we widen
ctx.exports from the MainModule-keyed Cloudflare.Exports to a generic
Record because we can't see the consumer worker's main module from
inside this library).

No runtime change. 79/79 sub-agent tests pass, all 75 typecheck
projects green.

Made-with: Cursor
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