Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f2a7e78
fix(builders): improve hook graph detection in transformed bundles
ijjk Apr 9, 2026
c4631a7
fix(builders): traverse try/finally in workflow graph extraction
ijjk Apr 9, 2026
342a31a
fix(builders): detect hook.create patterns in workflow graphs
ijjk Apr 10, 2026
6c4ce7b
fix(next): wait for deferred discovery rebuild before first use
ijjk Apr 10, 2026
b16ee1d
fix(world-local): use fast warmup retries for transient queue failures
ijjk Apr 10, 2026
9107bf6
fix(core): retry lazy hook lookup without blocking deferred builds
ijjk Apr 10, 2026
6f4bcb7
fix(next): always acknowledge trigger-build in deferred mode
ijjk Apr 10, 2026
110c538
fix(world-local): retry stalled lazy-discovery queue deliveries
ijjk Apr 10, 2026
91783a1
fix(utils): detect workflow port with post-only health endpoints
ijjk Apr 10, 2026
dadfd44
fix(core): retry unregistered workflows during lazy discovery
ijjk Apr 10, 2026
ad99aa1
fix(next): settle deferred trigger-build before flow ack
ijjk Apr 10, 2026
9344636
fix(next): rebuild deferred routes from cached discovery on boot
ijjk Apr 10, 2026
93b29f5
fix(world-local): resolve Next private origin for queue delivery
ijjk Apr 10, 2026
f2896ac
fix(world-local): resolve lazy queue base URL in detached worker cont…
ijjk Apr 10, 2026
93ee6c1
fix(world-local): detect next dev port from data-dir project process
ijjk Apr 10, 2026
cff4549
fix(next+world-local): stabilize lazy delivery and loader source-map …
ijjk Apr 10, 2026
52d1e31
fix(world-local): prefer effective port and ignore invalid PORT values
ijjk Apr 11, 2026
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
8 changes: 8 additions & 0 deletions .changeset/fix-hook-graph-extractor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@workflow/builders": patch
---

Fix workflow graph hook detection for transformed bundles

- Recognize step declarations with `@__PURE__` annotations in `WORKFLOW_USE_STEP` access
- Detect `createHook`/`createWebhook` calls wrapped by transpiled `using` helper calls
8 changes: 8 additions & 0 deletions .changeset/fix-workflow-port-probe-post-health.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@workflow/utils": patch
---

Fix local workflow port detection for POST-only health endpoints

- Probe `/.well-known/workflow/v1/flow?__health` with `POST` when `HEAD` is not healthy
- Prevent lazy-discovery socket ports from being selected as workflow HTTP base URL
21 changes: 21 additions & 0 deletions packages/builders/src/apply-swc-transform.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest';
import { applySwcTransform } from './apply-swc-transform.js';

describe('applySwcTransform', () => {
it('ignores missing external sourceMappingURL sidecars', async () => {
const source = [
'export const value = 1;',
'//# sourceMappingURL=index.js.map',
'',
].join('\n');

const result = await applySwcTransform(
'fixtures/missing-source-map.js',
source,
'client'
);

expect(result.code).toContain('const value = 1');
expect(result.workflowManifest).toEqual({});
});
});
4 changes: 4 additions & 0 deletions packages/builders/src/apply-swc-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export async function applySwcTransform(
// TODO: investigate proper source map support as they
// won't even be used in Node.js by default unless we
// intercept errors and apply them ourselves
// Explicitly disable reading input source maps from sourceMappingURL
// sidecars. Some published dependencies omit *.map files, which can
// otherwise fail deferred route compilation in lazy discovery mode.
inputSourceMap: false,
sourceMaps: false,
minify: false,
});
Expand Down
128 changes: 128 additions & 0 deletions packages/builders/src/workflows-extractor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import { extractWorkflowGraphs } from './workflows-extractor.js';

