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
6 changes: 6 additions & 0 deletions .changeset/fix-load-events-cursor-null.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/core": patch
"workflow": patch
---

Fix spurious "Event cursor missing after initial load" warning
17 changes: 16 additions & 1 deletion .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@
"bright-discovery-talk",
"bright-hooks-share",
"bright-pears-drum",
"bright-waits-refresh",
"builders-discovery-fixes",
"bundle-aliased-project-local-helpers",
"clever-wombats-drop",
"cold-lands-boil",
"cool-cups-greet",
"corrupted-event-log-code",
"curvy-dingos-cry",
"dirty-bees-notice",
"docs-rendered-link-lint",
Expand All @@ -85,9 +87,12 @@
"fix-examples-homepage-link",
"fix-health-check-correlation-id",
"fix-hook-loop-unconsumed-event",
"fix-inline-execution-flow-count-race",
"fix-load-events-cursor-null",
"fix-malformed-tool-call-input",
"fix-next-esm-compat",
"fix-next-version-resolution",
"fix-observability-getworld-import",
"fix-provider-tool-identity",
"fix-step-vs-wait-race",
"fix-stream-get-runid",
Expand All @@ -102,17 +107,20 @@
"fuzzy-mugs-learn",
"getter-step-support",
"green-streams-decode",
"guard-step-consumer-events",
"inline-step-registration",
"large-regions-talk",
"lazy-discovery-bare-specifiers",
"lucky-windows-smash",
"many-peas-jog",
"modern-penguins-peel",
"moody-rivers-play",
"narrow-step-bundling",
"neat-runs-serialize",
"next-diagnostics-dist",
"ninety-dancers-brush",
"nitro-forward-externals",
"nitro-webhook-rule-pattern-fix",
"no-eval-in-revive",
"node-module-error-cross-file-dce",
"o11y-run-ref-rendering",
Expand All @@ -123,16 +131,20 @@
"pretty-log-format",
"private-member-dce",
"quiet-trace-viewer-duration",
"rare-badgers-judge",
"remove-client-mode",
"remove-private-subpath",
"remove-sdk-serde-exclusion",
"remove-step-file-copy",
"rename-domain-urls",
"replay-timeout-excludes-step-bodies",
"retry-vqs-handler-errors-immediately",
"rich-toes-live",
"run-step-error-hydration",
"runtime-schema-validation-failure",
"serializable-abort-controller",
"serialization-refactor",
"setup-graphile-worker-schema",
"sixty-plants-shout",
"skip-community-worlds-main",
"slow-bottles-pull",
Expand All @@ -148,8 +160,10 @@
"swift-cobras-repair",
"sync-step-followup",
"tanstack-start-workbench",
"tired-pigs-hug",
"tired-spiders-rhyme",
"trace-viewer-polish",
"turbo-next-workbench-outputs",
"update-queue-client-version",
"v2-combined-bundle",
"vast-oranges-fail",
Expand All @@ -162,6 +176,7 @@
"world-local-path-traversal",
"world-local-run-failed-not-found",
"world-vercel-protection-bypass",
"world-vercel-trusted-sources"
"world-vercel-trusted-sources",
"yellow-pianos-relax"
]
}
7 changes: 7 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @workflow/ai

## 5.0.0-beta.5

### Patch Changes

