Files
Bryan Hunter bdeb2bf1b1 fix(workflow): isolate chat serializers from node runtime (#806)
## Failure

Workflow SDK `5.0.0-beta.40` produces an invalid workflow bundle when a
Chat SDK serializable class such as `Message`, `ThreadImpl`, or
`ChannelImpl` crosses a workflow step boundary.

The Workflow compiler imports the emitted module containing each class
to register its `@workflow/serde` methods. In Chat SDK `4.37.0`, tsup
emits those classes in `dist/index.js`. The root entry also imports the
conversation-scoping implementation added in #751, which uses
`AsyncLocalStorage` from `node:async_hooks`. Serializer registration
therefore pulls Node-only code into the sandboxed workflow bundle before
any workflow or step executes.

Build warning:

```text
Serde warning for classes "ChannelImpl", "Message", "ThreadImpl":
Workflow bundle contains Node.js built-in imports: async_hooks.
These will fail at runtime in the workflow sandbox.
```

Deployed workflows then fail during module initialization:

```text
var import_async_hooks = require("async_hooks");
                         ^

ReferenceError: require is not defined
```

## Minimal reproduction

```json
{
  "dependencies": {
    "chat": "4.37.0",
    "workflow": "5.0.0-beta.40"
  }
}
```

```ts
import { Message } from "chat";

async function createMessageStep(value: string): Promise<Message> {
  "use step";

  return new Message({
    id: "message",
    threadId: "slack:C123:123.456",
    text: value,
    formatted: {
      type: "root",
      children: [
        {
          type: "paragraph",
          children: [{ type: "text", value }],
        },
      ],
    },
    raw: {},
    author: {
      userId: "U123",
      userName: "user",
      fullName: "User",
      isBot: false,
      isMe: false,
    },
    metadata: { dateSent: new Date(), edited: false },
    attachments: [],
  });
}

export async function testWorkflow(value: string): Promise<string> {
  "use workflow";

  const message = await createMessageStep(value);
  return message.text;
}
```

Running `workflow build` on `4.37.0` emits the warning; deploying the
output produces the runtime failure above.

## Fix

- Add a dedicated `chat/serialization` package entry for `Message`,
`ThreadImpl`, `ChannelImpl`, `reviver`, and their serialized DTO types.
- Make serializer code a second tsup entry and explicitly enable
splitting. The serializer-bearing classes are now emitted into a shared
chunk with no dependency on `Chat` or its Node-only conversation
context.
- Preserve the existing root exports and automatic `@workflow/serde`
behavior. Existing `import { Message } from "chat"` workflow code
remains valid.
- Add a post-build module-graph assertion that fails if any emitted
serializer registration can transitively import a Node.js builtin.
- Test against Workflow SDK `5.0.0-beta.40`, the compiler version that
exposed the invalid bundle.
- Add a minor changeset for the fixed-version Chat SDK packages,
producing the `4.38.0` release line.

After the change, the emitted serializer classes live in a sandbox-safe
shared chunk while `AsyncLocalStorage` remains in a separate Node
runtime chunk. The exact reproduction compiles successfully with `5
steps, 1 workflow` and no Serde warning.

## Control cases

The failure requires a serializable Chat class to cross a durable
boundary. These cases were already safe and remain unchanged:

- `AsyncLocalStorage` used entirely inside a `"use step"` function.
- A Chat `Message` created and consumed within one step while returning
plain data.
- Request handlers that convert Chat objects to plain workflow DTOs
before starting a workflow.
- `@vercel/sandbox` used entirely inside a step.

## Validation

- Committed beta.40 reproduction fixture: type-correct and compiled
during every Chat package build with no Node builtin / Serde warning.
- Emitted serializer module graph: no transitive Node.js builtins.
- Chat package: 1,113 tests pass.
- Chat package typecheck passes.
- Repository formatting and lint checks pass.
- Package build passes.

Full repository validation reaches the pre-existing `knip` baseline and
reports unrelated unused dependencies and unlisted binaries in examples
and adapter packages.

---------

Signed-off-by: bryan-hunter <bryan.hunter@vercel.com>
2026-08-10 09:03:49 -05:00

41 lines
1.1 KiB
JSON

{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignoreDependencies": ["@biomejs/biome", "@vercel/oidc"],
"ignore": [
"apps/docs/**/*",
"packages/create-chat-sdk/_template/next.config.ts",
"packages/create-chat-sdk/_template/src/app/api/webhooks/[platform]/route.ts",
"examples/nextjs-chat/src/app/.well-known/workflow/**/*"
],
"workspaces": {
"packages/chat": {
"entry": ["fixtures/workflow-serialization.ts"]
},
"examples/nuxt-chat": {
"nuxt": {
"entry": [
"nuxt.config.ts",
"app/app.vue",
"app/error.vue",
"app/pages/**/*.{vue,jsx,tsx}",
"app/layouts/**/*.{vue,jsx,tsx}",
"app/middleware/**/*.ts",
"app/plugins/**/*.ts",
"app/assets/css/**/*.css",
"server/api/**/*.ts",
"server/middleware/**/*.ts",
"server/plugins/**/*.ts",
"server/routes/**/*.ts",
"server/lib/**/*.ts"
]
},
"ignoreDependencies": ["vue-tsc"]
}
},
"rules": {
"duplicates": "off",
"types": "off",
"optionalPeerDependencies": "off"
}
}