Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5544e9c
initial local + pg stubs
NathanColosimo Mar 17, 2026
03b016c
added test stubs
NathanColosimo Mar 17, 2026
4b918ca
add e2e examples
NathanColosimo Mar 17, 2026
4b9c8b6
DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>
NathanColosimo Mar 18, 2026
49ae775
add pg limit tests, lock tests, schema, migration
NathanColosimo Mar 18, 2026
1677f3d
pg limits implementation
NathanColosimo Mar 18, 2026
45cd62b
DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>
NathanColosimo Mar 18, 2026
dc85a46
Add in-step locking support - doesn't hang the step though
NathanColosimo Mar 19, 2026
190dd4f
merge main in
NathanColosimo Mar 19, 2026
27486dc
add new errors
NathanColosimo Mar 19, 2026
8c683a3
Increase ttl times for flaky tests on slow runners
NathanColosimo Mar 19, 2026
71de1c5
fix e2e test
NathanColosimo Mar 19, 2026
4ed44fc
Add FIFO to local and group e2e and contract tests
NathanColosimo Mar 19, 2026
39efdb3
add more e2e test cases
NathanColosimo Mar 19, 2026
eabe5ef
fixed type error
NathanColosimo Mar 19, 2026
b8480d3
fix ci issues
NathanColosimo Mar 19, 2026
a6b603a
Removed step lock and added lock index
NathanColosimo Mar 20, 2026
6f22676
Added event sourced flow limit architecture and simplify schema
NathanColosimo Mar 31, 2026
de4097e
Merge origin/main into codex/flow-limits-types-first-pass
NathanColosimo Mar 31, 2026
dd9e3f8
Merge origin/main into codex/flow-limits-types-first-pass
NathanColosimo Mar 31, 2026
a0bdb8e
Fix concurrent rustup target installs in CI
NathanColosimo Mar 31, 2026
25ebab3
Fix local TooEarlyError retryAfter type
NathanColosimo Mar 31, 2026
5973860
Fix lock retryAfter unit test contract
NathanColosimo Mar 31, 2026
0b7be69
Harden limits timing tests across CI
NathanColosimo Mar 31, 2026
7c7175c
Fix postgres world tests to use pg
NathanColosimo Mar 31, 2026
1ff210e
Use workspace tsx for postgres test setup
NathanColosimo Mar 31, 2026
7800784
Fix pg tests and stabilize lock contention e2e
NathanColosimo Mar 31, 2026
f1cba20
Fix shared limits test module format
NathanColosimo Mar 31, 2026
ad5768b
Harden concurrent limits contract timing
NathanColosimo Mar 31, 2026
4c1a767
Relax cancelled waiter e2e timing
NathanColosimo Mar 31, 2026
ded5a4a
Stabilize unrelated-key limits e2e timing
NathanColosimo Mar 31, 2026
17208ac
Add changset
NathanColosimo Mar 31, 2026
8a25c91
Stabilize limits test timing
NathanColosimo Mar 31, 2026
6cc468d
Harden FIFO limits contract timing
NathanColosimo Mar 31, 2026
a22fd76
Fix cancelled waiter e2e timing
NathanColosimo Mar 31, 2026
216c9ee
Prune dead lock holders on terminal state runs and disable lock() on …
NathanColosimo Apr 1, 2026
c1be937
fix race condition in lock event replay
NathanColosimo Apr 1, 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
28 changes: 28 additions & 0 deletions .changeset/eight-colts-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@workflow/swc-playground-wasm": patch
"@workflow/swc-plugin": patch
"@workflow/world-postgres": patch
"@workflow/world-testing": patch
"@workflow/world-vercel": patch
"@workflow/world-local": patch
"@workflow/web-shared": patch
"@workflow/sveltekit": patch
"@workflow/builders": patch
"workflow": patch
"@workflow/errors": patch
"@workflow/rollup": patch
"@workflow/vitest": patch
"@workflow/astro": patch
"@workflow/nitro": patch
"@workflow/world": patch
"@workflow/core": patch
"@workflow/nest": patch
"@workflow/next": patch
"@workflow/nuxt": patch
"@workflow/vite": patch
"@workflow/cli": patch
"@workflow/web": patch
"@workflow/ai": patch
---

