Perfect. Here’s a concrete execution checklist you can follow, with your hands-on piece highlighted.
Step 1: Add local RPC contract files (copy/paste)
- Create
packages/sessions/src/rpc-contract.ts with:
SyncData import/re-export from local ./types.ts
ProbeValueInput, ProbeValueOutput
ShareRpc interface:
createShare(sessionID: string)
syncShare(shareID: string, secret: string, data: SyncData[])
probeValue(input: ProbeValueInput)
probeCallback(cb: (msg: string) => string | Promise<string>)
- Create
packages/opencode/src/share/rpc-contract.ts with the same shape (temporary duplication).
Step 2: Add sessions RPC implementation
- Add
packages/sessions/src/rpc.ts:
class ShareRpcImpl extends RpcTarget implements ShareRpc
- Constructor takes env + storage adapters (or helper funcs).
- Reuse existing logic from
packages/sessions/src/index.tsx for:
- share create
- sync merge + index update + DO broadcast
- Add learning methods:
probeValue(input) returns normalized echo including Date and byte length.
probeCallback(cb) does const result = await cb("server-called") and returns it.
- Keep logic small and direct; no behavior changes to existing REST.
Step 3: Expose RPC route in sessions worker
- In
packages/sessions/src/index.tsx:
- Import
newWorkersRpcResponse from capnweb.
- Add route
POST /rpc/share (or /rpc/share for all methods) that returns:
newWorkersRpcResponse(c.req.raw, new ShareRpcImpl(...))
- Keep all existing
/api/* routes untouched.
Step 4: Add CLI RPC client path behind env toggle
- In
packages/opencode/src/share/share-next.ts:
- Import
newHttpBatchRpcSession from capnweb.
- Import local
ShareRpc type from ./rpc-contract.
- Add transport selector:
const transport = process.env["OPENCODE_SHARE_TRANSPORT"] ?? "http"
- If
transport === "rpc":
using rpc = newHttpBatchRpcSession<ShareRpc>(\${await url()}/rpc/share`)`
create() calls rpc.createShare(sessionID)
- queued sync flush calls
rpc.syncShare(share.id, share.secret, data)
- Leave delete/get paths on HTTP for now (small blast radius).
Step 5: Your hands-on learning piece (probeCallback)
- You implement + call:
- In CLI debug path (temporary), call
rpc.probeCallback((msg) => \client-received:${msg}`)`.
- Log returned value.
- What to observe:
- Function parameter is passed as stub (not serialized source code).
- Server invokes it over same RPC session.
- Return value is serialized back to server, then to client.
Step 6: Validate with current payloads
- Keep current
SyncData[] flow as-is for real validation.
- Manual checks:
- Create share via RPC.
- Sync real queued payloads (
session, message, part, session_diff, model).
- Confirm existing REST read endpoint still works:
GET /api/share/:id.
- Learning checks:
probeValue round-trip with Date, Uint8Array, nested object.
probeCallback confirms callback stub invocation.
Step 7: Minimal tests to add
- In sessions tests (new file or extend existing):
- RPC createShare success.
- RPC syncShare success + invalid secret case.
probeValue returns expected normalized data.
probeCallback works with client callback.
- Keep existing API tests unchanged as regression safety.
If you want, next I can draft the exact skeleton code for rpc-contract.ts + ShareRpcImpl signatures only (no business logic filled), so you can implement the callback method yourself first.