- Updated dependencies [[`9454151`](https://github.com/vercel/workflow/commit/9454151b0e3b8a4ceeb96de4d41c5937330e16a6), [`49da6c5`](https://github.com/vercel/workflow/commit/49da6c50b3d28f9c533ec0ee28437d7ed3887335)]:
- workflow@5.0.0-beta.7

## 5.0.0-beta.4

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workflow/ai",
"version": "5.0.0-beta.4",
"version": "5.0.0-beta.5",
"description": "Workflow SDK compatible helper library for the AI SDK",
"type": "module",
"main": "dist/index.js",
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# @workflow/astro

## 5.0.0-beta.7

### Patch Changes

- Updated dependencies []:
- @workflow/builders@5.0.0-beta.7
- @workflow/rollup@5.0.0-beta.7
- @workflow/vite@5.0.0-beta.7

## 5.0.0-beta.6

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workflow/astro",
"version": "5.0.0-beta.6",
"version": "5.0.0-beta.7",
"description": "Astro integration for Workflow SDK",
"type": "module",
"main": "dist/index.js",
Expand Down
8 changes: 8 additions & 0 deletions packages/builders/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @workflow/builders

## 5.0.0-beta.7

### Patch Changes

- Updated dependencies [[`dc0be50`](https://github.com/vercel/workflow/commit/dc0be50618bd6a465e3f9768ee7427d282aa1fd7), [`ad71b58`](https://github.com/vercel/workflow/commit/ad71b58bba65e739fbafee0440ffff48878e7e51), [`9454151`](https://github.com/vercel/workflow/commit/9454151b0e3b8a4ceeb96de4d41c5937330e16a6), [`b124365`](https://github.com/vercel/workflow/commit/b124365e14b0c47a5c830c7009dd5bf0149d5a59), [`2a446af`](https://github.com/vercel/workflow/commit/2a446af517dbb91ae959adade1d74ef0428a2b09), [`1d3959e`](https://github.com/vercel/workflow/commit/1d3959eaa8db5866d08ad3970324c1b5dae73f7b), [`49da6c5`](https://github.com/vercel/workflow/commit/49da6c50b3d28f9c533ec0ee28437d7ed3887335)]:
- @workflow/core@5.0.0-beta.7
- @workflow/errors@5.0.0-beta.4

## 5.0.0-beta.6

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/builders/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workflow/builders",
"version": "5.0.0-beta.6",
"version": "5.0.0-beta.7",
"description": "Shared builder infrastructure for Workflow SDK",
"type": "module",
"main": "./dist/index.js",
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# @workflow/cli

## 5.0.0-beta.7

### Patch Changes

- Updated dependencies [[`dc0be50`](https://github.com/vercel/workflow/commit/dc0be50618bd6a465e3f9768ee7427d282aa1fd7), [`ad71b58`](https://github.com/vercel/workflow/commit/ad71b58bba65e739fbafee0440ffff48878e7e51), [`9454151`](https://github.com/vercel/workflow/commit/9454151b0e3b8a4ceeb96de4d41c5937330e16a6), [`b124365`](https://github.com/vercel/workflow/commit/b124365e14b0c47a5c830c7009dd5bf0149d5a59), [`2a446af`](https://github.com/vercel/workflow/commit/2a446af517dbb91ae959adade1d74ef0428a2b09), [`1d3959e`](https://github.com/vercel/workflow/commit/1d3959eaa8db5866d08ad3970324c1b5dae73f7b), [`49da6c5`](https://github.com/vercel/workflow/commit/49da6c50b3d28f9c533ec0ee28437d7ed3887335)]:
- @workflow/core@5.0.0-beta.7
- @workflow/world@5.0.0-beta.4
- @workflow/world-local@5.0.0-beta.6
- @workflow/world-vercel@5.0.0-beta.6
- @workflow/errors@5.0.0-beta.4
- @workflow/builders@5.0.0-beta.7
- @workflow/web@5.0.0-beta.7

## 5.0.0-beta.6

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workflow/cli",
"version": "5.0.0-beta.6",
"version": "5.0.0-beta.7",
"description": "Command-line interface for Workflow SDK",
"type": "module",
"bin": {
Expand Down
26 changes: 26 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# @workflow/core

## 5.0.0-beta.7

### Minor Changes

- [#2059](https://github.com/vercel/workflow/pull/2059) [`49da6c5`](https://github.com/vercel/workflow/commit/49da6c50b3d28f9c533ec0ee28437d7ed3887335) Thanks [@TooTallNate](https://github.com/TooTallNate)! - A `WritableStream` from a workflow's `getWritable()` can now be passed as an argument to a child workflow via `start()`; the child's writes land on the parent run's stream directly for the full lifetime of the child run.

### Patch Changes

- [#2038](https://github.com/vercel/workflow/pull/2038) [`dc0be50`](https://github.com/vercel/workflow/commit/dc0be50618bd6a465e3f9768ee7427d282aa1fd7) Thanks [@pranaygp](https://github.com/pranaygp)! - Refresh workflow events after completing elapsed waits so concurrent hook events preserve deterministic replay order.

- [#2046](https://github.com/vercel/workflow/pull/2046) [`ad71b58`](https://github.com/vercel/workflow/commit/ad71b58bba65e739fbafee0440ffff48878e7e51) Thanks [@pranaygp](https://github.com/pranaygp)! - Report corrupted event logs with a distinct `CorruptedEventLogError` type and `CORRUPTED_EVENT_LOG` run error code.

- [#2056](https://github.com/vercel/workflow/pull/2056) [`9454151`](https://github.com/vercel/workflow/commit/9454151b0e3b8a4ceeb96de4d41c5937330e16a6) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Fix spurious "Event cursor missing after initial load" warning

- [#2030](https://github.com/vercel/workflow/pull/2030) [`b124365`](https://github.com/vercel/workflow/commit/b124365e14b0c47a5c830c7009dd5bf0149d5a59) Thanks [@pranaygp](https://github.com/pranaygp)! - Validate step, wait, and hook lifecycle events against replay ownership metadata.

- [#2013](https://github.com/vercel/workflow/pull/2013) [`2a446af`](https://github.com/vercel/workflow/commit/2a446af517dbb91ae959adade1d74ef0428a2b09) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Exclude inline step execution from the workflow replay timeout. Long-running steps no longer hit `REPLAY_TIMEOUT` (fixes #2009). Adds a `WORKFLOW_REPLAY_TIMEOUT_MS` env var override and a new optional `World.processExitTriggersQueueRedelivery` capability used to gate the runtime's `process.exit(1)` failure path.

- [#2060](https://github.com/vercel/workflow/pull/2060) [`1d3959e`](https://github.com/vercel/workflow/commit/1d3959eaa8db5866d08ad3970324c1b5dae73f7b) Thanks [@pranaygp](https://github.com/pranaygp)! - Record fatal world response contract failures as non-retryable workflow errors.

- Updated dependencies [[`dc0be50`](https://github.com/vercel/workflow/commit/dc0be50618bd6a465e3f9768ee7427d282aa1fd7), [`ad71b58`](https://github.com/vercel/workflow/commit/ad71b58bba65e739fbafee0440ffff48878e7e51), [`b124365`](https://github.com/vercel/workflow/commit/b124365e14b0c47a5c830c7009dd5bf0149d5a59), [`2a446af`](https://github.com/vercel/workflow/commit/2a446af517dbb91ae959adade1d74ef0428a2b09), [`1d3959e`](https://github.com/vercel/workflow/commit/1d3959eaa8db5866d08ad3970324c1b5dae73f7b)]:
- @workflow/world@5.0.0-beta.4
- @workflow/world-local@5.0.0-beta.6
- @workflow/world-vercel@5.0.0-beta.6
- @workflow/errors@5.0.0-beta.4

## 5.0.0-beta.6

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@workflow/core",
"version": "5.0.0-beta.6",
"version": "5.0.0-beta.7",
"description": "Core runtime and engine for Workflow SDK",
"type": "module",
"main": "dist/index.js",
Expand Down
129 changes: 127 additions & 2 deletions packages/core/src/runtime/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { getWorkflowQueueName } from './helpers.js';
import type { Event } from '@workflow/world';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getWorkflowQueueName, loadWorkflowRunEvents } from './helpers.js';

// Mock the logger to suppress output during tests
vi.mock('../logger.js', () => ({
Expand All @@ -11,6 +12,25 @@ vi.mock('../logger.js', () => ({
},
}));

const eventsListMock = vi.fn();

vi.mock('./get-world-lazy.js', () => ({
getWorldLazy: vi.fn(async () => ({
events: {
list: eventsListMock,
},
})),
}));

const makeEvent = (eventId: string): Event =>
({
eventId,
runId: 'wrun_mockidnumber0001',
eventType: 'step_created',
correlationId: 'step_mock',
createdAt: new Date(),
}) as unknown as Event;

describe('getWorkflowQueueName', () => {
it('should return a valid queue name for a simple workflow name', () => {
expect(getWorkflowQueueName('myWorkflow')).toBe(
Expand Down Expand Up @@ -80,3 +100,108 @@ describe('getWorkflowQueueName', () => {
expect(() => getWorkflowQueueName('')).toThrow('Invalid workflow name');
});
});

describe('loadWorkflowRunEvents', () => {
beforeEach(() => {
eventsListMock.mockReset();
});

it('returns the cursor from the last page when pagination terminates normally', async () => {
const page1 = [makeEvent('evnt_a'), makeEvent('evnt_b')];
eventsListMock.mockResolvedValueOnce({
data: page1,
cursor: 'eid:evnt_b',
hasMore: false,
});

const result = await loadWorkflowRunEvents('wrun_test');

expect(result.events).toHaveLength(2);
expect(result.cursor).toBe('eid:evnt_b');
expect(eventsListMock).toHaveBeenCalledTimes(1);
});

// Regression test for the "Event cursor missing after initial load" warning.
//
// A World may legitimately return `{ data: [], cursor: null, hasMore: false }`
// on a trailing empty page — workflow-server does this whenever the previous
// page's DynamoDB query hit `Limit` exactly and DynamoDB returned a
// `LastEvaluatedKey` "just in case." If the pagination loop overwrites the
// cursor with `null` on that trailing page, the runtime's incremental-load
// path can't proceed and falls back to a full reload on every replay
// iteration, logging "Event cursor missing after initial load" each time.
it('preserves the cursor from the previous page when the final page is empty', async () => {
const page1 = [makeEvent('evnt_a'), makeEvent('evnt_b')];
eventsListMock.mockResolvedValueOnce({
data: page1,
cursor: 'eid:evnt_b',
hasMore: true,
});
eventsListMock.mockResolvedValueOnce({
data: [],
cursor: null,
hasMore: false,
});

const result = await loadWorkflowRunEvents('wrun_test');

expect(result.events).toHaveLength(2);
expect(result.cursor).toBe('eid:evnt_b');
expect(eventsListMock).toHaveBeenCalledTimes(2);
});

it('returns null cursor only when no events exist at all', async () => {
eventsListMock.mockResolvedValueOnce({
data: [],
cursor: null,
hasMore: false,
});

const result = await loadWorkflowRunEvents('wrun_test');

expect(result.events).toHaveLength(0);
expect(result.cursor).toBeNull();
});

it('uses the latest cursor when paginating through multiple non-empty pages', async () => {
eventsListMock.mockResolvedValueOnce({
data: [makeEvent('evnt_a')],
cursor: 'eid:evnt_a',
hasMore: true,
});
eventsListMock.mockResolvedValueOnce({
data: [makeEvent('evnt_b')],
cursor: 'eid:evnt_b',
hasMore: true,
});
eventsListMock.mockResolvedValueOnce({
data: [makeEvent('evnt_c')],
cursor: 'eid:evnt_c',
hasMore: false,
});

const result = await loadWorkflowRunEvents('wrun_test');

expect(result.events.map((e) => e.eventId)).toEqual([
'evnt_a',
'evnt_b',
'evnt_c',
]);
expect(result.cursor).toBe('eid:evnt_c');
});

it('falls back to the afterCursor when an incremental load returns no events', async () => {
eventsListMock.mockResolvedValueOnce({
data: [],
cursor: null,
hasMore: false,
});

const result = await loadWorkflowRunEvents('wrun_test', 'eid:evnt_z');

expect(result.events).toHaveLength(0);
// Preserving the input cursor avoids the runtime treating "no new events
// since last poll" as "I have no idea where I am in the log."
expect(result.cursor).toBe('eid:evnt_z');
});
});
10 changes: 9 additions & 1 deletion packages/core/src/runtime/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,15 @@ export async function loadWorkflowRunEvents(

loadedEvents.push(...response.data);
hasMore = response.hasMore;
cursor = response.cursor;
// Preserve the last non-null cursor across pages. A World may
// legitimately return `{ data: [], cursor: null, hasMore: false }`
// on a trailing empty page — for example when the previous page's
// underlying DynamoDB query hit `Limit` exactly and returned a
// `LastEvaluatedKey` "just in case". Overwriting with that null
// would lose the position past the last real event we loaded and
// force the runtime into the "no cursor after initial load" full-
// reload fallback on every subsequent replay iteration.
cursor = response.cursor ?? cursor;
pagesLoaded++;

runtimeLogger.debug('Loaded event page', {
Expand Down
Loading
Loading