How Exec Events Are Encoded and Decoded Across the RPC Boundary in Computer
Exec events in Computer are encoded as Cap'n Proto binary structs for transport over a WebSocket-based RPC layer, then decoded by the client stub into TypeScript ExecEvent objects that can be streamed and replayed from persistent SQLite storage.
The Computer project by Cloudflare implements a remote execution system where shell commands run in a server-side runtime and stream output back to clients. Understanding the encoding and decoding mechanism for these exec events across the RPC boundary is essential for developers building on or debugging this architecture.
The Exec Event Lifecycle: From Spawn to Stream
The encoding and decoding flow spans six distinct phases, each handled by specific components in the codebase.
1. Command Invocation via RPC Interface
The client initiates execution by calling the exec() method. The RPC contract is defined in packages/rpc/src/interface.ts, where the server-side Runner.exec receives an ExecInput struct containing the command source and configuration.
2. Event Generation in the Runner
Inside packages/computerd/src/exec/runner.ts, the Runner.exec method spawns the process. Each output fragment—stdout, stderr, exit status, or error—is wrapped in an ExecEvent record and persisted via the enqueue function to the SQLite computerd_exec_log table.
3. Wire Encoding with Cap'n Proto
The encoding mechanism is Cap'n Proto binary serialization. The ExecEvent struct (defined in packages/rpc/src/interface.ts) is serialized to its wire format and transmitted over the persistent WebSocket underlying the Cap'n Web transport. The wire contract is documented in docs/08_capnweb_interface.md.
4. Stream Construction for Replay
The replayToStream function in packages/computerd/src/exec/runner.ts constructs a ReadableStream<ExecEvent>. It queries the SQLite log, converts stored rows back to Cap'n Proto structs, and pushes them into the stream—enabling both live and historical event consumption.
5. Decoding on the Client Side
The decoding mechanism runs in packages/rpc/src/client.ts. The generated RPC client stub receives binary Cap'n Proto messages, deserializes them into TypeScript ExecEvent objects, and yields them from the ReadableStream returned by the original exec() call.
6. Event Consumption in User Code
The caller iterates the decoded stream using for await...of syntax to process stdout chunks, stderr output, and the final exit code.
Encoding Format: Cap'n Proto Struct Details
Cap'n Proto provides several advantages for this use case:
- Zero-copy deserialization — The client can access fields without parsing overhead
- Packed encoding —
ExecEventstructs are compact on the wire - Schema evolution — New event types can be added without breaking existing clients
- Streaming support — Messages can be sent incrementally over the WebSocket
The ExecEvent type discriminates on a type field with variants including "stdout", "stderr", "exit", and "error". Each variant carries appropriate payload fields (e.g., data: Uint8Array for output chunks, code: number for exit status).
Code Examples: Encoding and Decoding in Practice
Running a Command and Streaming Decoded Events
import { createWorkspaceClient } from "@cloudflare/computer";
const client = await createWorkspaceClient(/* workspace config */);
// Encoding happens server-side; client receives decoded ExecEvent stream
const handle = await client.exec({
source: "echo hello && echo world >&2 && exit 3"
});
for await (const ev of handle.events) {
switch (ev.type) {
case "stdout":
process.stdout.write(new TextDecoder().decode(ev.data));
break;
case "stderr":
process.stderr.write(new TextDecoder().decode(ev.data));
break;
case "exit":
console.log(`\n[exit: ${ev.code}]`);
break;
case "error":
console.error(`\n[error: ${ev.message}]`);
break;
}
}
Replaying Events from Persistent Storage
The decoding mechanism supports replay for clients that disconnect and reconnect:
// Re-attach to completed exec with full history
const replay = await client.getExec({
id: handle.id,
after: 0 // Sequence number offset
});
for await (const ev of replay.events) {
// Same ExecEvent decoding as live stream
processEvent(ev);
}
The after parameter enables resumption from any point in the log, with the server re-encoding stored events through the same Cap'n Proto pipeline.
Controlling and Cleaning Up Executions
// Signal the running process
await client.killExec({ id: handle.id, signal: "SIGTERM" });
// Dispose releases SQLite log entries and server-side resources
await client.disposeExec({ id: handle.id });
Key Implementation Files
| File Path | Responsibility |
|---|---|
packages/computerd/src/exec/types.ts |
TypeScript type definitions for ExecEvent and related structures |
packages/computerd/src/exec/runner.ts |
Event generation, SQLite persistence, and replayToStream implementation |
packages/rpc/src/interface.ts |
RPC method signatures and Cap'n Proto struct declarations |
packages/rpc/src/client.ts |
Client stub with Cap'n Proto decoding logic |
docs/08_capnweb_interface.md |
Wire protocol specification for the encoding format |
Performance and Reliability Considerations
- Backpressure handling — The
ReadableStreaminterface propagates flow control across the RPC boundary - Persistence guarantees — SQLite logging ensures no event loss even if the client disconnects
- Binary efficiency — Cap'n Proto's packed encoding minimizes bandwidth for high-volume output
- Type safety — Generated TypeScript bindings from Cap'n Proto schemas prevent decoding errors
Summary
- Encoding mechanism: Cap'n Proto binary serialization of
ExecEventstructs inpackages/rpc/src/interface.ts - Transport: WebSocket-based Cap'n Web protocol with persistent connections
- Persistence: SQLite
computerd_exec_logtable enables replay and resilience - Decoding mechanism: Generated client stub in
packages/rpc/src/client.tsreconstructs TypeScript objects - Streaming interface:
ReadableStream<ExecEvent>provides idiomatic async iteration for consumers
Frequently Asked Questions
What encoding format does Computer use for RPC messages?
Computer uses Cap'n Proto for all RPC message encoding, including exec events. This binary format provides zero-copy deserialization, compact wire representation, and strong schema evolution guarantees compared to JSON or Protocol Buffers.
Can exec events be replayed after a client disconnects?
Yes. The computerd runtime persists all ExecEvent records to SQLite via the enqueue function in packages/computerd/src/exec/runner.ts. Clients can call getExec({ id, after }) to request a decoded stream starting from any sequence number, enabling seamless reconnection and historical replay.
How does the client decode exec events from the wire?
The generated RPC client stub in packages/rpc/src/client.ts handles Cap'n Proto decoding automatically. Application code receives fully-typed TypeScript ExecEvent objects through a standard ReadableStream, with no manual deserialization required.
What happens to exec events after a process completes?
Events remain in the SQLite computerd_exec_log table until disposeExec({ id }) is called. This allows late-joining clients to retrieve complete output, but requires explicit cleanup to prevent unbounded storage growth.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →