Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/stream-run-started-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/world-vercel': patch
---

Load the first replay page from non-Turbo `run_started` responses while preserving Turbo's preload opt-out.
1 change: 1 addition & 0 deletions packages/world-vercel/src/events-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const stepCompleted = () => ({
});

const v4Success = () => ({
type: 'event' as const,
eventId: 'evnt_1',
runId: RUN_ID,
createdAt: '2020-01-01T00:00:00.000Z',
Expand Down
96 changes: 61 additions & 35 deletions packages/world-vercel/src/events-v4.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Buffer } from 'node:buffer';
import {
EntityConflictError,
RunExpiredError,
Expand Down Expand Up @@ -398,68 +399,86 @@ describe('createWorkflowRunEventV4 over HTTP', () => {
agent.assertNoPendingInterceptors();
});

it('forwards skipPreload in the run_started frame meta (turbo preload opt-out)', async () => {
it('requests and decodes the first event page for run_started', async () => {
const origin =
WORKFLOW_SERVER_URL_OVERRIDE || 'https://vercel-workflow.com';
const agent = new MockAgent();
agent.disableNetConnect();

// Decode the posted frame's CBOR meta block:
// u32_be(meta_len) || cbor_meta || u32_be(body_len) || body
let capturedMeta: Record<string, unknown> | undefined;
const captureMeta = (rawBody: unknown) => {
const bytes =
typeof rawBody === 'string'
? new TextEncoder().encode(rawBody)
: new Uint8Array(rawBody as ArrayBufferLike);
const metaLen = new DataView(
bytes.buffer,
bytes.byteOffset,
bytes.byteLength
).getUint32(0, false);
capturedMeta = decode(bytes.subarray(4, 4 + metaLen)) as Record<
string,
unknown
>;
};
const input = new TextEncoder().encode('"workflow input"');

agent
.get(origin)
.intercept({
path: '/api/v4/runs/wrun_1/events/run_started',
method: 'POST',
headers: { accept: V4_FRAME_CONTENT_TYPE },
})
.reply(
200,
(opts: { body?: unknown }) => {
captureMeta(opts.body);
return encode({ run: { runId: 'wrun_1', status: 'running' } });
},
Buffer.concat([
encodeFrame(
{
_result: 1,
event: {
eventId: 'evnt_2',
runId: 'wrun_1',
eventType: 'run_started',
},
run: { runId: 'wrun_1', status: 'running' },
},
new Uint8Array()
),
encodeFrame(
{
eventId: 'evnt_1',
runId: 'wrun_1',
eventType: 'run_created',
},
input
),
encodeFrame(
{
eventId: 'evnt_2',
runId: 'wrun_1',
eventType: 'run_started',
},
new Uint8Array()
),
encodeFrame(
{ _end: 1, next: 'eid:evnt_2', hasMore: false },
new Uint8Array()
),
]),
{
headers: {
'x-wf-event-id': 'evnt_1',
'content-type': V4_FRAME_CONTENT_TYPE,
'x-wf-event-id': 'evnt_2',
'x-wf-run-id': 'wrun_1',
'x-wf-created-at': '2026-06-10T00:00:00.000Z',
},
}
);

await createWorkflowRunEventV4(
const result = await createWorkflowRunEventV4(
{
runId: 'wrun_1',
eventType: 'run_started',
specVersion: 5,
skipPreload: true,
},
{ token: 'test-token', dispatcher: agent }
);

expect(capturedMeta?.eventType).toBe('run_started');
expect(capturedMeta?.skipPreload).toBe(true);
expect(result.type).toBe('event-page');
if (result.type !== 'event-page') throw new Error('expected event page');
expect(result.body.run).toMatchObject({ status: 'running' });
expect(result.page.events).toHaveLength(2);
expect(result.page.events[0].body).toEqual(input);
expect(result.page.next).toBe('eid:evnt_2');
expect(result.page.hasMore).toBe(false);
agent.assertNoPendingInterceptors();
});

it('omits skipPreload from the frame meta when not set (default / old SDK parity)', async () => {
it('keeps the CBOR response when run_started skips the preload', async () => {
const origin =
WORKFLOW_SERVER_URL_OVERRIDE || 'https://vercel-workflow.com';
const agent = new MockAgent();
Expand All @@ -471,6 +490,7 @@ describe('createWorkflowRunEventV4 over HTTP', () => {
.intercept({
path: '/api/v4/runs/wrun_1/events/run_started',
method: 'POST',
headers: (headers) => headers.accept === '*/*',
})
.reply(
200,
Expand All @@ -489,20 +509,26 @@ describe('createWorkflowRunEventV4 over HTTP', () => {
},
{
headers: {
'x-wf-event-id': 'evnt_1',
'x-wf-event-id': 'evnt_2',
'x-wf-run-id': 'wrun_1',
'x-wf-created-at': '2026-06-10T00:00:00.000Z',
},
}
);

await createWorkflowRunEventV4(
{ runId: 'wrun_1', eventType: 'run_started', specVersion: 5 },
const result = await createWorkflowRunEventV4(
{
runId: 'wrun_1',
eventType: 'run_started',
specVersion: 5,
skipPreload: true,
},
{ token: 'test-token', dispatcher: agent }
);

expect(capturedMeta?.eventType).toBe('run_started');
expect('skipPreload' in (capturedMeta ?? {})).toBe(false);
expect(capturedMeta?.skipPreload).toBe(true);
expect(result.type).toBe('event');
expect(result.body.run).toMatchObject({ status: 'running' });
agent.assertNoPendingInterceptors();
});

Expand Down
Loading
Loading