Repository navigation
instrumentAgentWithSentry: @callable() methods are lost after a constructor throws once #25728
Description
Package: @sentry/cloudflare 11.4.0, with agents 0.30.1, Workers compatibility date 2026-10-01
What happens
If an Agent's constructor throws once (in our case the Durable Objects free tier's rows-read limit during storage initialization), the next construction of the same class loses its @callable() methods. RPC calls from a connected client then fail with Method callTool is not callable.
Why
In constructInstrumentedDurableObject (build/*/durableobject.js), methodsBeforeConstruction is resolved from the prototype on every construction until frameworkManagedMethodsCache holds an entry for that prototype. That entry is only written after Reflect.construct returns. When the constructor throws, the Agents SDK has already replaced the prototype's methods with its wrapped versions, but no cache entry is written. On the next construction, Sentry snapshots the wrapped methods as the originals, so it no longer recognizes them as framework-managed, and the callable registration is lost.
Reproduction (vitest-pool-workers)
The test needs a Durable Object binding named MyAgent in the Worker's test config, bound to any Agent class; it only borrows that object's state, and the test's own RecoveryAgent does the rest.
import { runInDurableObject } from "cloudflare:test";
import { env } from "cloudflare:workers";
import * as Sentry from "@sentry/cloudflare";
import { Agent, callable } from "agents";
import { expect, test } from "vitest";
test("callable methods survive construction retry after a constructor failure", async () => {
await runInDurableObject(env.MyAgent.getByName(crypto.randomUUID()), async (_obj, state) => {
let fail = true;
class RecoveryAgent extends Agent<Env> {
constructor(ctx: DurableObjectState, bindings: Env) {
super(ctx, bindings);
if (fail) {
fail = false;
throw new Error("Exceeded allowed rows read in Durable Objects free tier.");
}
}
@callable()
async read() {
return "ready";
}
}
const Wrapped = Sentry.instrumentAgentWithSentry(() => ({ dsn: "" }), RecoveryAgent);
expect(() => new Wrapped(state, env)).toThrow("Exceeded allowed rows read");
const recovered = new Wrapped(state, env);
expect(recovered.getCallableMethods().has("read")).toBe(true); // fails on 11.4.0
});
});Workaround we are running
Cache the pre-construction snapshot per prototype, so a retry after a failure reuses the first snapshot:
- const methodsBeforeConstruction = cachedFrameworkManagedMethods ? void 0 : resolvePrototypeMethods(prototype);
+ let methodsBeforeConstruction = methodsBeforeConstructionCache.get(prototype);
+ if (!cachedFrameworkManagedMethods && !methodsBeforeConstruction) {
+ methodsBeforeConstruction = resolvePrototypeMethods(prototype);
+ methodsBeforeConstructionCache.set(prototype, methodsBeforeConstruction);
+ }
...
const frameworkManagedMethodsCache = /* @__PURE__ */ new WeakMap();
+const methodsBeforeConstructionCache = /* @__PURE__ */ new WeakMap();Happy to open a PR with this and the test if the approach looks right.
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsNo status
Activity
linear-code commentedon Oct 24, 2026
JS-3996
logaretm commentedon Oct 26, 2026
Thanks for the report, feel free to open a PR!