Add experimental rate limiting and flow concurrency control
13 changes: 13 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,19 @@ jobs:
DEPLOYMENT_URL: "http://localhost:${{ matrix.app.name == 'sveltekit' && '4173' || (matrix.app.name == 'astro' && '4321' || '3000') }}"
NEXT_CANARY: ${{ matrix.app.canary && '1' || '' }}

- name: Run Low-Concurrency Worker-Slot Test
if: ${{ !matrix.app.canary && matrix.app.name == 'nextjs-turbopack' }}
run: |
cd "${{ steps.prepare-workbench.outputs.workbench_app_path }}" && PORT=3001 WORKFLOW_POSTGRES_WORKER_CONCURRENCY=1 pnpm start &
echo "starting low-concurrency tests in 10 seconds" && sleep 10
pnpm vitest run packages/core/e2e/e2e.test.ts -t "frees worker slots for unrelated workflows while a waiter is blocked"
env:
NODE_OPTIONS: "--enable-source-maps"
APP_NAME: ${{ matrix.app.name }}
WORKBENCH_APP_PATH: ${{ steps.prepare-workbench.outputs.workbench_app_path }}
DEPLOYMENT_URL: "http://localhost:3001"
WORKFLOW_LIMITS_LOW_CONCURRENCY: "1"

- name: Generate E2E summary
if: always()
run: node .github/scripts/aggregate-e2e-results.js . --job-name "E2E Local Postgres (${{ matrix.app.name }})" >> $GITHUB_STEP_SUMMARY || true
Expand Down
118 changes: 59 additions & 59 deletions docs/lib/ai-agent-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,84 +18,84 @@
// Layer 1: Known AI agent UA substrings (lowercase).
const AI_AGENT_UA_PATTERNS = [
// Anthropic — https://support.claude.com/en/articles/8896518
"claudebot",
"claude-searchbot",
"claude-user",
"anthropic-ai",
"claude-web",
'claudebot',
'claude-searchbot',
'claude-user',
'anthropic-ai',
'claude-web',

// OpenAI — https://platform.openai.com/docs/bots
"chatgpt",
"gptbot",
"oai-searchbot",
"openai",
'chatgpt',
'gptbot',
'oai-searchbot',
'openai',

// Google AI
"gemini",
"bard",
"google-cloudvertexbot",
"google-extended",
'gemini',
'bard',
'google-cloudvertexbot',
'google-extended',

// Meta
"meta-externalagent",
"meta-externalfetcher",
"meta-webindexer",
'meta-externalagent',
'meta-externalfetcher',
'meta-webindexer',

// Search/Research AI
"perplexity",
"youbot",
"you.com",
"deepseekbot",
'perplexity',
'youbot',
'you.com',
'deepseekbot',

// Coding assistants
"cursor",
"github-copilot",
"codeium",
"tabnine",
"sourcegraph",
'cursor',
'github-copilot',
'codeium',
'tabnine',
'sourcegraph',

// Other AI agents / data scrapers (low-harm to serve markdown)
"cohere-ai",
"bytespider",
"amazonbot",
"ai2bot",
"diffbot",
"omgili",
"omgilibot",
'cohere-ai',
'bytespider',
'amazonbot',
'ai2bot',
'diffbot',
'omgili',
'omgilibot',
];

// Layer 2: Known AI service URLs in Signature-Agent header (RFC 9421).
const SIGNATURE_AGENT_DOMAINS = ["chatgpt.com"];
const SIGNATURE_AGENT_DOMAINS = ['chatgpt.com'];