async function createWorkflowBundleFile(
workflowCode: string
): Promise<{ filePath: string; tempDir: string }> {
const tempDir = await mkdtemp(join(tmpdir(), 'workflow-extractor-'));
const filePath = join(tempDir, 'route.js');
const escapedWorkflowCode = workflowCode.replace(/[\\`$]/g, '\\$&');
const bundleCode = `const workflowCode = \`${escapedWorkflowCode}\`;
export const POST = workflowCode;`;
await writeFile(filePath, bundleCode, 'utf8');
return { filePath, tempDir };
}

describe('workflows-extractor', () => {
const tempDirs: string[] = [];

afterEach(async () => {
await Promise.all(
tempDirs
.splice(0)
.map((tempDir) => rm(tempDir, { recursive: true, force: true }))
);
});

it('detects step declarations that include @__PURE__ annotations', async () => {
const workflowCode = `
var stepWithPure = globalThis[/* @__PURE__ */ Symbol.for("WORKFLOW_USE_STEP")]("step//./workflows/demo//stepWithPure");
async function demo() {
const value = await stepWithPure();
return value;
}
demo.workflowId = "workflow//./workflows/demo//demo";
globalThis.__private_workflows.set("workflow//./workflows/demo//demo", demo);
`;
const { filePath, tempDir } = await createWorkflowBundleFile(workflowCode);
tempDirs.push(tempDir);

const graphs = await extractWorkflowGraphs(filePath);
const demoGraph = graphs['./workflows/demo']?.demo?.graph;
const labels = (demoGraph?.nodes || []).map((node) => node.data.label);

expect(labels).toContain('stepWithPure');
});

it('detects createHook wrapped by transpiled using helpers inside try/finally', async () => {
const workflowCode = `
var stepA = globalThis[/* @__PURE__ */ Symbol.for("WORKFLOW_USE_STEP")]("step//./workflows/hooks//stepA");
function _ts_add_disposable_resource(_env, value, _isAsync) {
return value;
}
function _ts_dispose_resources(_env) {}
async function withHook() {
const env = { stack: [] };
try {
const responseId = await stepA();
const hook = _ts_add_disposable_resource(env, createHook({ token: 'hook:' + responseId }), false);
const payload = await hook;
return payload;
} finally {
_ts_dispose_resources(env);
}
}
withHook.workflowId = "workflow//./workflows/hooks//withHook";
globalThis.__private_workflows.set("workflow//./workflows/hooks//withHook", withHook);
`;
const { filePath, tempDir } = await createWorkflowBundleFile(workflowCode);
tempDirs.push(tempDir);

const graphs = await extractWorkflowGraphs(filePath);
const hookGraph = graphs['./workflows/hooks']?.withHook?.graph;
const labels = (hookGraph?.nodes || []).map((node) => node.data.label);

expect(labels).toEqual(
expect.arrayContaining(['stepA', 'createHook', 'awaitWebhook'])
);
});

it('detects hook.create calls used directly inside Promise.race', async () => {
const workflowCode = `
async function waitForAuthWorkflow() {
const session = await Promise.race([
authCompleteHook.create({ token: 'auth:demo' }),
sleep('1h').then(() => null),
]);
return session;
}
waitForAuthWorkflow.workflowId = "workflow//./workflows/hooks//waitForAuthWorkflow";
globalThis.__private_workflows.set("workflow//./workflows/hooks//waitForAuthWorkflow", waitForAuthWorkflow);
`;
const { filePath, tempDir } = await createWorkflowBundleFile(workflowCode);
tempDirs.push(tempDir);

const graphs = await extractWorkflowGraphs(filePath);
const hookGraph = graphs['./workflows/hooks']?.waitForAuthWorkflow?.graph;
const labels = (hookGraph?.nodes || []).map((node) => node.data.label);

expect(labels).toEqual(expect.arrayContaining(['createHook']));
});

it('detects for-await hook consumption when hook is created via hook.create', async () => {
const workflowCode = `
var processPayload = globalThis[/* @__PURE__ */ Symbol.for("WORKFLOW_USE_STEP")]("step//./workflows/hooks//processPayload");
async function streamHookWorkflow() {
const hook = activeSubagentRunHook.create({ token: 'stream:demo' });
for await (const payload of hook) {
await processPayload(payload);
}
}
streamHookWorkflow.workflowId = "workflow//./workflows/hooks//streamHookWorkflow";
globalThis.__private_workflows.set("workflow//./workflows/hooks//streamHookWorkflow", streamHookWorkflow);
`;
const { filePath, tempDir } = await createWorkflowBundleFile(workflowCode);
tempDirs.push(tempDir);

const graphs = await extractWorkflowGraphs(filePath);
const hookGraph = graphs['./workflows/hooks']?.streamHookWorkflow?.graph;
const labels = (hookGraph?.nodes || []).map((node) => node.data.label);

expect(labels).toEqual(
expect.arrayContaining(['processPayload', 'createHook', 'awaitWebhook'])
);
});
});
Loading
Loading