From 7211ad50befdcd3d9fbc1709b2ee460e703d7090 Mon Sep 17 00:00:00 2001 From: octo-patch Date: Tue, 28 Apr 2026 12:13:18 +0800 Subject: [PATCH] fix(core): gracefully handle runtimes that omit AsyncLocalStorage.enterWith() (fixes #2055) Cloudflare Workers intentionally omit enterWith() from their AsyncLocalStorage implementation because it mutates context across the entire remaining async chain, which is unsafe under concurrent requests. Calling it throws immediately, crashing Stagehand during init() before any browser work begins. Wrap the enterWith() call in a try/catch so the init() path stays safe on any runtime. The returned FlowLoggerContext is still valid; callers that need ALS propagation use loggerContext.run() via runWithLogging() and withContext(), which are already runtime-safe. Adds a unit test that simulates the missing enterWith() and asserts that FlowLogger.init() completes without throwing and returns a valid context. Co-Authored-By: Octopus --- packages/core/lib/v3/flowlogger/FlowLogger.ts | 10 ++++++- .../unit/flowlogger-capturing-llm.test.ts | 28 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/core/lib/v3/flowlogger/FlowLogger.ts b/packages/core/lib/v3/flowlogger/FlowLogger.ts index 38c974924..9ced6ac12 100644 --- a/packages/core/lib/v3/flowlogger/FlowLogger.ts +++ b/packages/core/lib/v3/flowlogger/FlowLogger.ts @@ -413,7 +413,15 @@ export class FlowLogger { parentEvents: [], }; - loggerContext.enterWith(ctx); + try { + loggerContext.enterWith(ctx); + } catch { + // Some runtimes (e.g. Cloudflare Workers) do not implement enterWith() + // because it mutates context across concurrent requests, which is unsafe + // in that environment. Fall through: the context is still returned and + // callers that need ALS can use loggerContext.run() via runWithLogging() + // or withContext(). + } return ctx; } diff --git a/packages/core/tests/unit/flowlogger-capturing-llm.test.ts b/packages/core/tests/unit/flowlogger-capturing-llm.test.ts index 6bdf375c7..085e1d37b 100644 --- a/packages/core/tests/unit/flowlogger-capturing-llm.test.ts +++ b/packages/core/tests/unit/flowlogger-capturing-llm.test.ts @@ -1,5 +1,7 @@ +import { AsyncLocalStorage } from "node:async_hooks"; import { describe, expect, it } from "vitest"; import { FlowLogger } from "../../lib/v3/flowlogger/FlowLogger.js"; +import { EventEmitterWithWildcardSupport } from "../../lib/v3/flowlogger/EventEmitter.js"; describe("flow logger llm logging", () => { it("no-ops direct llm logging calls when no flow context is active", () => { @@ -47,4 +49,30 @@ describe("flow logger llm logging", () => { text: "done", }); }); + + it("FlowLogger.init() does not throw when enterWith() is not implemented (e.g. Cloudflare Workers)", () => { + // Simulate a runtime that omits enterWith() from AsyncLocalStorage. + const originalEnterWith = AsyncLocalStorage.prototype.enterWith; + Object.defineProperty(AsyncLocalStorage.prototype, "enterWith", { + value: undefined, + configurable: true, + writable: true, + }); + + try { + const bus = new EventEmitterWithWildcardSupport(); + let ctx: ReturnType | undefined; + expect(() => { + ctx = FlowLogger.init("session-cloudflare", bus); + }).not.toThrow(); + // The returned context must still be valid even without ALS support. + expect(ctx).toMatchObject({ sessionId: "session-cloudflare" }); + } finally { + Object.defineProperty(AsyncLocalStorage.prototype, "enterWith", { + value: originalEnterWith, + configurable: true, + writable: true, + }); + } + }); });