Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/agents/agent-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,15 @@ There's also `this.broadcast` that sends a WS message to all connected clients (

### `this.name`

It's hard to get a Durable Object's `name` from within it. `partyserver` tries to make it available in `this.name` but it's not a perfect solution. Read more about it [here](https://github.com/cloudflare/workerd/issues/2240).
Since [2026-03-15](https://developers.cloudflare.com/changelog/post/2026-03-15-durable-object-id-name/), the Workers runtime populates `ctx.id.name` inside a Durable Object addressed via `idFromName()` or `getByName()`, including in alarm handlers. Constructor-time availability isn't spelled out in the docs, but workerd's own tests pin it ([workerd#6421](https://github.com/cloudflare/workerd/pull/6421)), as do `partyserver`'s runtime-contract tests. `partyserver` reads `ctx.id.name` first, so for named access `this.name` resolves natively with no extra machinery.

`ctx.id.name` is still `undefined` in these cases (see [the DO id docs](https://developers.cloudflare.com/durable-objects/api/id/#name)):

- the object is addressed via `idFromString()` (even if the id was originally created with `idFromName()`) or `newUniqueId()` — deliberate design, not a gap;
- the name is longer than 1,024 bytes;
- the alarm firing was scheduled before 2026-03-15, or was scheduled from a context that itself had no name (reschedule it from a `fetch()` or RPC handler where the name is available).

For those cases `partyserver` falls back to a legacy name record in storage (written automatically during named-access initialization, or by the `setName()` bootstrap for raw-id DOs), and `this.name` throws if no name can be resolved at all.

## Layer 2: Agent

Expand Down
2 changes: 1 addition & 1 deletion docs/shell/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ All options are passed as a single object to `new Workspace(options)`.

### Lazy name resolution

In Durable Objects, `this.name` is not available at class field initialization time. Pass a function to defer evaluation:
In Durable Objects, `this.name` may not be resolvable at class field initialization time (for example, reading it throws when the object was addressed by raw id instead of by name). Pass a function to defer evaluation:

```typescript
class MyAgent extends Agent<Env> {
Expand Down
55 changes: 55 additions & 0 deletions packages/agents/src/tests/name-resolution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { env } from "cloudflare:workers";
import { runInDurableObject } from "cloudflare:test";
import { TEST_MESSAGES } from "./shared/test-utils";
import type { McpAgent } from "../mcp";

// Since 2026-03-15 the Workers runtime populates ctx.id.name inside a
// Durable Object addressed via idFromName()/getByName(), from construction
// onward — the name no longer has to be smuggled in through storage.
// https://developers.cloudflare.com/changelog/post/2026-03-15-durable-object-id-name/
// https://developers.cloudflare.com/durable-objects/api/id/#name
//
// These tests pin that behavior: a cold-woken agent whose first-ever entry
// point is a native DO RPC call resolves this.name purely from ctx.id.name,
// with NO __ps_name seeding and no setName() bootstrap. (The "Cold Wake
// Initialization" tests in mcp/transports/rpc.test.ts still seed __ps_name,
// but they address via idFromName(), so under the current runtime the seed
// is inert — the legacy fallback paths it would feed are pinned upstream in
// partyserver's own test suite, not here.)
describe("this.name resolution from ctx.id.name", () => {
it("resolves this.name on a cold agent addressed via idFromName(), without __ps_name seeding", async () => {
// The "rpc:" prefix is load-bearing: McpAgent parses this.name as
// `${transport}:${sessionId}`, and handleMcpMessage requires the RPC
// transport.
const doName = `rpc:ctx-id-name-${crypto.randomUUID()}`;
const id = env.MCP_OBJECT.idFromName(doName);
const stub = env.MCP_OBJECT.get(id) as DurableObjectStub<McpAgent>;

// No storage seeding — the first contact with this DO is a native RPC
// entry point that bypasses fetch/alarm/webSocket paths. Name resolution
// must come from ctx.id.name alone.
const response = await stub.handleMcpMessage(TEST_MESSAGES.initialize);

expect(response).toBeDefined();
expect(response).toHaveProperty("result");

const name = await runInDurableObject(stub, (instance) => instance.name);
expect(name).toBe(doName);
});

it("resolves this.name on a cold agent addressed via getByName(), without __ps_name seeding", async () => {
const doName = `rpc:ctx-id-name-${crypto.randomUUID()}`;
const stub = env.MCP_OBJECT.getByName(
doName
) as DurableObjectStub<McpAgent>;

const response = await stub.handleMcpMessage(TEST_MESSAGES.initialize);

expect(response).toBeDefined();
expect(response).toHaveProperty("result");

const name = await runInDurableObject(stub, (instance) => instance.name);
expect(name).toBe(doName);
});
});
Loading