// Layer 3: Traditional bot exclusion list — bots that should NOT trigger
// the heuristic layer (they're search engine crawlers, social previews, or
// monitoring tools, not AI agents).
const TRADITIONAL_BOT_PATTERNS = [
"googlebot",
"bingbot",
"yandexbot",
"baiduspider",
"duckduckbot",
"slurp",
"msnbot",
"facebot",
"twitterbot",
"linkedinbot",
"whatsapp",
"telegrambot",
"pingdom",
"uptimerobot",
"newrelic",
"datadog",
"statuspage",
"site24x7",
"applebot",
'googlebot',
'bingbot',
'yandexbot',
'baiduspider',
'duckduckbot',
'slurp',
'msnbot',
'facebot',
'twitterbot',
'linkedinbot',
'whatsapp',
'telegrambot',
'pingdom',
'uptimerobot',
'newrelic',
'datadog',
'statuspage',
'site24x7',
'applebot',
];

// Broad regex for bot-like UA strings (used only in Layer 3 heuristic).
const BOT_LIKE_REGEX = /bot|agent|fetch|crawl|spider|search/i;

export type DetectionMethod = "ua-match" | "signature-agent" | "heuristic";
export type DetectionMethod = 'ua-match' | 'signature-agent' | 'heuristic';

export interface DetectionResult {
detected: boolean;
Expand All @@ -111,36 +111,36 @@ export interface DetectionResult {
export function isAIAgent(request: {
headers: { get(name: string): string | null };
}): DetectionResult {
const userAgent = request.headers.get("user-agent");
const userAgent = request.headers.get('user-agent');

// Layer 1: Known UA pattern match
if (userAgent) {
const lowerUA = userAgent.toLowerCase();
if (AI_AGENT_UA_PATTERNS.some((pattern) => lowerUA.includes(pattern))) {
return { detected: true, method: "ua-match" };
return { detected: true, method: 'ua-match' };
}
}

// Layer 2: Signature-Agent header (RFC 9421, used by ChatGPT agent)
const signatureAgent = request.headers.get("signature-agent");
const signatureAgent = request.headers.get('signature-agent');
if (signatureAgent) {
const lowerSig = signatureAgent.toLowerCase();
if (SIGNATURE_AGENT_DOMAINS.some((domain) => lowerSig.includes(domain))) {
return { detected: true, method: "signature-agent" };
return { detected: true, method: 'signature-agent' };
}
}

// Layer 3: Missing browser fingerprint heuristic
// Real browsers (Chrome 76+, Firefox 90+, Safari 16.4+) send sec-fetch-mode
// on navigation requests. Its absence signals a programmatic client.
const secFetchMode = request.headers.get("sec-fetch-mode");
const secFetchMode = request.headers.get('sec-fetch-mode');
if (!secFetchMode && userAgent && BOT_LIKE_REGEX.test(userAgent)) {
const lowerUA = userAgent.toLowerCase();
const isTraditionalBot = TRADITIONAL_BOT_PATTERNS.some((pattern) =>
lowerUA.includes(pattern)
);
if (!isTraditionalBot) {
return { detected: true, method: "heuristic" };
return { detected: true, method: 'heuristic' };
}
}

Expand Down
14 changes: 7 additions & 7 deletions docs/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,24 @@ const proxy = (request: NextRequest, context: NextFetchEvent) => {
// AI agent detection — rewrite docs pages to markdown for agents
// so they always get structured content without needing .md URLs or Accept headers
if (
(pathname === "/docs" || pathname.startsWith("/docs/")) &&
!pathname.includes("/llms.mdx/")
(pathname === '/docs' || pathname.startsWith('/docs/')) &&
!pathname.includes('/llms.mdx/')
) {
const agentResult = isAIAgent(request);
if (agentResult.detected && !isMarkdownPreferred(request)) {
const result =
pathname === "/docs"
pathname === '/docs'
? `/${i18n.defaultLanguage}/llms.mdx`
: rewriteLLM(pathname);

if (result) {
context.waitUntil(
trackMdRequest({
path: pathname,
userAgent: request.headers.get("user-agent"),
referer: request.headers.get("referer"),
acceptHeader: request.headers.get("accept"),
requestType: "agent-rewrite",
userAgent: request.headers.get('user-agent'),
referer: request.headers.get('referer'),
acceptHeader: request.headers.get('accept'),
requestType: 'agent-rewrite',
detectionMethod: agentResult.method,
})
);
Expand Down
Loading
Loading