Minor Changes
-
#2249
ebab868Thanks @mattzcarey! -WebSocketsspeaks the Agent protocol for plain hosts, and gains a Cap'n Web transport.- A plain Durable Object composed with
WebSocketsnow works withuseAgentandAgentClient: the capability sends the identity frame on connect (protocol: falseleaves the connect sequence to the host) and servescallables— anRpcTargetwhose prototype methods are the host's remote interface. useAgent({ transport })andAgentClient({ transport })pick the wire."cf-websocket"(default) is the hibernating socket;call()/stubsend JSONrpcframes."capnweb"runs one Cap'n Web session that carries protocol frames and servescallablesnatively:call()/stubinvoke them directly, anRpcTargetresult is a live stub, aReadableStreamstreams, calls pipeline. The Durable Object stays in memory while a capnweb connection is open. PartySocket still owns reconnection on both.@callable()decorators remain the JSON-wire interface ofAgent; an Agent wanting native calls passescallables.- The experimental
?__agents_rpc=capnwebendpoint from 0.23.0 is removed. LifecycleServicesexposesnameandclassName.
- A plain Durable Object composed with
-
#2245
beff78aThanks @mattzcarey! - Add theQueueLifecycle capability (agents/queue) for durable background work. Each pushed item is a job in the Lifecycle job queue, due immediately, run from the alarm loop one at a time in push order with Lifecycle's retry, deadman, and memory-limit policy. Callbacks are registered in the constructor and typed at declaration and push;push()accepts a stableid(upsert) and per-itemretry.Agent.queue()and friends now delegate to the capability. Queued callbacks run from the alarm loop in a fresh invocation, so they no longer see the enqueuing request'sconnectionorrequestthroughgetCurrentAgent()(the agent itself is still available). Thecf_agents_queuestable and the in-isolate drain are gone; legacy rows migrate into the job queue on the next start.queue()acceptsoptions.id;dequeue,dequeueAll,dequeueAllByCallback,getQueue, andgetQueuesare now asynchronous; andQueueItem.created_atis renamedcreatedAt.LifecycleServices.starting()is replaced bystatus(), which returns"zero" | "starting" | "started".Think's workflow-notification outbox and submission drain now run as queue items; the
cf_think_workflow_notificationstable migrates and is dropped on start.Both one-shot migrations (
cf_agents_queuesin Queue,cf_think_workflow_notificationsin Think) are temporary upgrade paths and will be removed in the next minor release, by which point every started object has migrated. Deployments skipping this release should upgrade through it. Workflow-notification delivery retries with backoff capped at ten minutes (previously five) and gives up after twelve hours of continuous failure (previously never), reporting the failure throughonError. -
#2179
43a58a1Thanks @AntoniTok! - Move agent state into the opt-inStateLifecycle capability.State was one method doing four jobs inside
Agent— validate,
persist, broadcast, notify. It moves wholesale into aState
capability that owns storage and change ordering, so any Lifecycle
host gets durable, validated state without inheritingAgent:new State({ initialState: { count: 0 }, validateStateChange: (next, source) => validate(next, source), onChanged: (state, source) => notify(state, source), });
The capability owns the
cf_agents_statestate row, lazy load with an
in-memory cache, initial-state seeding, and validated persistence. It
runs only theonStarthook (versioned schema init under its own
cf_agents:state_schema_versionkey) and reaches Lifecycle only for
storage— no alarm, no request path. It never touches connections.Host-owned behavior is injected, not moved:
validateStateChange
stays an overridableAgentmethod and the post-changeonChanged
hook is passed intoStateas a plain option. Synchronous and
asynchronous notification hooks are both supported.Agentkeeps its
initialStatefield and seeds it from thestategetter (standalone
hosts passinitialStatetoStatedirectly). Broadcast and the
notification hook stay onAgent, and theonMessagestate branch
stays too; only its inner write delegates to the capability.Stateis the only owner ofcf_agents_state: it creates the table,
holds the state row, and runs the legacycf_state_was_changed
cleanup in its own versioned migration.Agentused to keep its
global schema version as a row in that table; it now lives under the
cf_agents:schema_versionKV key like every other capability's
version. A DO created under the old layout has the row read once,
moved to the key, and deleted on its next construction.Agent's public API and wire protocol are unchanged:state,
setState(),onStateChanged, and theCF_AGENT_STATEframes behave
identically. -
#2257
46760e6Thanks @mattzcarey! - TheWebSocketscapability now owns the Agent protocol's state sync and per-connection flags, forAgentand plain hosts alike.statetakes aStatecapability. The current value is pushed to each new connection after identity, a client'scf_agent_stateframe goes through the host'svalidateStateChange(a readonly connection is refused, a rejected change gets a genericcf_agent_state_error), andbroadcastState(source)pushes a change to every protocol-enabled connection but its source — wire it to theState'sonChanged. SouseAgent().stateandsetState()work against a plain Durable Object.protocolandreadonlyare per-connection policies decided at accept time;sendIdentity(),sendState(),applyStateFrame(),isReadonly()/setReadonly(), andisProtocolEnabled()/setProtocolEnabled()are the capability's surface for hosts that drive the sequence themselves. Theidentityoption from the previous changeset is replaced byprotocol.- The readonly and no-protocol flags, their
_cf_storage in connection state, and the wrapper that hides them fromconnection.statemove out ofAgentintoagents/websockets(registerInternalConnectionKeysfor a host's own keys).Agent's public methods and wire behaviour are unchanged; it passesprotocol: falseand drives its connect sequence through the capability after its facet routing decision.
Patch Changes
-
#2248
c96418dThanks @mattzcarey! - Lifecycle capabilities declare how they claim traffic withclaims: "selective" | "catch-all"instead of hosts passing{ fallback: true }tolifecycle.use(). A catch-all always dispatches last, whenever it was installed. Catch-alls are unique per dispatch hook, so one foronRequestand one foronWebSocketUpgradecoexist while a second for the same hook is refused.WebSocketsdeclares itself a catch-all for upgrades and never declines one: withouthandlersit still accepts and tracks connections; handlers only add behavior on connect, message, close and error.LifecycleUseOptionsis removed. -
#2244
f6e556fThanks @mattzcarey! - Preserve a legacyrunFiberresult when deleting its bookkeeping row fails. The cleanup error is logged and the row is left for the existing recovery pass instead of replacing the fiber body's outcome. -
#2297
0fd3c8bThanks @ben-reitz! - ExposeResumableStream.pendingCutoverIdso callers can detect whether a finished stream still awaits its cutover, and clear the pending-cutover marker only after settlement succeeds infinalizePending, keeping a failed settlement retryable instead of silently leaving the stream row live. -
#2288
9efee5fThanks @ben-reitz! - Bumpvalibotto 1.4.2, picking up the fix for a moderate-severity
advisory affectingrecord()schemas andflatten().