Skip to content

feat: serverless executors and browser worker - #3

Merged
ovasylenko merged 12 commits into
mainfrom
feat/serverless-executors
Sep 29, 2026
Merged

ovasylenko merged 12 commits into
mainfrom
feat/serverless-executors

Conversation

@ovasylenko

Copy link
Copy Markdown
Contributor

Includes the BrowserWorker branch. Adds @orch8.io/sdk/serverless: drainOnce with deadline-aware lease release, AWS Lambda and Cloudflare Workers adapters, examples and docs; the client now uses Web Crypto so it runs on Workers without nodejs_compat.

Checks: typecheck, build, check:browser, pnpm test.

https://claude.ai/code/session_01EEPZNNP6CHDVjcDw3W92W5

ovasylenko and others added 12 commits September 27, 2026 23:24
Add RuntimeKind/RuntimeCapabilities/RuntimePlacement types, the optional
effect_id/continuity_epoch/lease_secs/target_runtime_id/runtime_kinds task
fields, and a builder `runtime` step option that emits params.$runtime.
Replace NodeJS.Timeout with ReturnType<typeof setTimeout> in the worker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
createBrowserSession mints a short-lived browser worker token from the
customer backend via POST /runtimes/browser-sessions; releaseTask wraps the
new voluntary give-back endpoint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
…lpers

Add @orch8.io/sdk/browser (exports subpath with a browser condition). Its
module graph has no Node built-ins. BrowserWorker authenticates with a
short-lived session token from the customer's backend, polls with
kind=browser capabilities, heartbeats per lease_secs, completes/fails,
releases in-flight tasks with keepalive fetch on pagehide/hidden, refreshes
the token before expiry and stops when it lapses. The lease loop runs in a
dedicated Web Worker (pre-bundled to a string, started from a Blob URL)
while handlers run on the main thread with DOM access; it falls back to the
main thread when workers are unavailable or blocked by CSP.

check:browser bundles the entry with esbuild for platform=browser, fails
on any node: reference, and runs the lease worker source in isolation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
Avoids a redundant getToken() round-trip when the engine starts before the
token command arrives; the crash-recovery path reuses the same command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
jsdom + mocked fetch: poll capabilities, heartbeat cadence per lease_secs,
409 lease loss aborting the signal, keepalive release on pagehide/hidden,
bfcache resume, unstarted-claim release, token refresh and expiry stop,
401 re-auth, exponential backoff, oversized output rejection, timeouts and
no payloads in events. The Web Worker path runs the generated source in an
isolated VM realm; staleness of the generated source is checked too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
…down

- WorkerConfig.capabilities advertises RuntimeCapabilities with each poll
  (runtime_id = workerId, kind default server, trust default registered,
  handlers default to the configured names, fresh observed_at/expires_at
  capped at five minutes), so Node workers can claim tasks with $runtime
  placement requirements.
- Handlers get a WorkerTaskContext second argument with effectId,
  leaseSecs, continuityEpoch and workerId; the push runner passes it too.
  workerTaskContext() lives in internal/ so the push entry does not pull
  in the worker.
- Claimed-but-unstarted tasks (a batch that lands after stop(), or tasks
  beyond free capacity, previously orphaned until lease expiry) are
  released with started: false; 404/405 from older servers is tolerated.
- Heartbeat interval also honours each in-flight task's lease_secs.

Verified: typecheck, vitest (242 tests, 8 new), npm run check:browser.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
… completion

A task released before its handler was invoked (pagehide fired as the tab
received it) was reported as started: true and the handler still ran with
an aborted signal. Mark the task started only when the handler is invoked,
and never invoke it after a release, so the engine returns the task to
pending immediately.

A 413 from complete (output above the engine's browser output cap) left the
claim held until lease expiry. Fail the step non-retryably with the
engine's reason instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
npm run test:e2e:browser runs a jsdom page (lease loop in main-thread mode;
jsdom has no Worker) against a live engine over real fetch: happy path with
page data and provenance, no-secrets dispatch and context filtering, token
scope, tab close via release, token refresh and expiry, oversized output
(client guard and engine 413), and CORS preflights. Skipped unless
ORCH8_E2E_URL and ORCH8_E2E_ADMIN_KEY are set.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NqczyJaTHD2CWHq3m6FAWU
…tisement

Orch8Client drew request ids from node:crypto, which kept it off Cloudflare
Workers without nodejs_compat. It now uses the Web Crypto global and falls
back to node:crypto lazily on Node 18. The worker's capability advertisement
moves to internal/capabilities so other executors build the same payload.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEPZNNP6CHDVjcDw3W92W5
@orch8.io/sdk/serverless claims up to maxTasks through the existing worker
lease API, runs them concurrently with heartbeats, and releases every lease
still held at deadlineMs - releaseMarginMs (started: true). A claim whose
timeout_ms cannot fit the budget is released started: false, and a result
that arrives after its release is never acknowledged.

createLambdaExecutor budgets from getRemainingTimeInMillis() minus a safety
margin; createCloudflareExecutor offers scheduled and a secret-guarded fetch
trigger, using fetch only. The entry bundles without Node built-ins.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEPZNNP6CHDVjcDw3W92W5
README section and examples for Lambda (with a SAM template), Cloudflare
Workers (with wrangler.toml) and a plain drainOnce script, plus
docs/browser-executor.md covering targeting, lifecycle and security for
steps that run in the user's tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EEPZNNP6CHDVjcDw3W92W5
@ovasylenko
ovasylenko merged commit 586f854 into main Sep 29, 2026
3 checks passed
@ovasylenko
ovasylenko deleted the feat/serverless-executors branch October 2, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant