Customer runtime Effect migration plan #
Move all application-owned asynchronous logic in the customer Worker to Effect, following the control-plane migration. Keep Cloudflare Agents, Think, Workers AI, Browser Run, and Sandbox as the native runtime. Use Distilled for supported remote service APIs. Full Alchemy adoption is excluded.
This is an implementation plan, not a record of completed changes. The baseline
is 945d0cd, including control-plane PR #59, with Effect 4.0.0-rc.112,
@distilled.cloud/cloudflare 1.0.0-rc.8, Agents 0.22.0, Think 0.17.0, and
Sandbox 0.12.9. Keep those versions pinned during the migration. The phases
below describe the complete migration, with each phase forming a reviewable change.
“All” covers Worker ingress, authentication, bridge operations, customer settings, memory, conversation lifecycle and turn preparation, tools, temporary resources, task dispatch/reconciliation, and diagnostic export. Pure transformations and validation stay synchronous. Native SDK callbacks, RPC methods, stream callbacks, and synchronous transactions retain the interfaces their owners require.
The frontend, public RPC payloads, model/provider behavior, storage formats, and deployment architecture retain their existing contracts. This migration adds no new product integrations or customer permissions. Keep existing Zod schemas; adopting Effect does not require changing the validation system.
Dependency decisions #
Distilled supplies typed API operations; Effect supplies dependency composition. Prefer explicit narrow dependencies for application modules, matching the control-plane review. Use Context services and Layers where an SDK requires them or where they remove repeated construction of shared capabilities. Avoid a service wrapping every function or a single service that exposes the entire Agent and environment.
| Existing dependency | Migration decision |
|---|---|
| Cloudflare management APIs in the publisher | Retain the existing Distilled integration and bounded, request-specific credentials/transport. |
AI.run, AI Gateway binding transport, and workers-ai-provider |
Keep native bindings and provider protocols; compose application operations as Effects. Preserve gateway routing, logging policy, model options, and session affinity. |
| Anthropic and OpenAI-compatible AI SDK adapters | Keep their LanguageModel, tool, and stream contracts. Adapt application I/O at their native callback boundaries. A management SDK does not replace the model protocol. |
agents/browser |
Keep native browser creation, CDP commands, and deletion. Express application acquisition, policy, extraction, and cleanup as Effects. |
@cloudflare/sandbox |
Keep native Sandbox RPC, execution streams, and destruction. Migrate lease and execution orchestration. |
| Agents RPC, SQL, schedules, facets, and Think submissions/actions | Retain native owners; add narrow Effect adapters and application programs. |
| Publisher bridge and release-check HTTP | Keep the existing Flarebot protocol behind a bounded Effect HTTP adapter. Distilled has no application-specific endpoint contract to substitute. |
| Think fetch tool and HTML extraction | Keep the native fetch tool, redirect policy, and HTMLRewriter; migrate the enclosing operation and resource ownership. |
| Web Crypto | Adapt asynchronous signing, verification, hashing, and encryption. Preserve the current keys, byte formats, and domain separation. |
The audited customer paths do not currently require another Distilled package. For any remote API uncovered during implementation, record its endpoint and operation before choosing an adapter. Prefer Distilled when the pinned SDK covers the actual protocol, supports the target runtime and authentication, and preserves required response fields, cancellation, and retry control. Use a narrow native adapter for gaps; do not assert missing SDK coverage with casts.
Customer Workers must not receive publisher deployment grants to make a Distilled integration possible. The deployment manifest explicitly confines those grants to deployment. Existing endpoint coverage limits remain documented in Effect control plane.
Execution and ownership contracts #
Application modules return Effect<A, DomainFailure, Requirements> and compose
directly. Adapt individual external operations at their edges; wrapping an entire
existing async implementation in tryPromise is an intermediate step, not the
completed migration.
| Boundary | Required behavior |
|---|---|
Worker fetch |
Run the request program with its signal; preserve authentication order, exact origins/paths, private response headers, and native 101 responses. SSR receives assets only. |
| Agent RPC and asynchronous hooks | Run the application program once per invocation. Keep public method names, arguments, return values, safe error behavior, and @callable exposure. Preserve required super hook ordering. |
| Internal parent/child RPC | Transport native values, not Effects or Cause objects. Derive clients from server contracts. Translate declared safe failures explicitly; retain public client compatibility. |
| Think tools and actions | Run one program per invocation using its cancellation signal. Preserve action idempotency keys, preliminary results, progress, and native submission/tool identities. |
| Streams and async iterators | Keep the resource scope alive for the entire consumption lifetime, including early consumer return/cancel. Returning a stream from a closed scope is invalid. Preserve backpressure and bounded output. |
| Scheduled callbacks | Execute one attempt as an Effect and let Agents own alarm dispatch and callback retries. Reconciliation uses persisted state and the same submission identity. |
| Background work and late settlement | Execute under the owning native object's waitUntil where required. A parent-owned acquisition must survive child deletion. Durable leases/alarms remain the restart fallback. |
| Synchronous hooks and SQL transactions | Keep synchronous execution. Wrap atomic operations as a unit; never insert asynchronous yields inside transactionSync or convert a failed write into a successful transaction result. |
Expected errors use domain-specific tagged failures. Match them at the relevant HTTP, callable, tool, or scheduler boundary. Defects and interruption remain distinct internally; do not broadly convert them into successful empty values. Use the existing safe fallback where the contract requires it, such as continuing navigation when the publisher update check is unavailable. Raw provider errors, credentials, request bodies, and Effect causes must not enter user responses, transcripts, public state, or diagnostics.
Cancellation must reach the native operation when supported. Interruption of a Promise adapter does not prove an RPC or remote mutation stopped. Preserve authorization/version rechecks after asynchronous operations and the durable cleanup paths for late results. Resource release gets its own bounded cleanup budget; cancellation of the caller must not prevent cleanup from being attempted.
Effect runtimes, fibers, scopes, and mutable caches are ephemeral. Reconstruct instance dependencies after activation, scope credentials to the appropriate request or turn, and never persist execution objects. Native Agents/Think own hibernation, scheduling, recovery, and durable queues. Each retry has one owner; do not multiply native retries with blanket Effect or SDK retries.
Ordered implementation phases #
0. Establish the baseline and migration inventory #
Record the exact source revision, dependency pins, existing fixture overrides,
native boundary methods, and application-owned tables. Inventory every async
operation in worker/, plus the server-side crypto helpers it calls in
shared/ and configuration/. Classify each as an application program, native
adapter, or required native callback. Use that inventory to account for the final
remaining Promises and timers.
Build the customer artifact and run the relevant native baseline suites before
changing behavior. Record raw/compressed Worker size, build-reported startup time
where available, and comparable fresh-runtime HTTP/facet readiness timings.
Record the dependency import graph and the responsibilities of the largest files:
personal-agent.ts currently has 1,497 lines and conversation.ts has 567.
The control-plane notes report a pre-existing bridge-browser timeout and missing local Docker at the time of that migration. Recheck current conditions rather than treating those reports as either current failures or exemptions. For any failure, symptom-match bug lessons before forming a hypothesis. Do not mark an unavailable or skipped native suite as passing.
Exit: reproducible baseline results and a complete operation/boundary inventory.
1. Establish Effect boundaries through web research #
Migrate web-read.ts, web-search.ts, asynchronous source hashing in
web-source.ts, and the corresponding entries in web-tools.ts. Keep pure URL,
text, citation, and response-shape validation synchronous. Introduce web-specific
tagged failures and convert them to the current WebResult at the tool boundary.
Implement bounded native-operation adapters for fetch/AI responses and body readers. Cover the complete body lifetime with cancellation and deadlines, release readers on every exit, and preserve all byte/content limits and source provenance checks. Search continues through the native AI Gateway path with its existing model and billing/authentication error distinctions. Retain the pinned Think fetch patch until its underlying slow-body regression is independently resolved.
Reuse the control-plane transport implementation only where contracts match. If
both sides need the same primitive, extract a small server-only utility and update
both consumers; the customer bundle must not import publisher orchestration.
Keep WebDeadline for unmigrated browser/shell callers until their phases finish.
Add tests/customer-runtime-effect.test.mjs and
tests/customer-runtime-effect.types.ts, with a test:customer-runtime-effect
script and inclusion in Worker typechecking/CI. Test real failure behavior:
underlying cancellation during slow body reads, bounded cleanup, safe error
mapping, and exact result/error/dependency types. Extend this suite in later
phases instead of creating duplicate tests for wrapper syntax.
Exit: migrated web operations compose Effects end to end and test:web passes,
including the native slow-body regression. The paid live search test remains
explicitly opt-in and is reported separately from local coverage.
2. Migrate ingress, authentication, bridge, and instance management #
Migrate index.ts, session.ts, socket-session.ts, bridge.ts, and
update-on-visit.ts. Include parent operations for signed domain/provider
notifications and bootstrap health. Keep configuration parsing, installation
identity validation, runtime path checks, and runtime-info DTO construction
synchronous where they are pure.
Extract cohesive parent management operations from PersonalAgent, leaving its
native RPC methods and startup hooks in place. Adapt bridge-store.ts atomic
operations without changing challenge consumption, expiry, replay, or pending
health-cleanup records. Shared crypto consumers in the publisher must continue
to work; keep frontend-safe shared contracts free of server execution imports.
Preserve the localhost login path, cookies, exact origin/audience checks, the custom-domain approval path, socket expiry schedules, asset health verification, and the bounded update check's fallback. Keep original background probe ownership while browser and shell implementation migrate in later phases.
Exit: test:config, test:worker, test:runtime, test:bridge-server,
test:bridge, test:domains, and test:updates establish the same public behavior.
Run publisher OAuth/ownership regressions if shared crypto or transport changes.
3. Extract personal data and conversation application programs #
Move settings, credentials, instructions, memory, and conversation lifecycle into
focused modules, with separate storage-independent transition rules where useful.
Keep PersonalAgent as the native class and contract boundary; domain modules
receive small capabilities rather than the whole class.
Migrate asynchronous encryption in model-settings.ts, provider configuration
reads/writes, conversation creation/deletion, title generation, and readiness.
Preserve synchronous SQL batches and compare/version checks. Keep one bounded
title-generation attempt and its existing manual-rename protection.
Migrate Conversation.beforeTurn and memory tools/actions to Effect programs.
Preserve the coherent configuration/credential snapshot, per-conversation model
override, scheduled model snapshot, current instructions, memory limits, and
native action idempotency. Task submission/action migration completes in phase 6.
Keep synchronous model factories and AI SDK contracts in model-provider.ts and
gateway-model.ts. Migrate application async transport/observation work behind
their native callbacks. Preserve stream backpressure, reasoning metadata,
redaction, provider options, and cancellation; avoid buffering model streams to
make them fit an Effect return value.
Creation/deletion retains its durable pending states and restart cleanup. Preserve the parent constructor's native message/close guards against deleted-facet resurrection. Replace in-memory in-flight coordination only with an equivalent mechanism owned by the same Agent instance, never a process-wide cache.
Exit: test:runtime, test:think, test:memory, test:providers, test:settings,
and test:chat-ui pass. Existing fixture subclass hooks remain usable. The
parent's extracted responsibilities are independently understandable, without a
new forwarding facade recreating the original monolith.
4. Migrate browser acquisition, extraction, and durable cleanup #
Migrate browser-session.ts, browser-read.ts, and parent browser lease methods.
The parent owns external creation and late settlement; the conversation owns its
CDP command/extraction work. Preserve lease rows, native expiry callbacks, and
cleanup reconciliation on activation.
Use Effect resource scopes for acquired sessions and connections, retaining one absolute work deadline and a separate bounded cleanup deadline. Model acquisition that settles after interruption explicitly. A scoped finalizer in a deleted conversation cannot replace the surviving parent's acquisition continuation.
Preserve request interception, public URL/redirect enforcement, isolated-world extraction, selector validation, output limits, progress, and remote deletion confirmation. A disconnected CDP socket is not evidence that the remote browser session was deleted.
Exit: test:browser and test:activities pass cancellation, acquisition failure,
late creation/registration, parallel calls, conversation deletion, restart, and
cleanup-failure cases. Tests observe the actual session outcome.
5. Migrate shell leases and streamed execution #
Migrate shell-tool.ts, application logic in sandbox.ts, and parent shell
reservation/launch/close methods. Extract lease orchestration from PersonalAgent
while retaining the native Sandbox class and instance identity.
Keep lease reservation and capacity accounting atomic. The parent continues to own late launch settlement, and the Sandbox retains its persisted invocation marker and revocation behavior. A successful destruction while a launch is still pending remains provisional; retain the required post-launch destruction and durable retry until closure is confirmed.
Keep the tool's native preliminary-result/async-iterator interface. Its Effect
scope spans stream consumption and closure, with output limits, UTF-8 handling,
progress cadence, and early consumer termination covered. Preserve separate
command outcome and cleanup outcome, including cleanup: pending. Do not retry
shell commands automatically.
Remove WebDeadline only after every former caller has an equivalent deadline,
cancellation, and cleanup contract. Review each retained timer against the phase
0 inventory; native deadline callbacks with a distinct lifetime may remain.
Exit: test:shell, test:activities, and affected health/bridge checks pass with
native Docker-backed Sandbox execution, including late launch and restart cleanup.
6. Migrate tasks, native scheduling, and submission reconciliation #
Migrate task-execution.ts, task operations in PersonalAgent, scheduled
submission methods in Conversation, and schedule-action.ts integration.
Retain task-validation.ts pure validation and task-store.ts synchronous
transactions behind precise application contracts.
Separate task intent/transitions, storage operations, and native scheduling/ submission adapters. Keep callback names and serialized payloads stable because existing installations already persist them. Preserve task versions, occurrence identity, manual request IDs, tombstones, and exact Think submission/idempotency keys.
Keep authorization/version rechecks across asynchronous boundaries, cancellation of stale queued/running submissions, bounded reconciliation batches, and native callback retries. An ambiguous accepted reply must reconcile the original submission; it must not create another turn. Effect sleeps and in-memory queues must not become the durable scheduler.
Exit: test:tasks, test:execution, test:schedule-action, and test:tasks-ui
pass, including unattended restart, lost acceptance replies, concurrent edits,
deletion, and scheduled model snapshot behavior.
7. Finish diagnostics, audit boundaries, and validate upgrades #
Migrate remaining asynchronous diagnostic export and aggregation in
diagnostics.ts, tool-activity.ts, Conversation, and PersonalAgent. Preserve
synchronous observability receivers, clear/reset hooks, native hook ordering,
redacted allowlisted fields, bounded retention, and activity tombstones. Optional
diagnostics must not fail an otherwise valid model/tool invocation. Interrupted
observations and known terminal outcomes retain their different recovery rules.
Complete the phase 0 inventory: every remaining Promise, async generator, timer, and Effect execution call has a native-boundary or lifecycle reason. Remove temporary Promise compatibility implementations and obsolete helpers. Audit runtime imports for cycles, frontend imports for server dependency leakage, and the largest files for concentrated responsibilities. Record before/after sizes and startup/readiness measurements under the same conditions; investigate regressions before making a release claim.
Update docs/runtime.md and the affected tool/auth/task/diagnostic documentation
with the implemented architecture and any retained boundary exceptions. Add the
customer Effect suite to the existing CI structure without dropping native suites
or running fixtures concurrently when they share generated artifacts/state.
Exercise a real pre-migration customer bundle upgraded to the candidate bundle
against the same persisted native storage. Preserve conversations/transcripts,
encrypted credentials, instructions, memory, tasks, schedule callback payloads,
cookies, and pending recovery state. The existing test:upgrade-state uses two
fixture bundles; extend its evidence to cover the actual pre/post migration code.
Build a new immutable release through the existing packaging path when shipping is authorized. Preserve Worker names, PersonalAgent/Conversation/Sandbox class identities, namespaces, bindings, application schemas, and customer secrets. Declare the supported source artifact digests using the existing catalog policy; never mutate old release archives. Recovery after a failed rollout remains the existing forward-repair procedure. Publishing this plan does not deploy a release.
Exit: all applicable CI suites, upgrade compatibility checks, and the connected native golden path pass, with remaining environment limitations stated explicitly.
Validation and completion #
Run pnpm typecheck, the focused customer Effect suite, and the affected native
suites in each phase. Rebuild dist/release before tests that consume it, and
build the control-plane fixture where required. Preserve the existing distinction
between local fixture builds and publishable production artifacts.
The final validation follows all four jobs in
CI: core/control, execution, browser/UI, and Sandbox.
It includes test:diagnostics, test:upgrade-state, test:deployment,
test:orchestrator, test:catalog, and test:golden-path, in addition to the
phase-specific suites. Docker is required for native shell/golden-path evidence;
Chromium is required for the relevant browser flows. Report opt-in live-provider
tests separately, and do not infer deployed edge routing or provider behavior
solely from local tests.
The migration is complete when every scoped application operation composes as an
Effect, required native boundaries are documented, and there are no parallel
legacy implementations. Existing client behavior and stored identities/formats
must remain compatible, native failure/recovery tests must retain their assertions,
and measured bundle/startup changes must be recorded. A dependency import count
or blanket replacement of async is not a completion criterion.
Implementation should revisit these existing lessons at the corresponding phase: slow response bodies retaining their deadlines; browser acquisition outliving its conversation facet; native fetch receiver requirements; deleted-facet socket guards; and tool activity clear/recovery using distinct native lifecycle paths. These are preserved behaviors, not opportunities to remove safeguards as boilerplate.