Absorb user-DO resets in the Overseer's session capabilities - #162
Conversation
The Worker side already re-resolves its user-DO stub on every call (#133), but the Overseer kept the older design: open() minted owner and clientUser stubs once and the session capability classes (OverseerClientInterface, UseOverseerInterface, GadgetClientImpl, UseGadgetClientInterface) retained them for the session's lifetime. A stub is bound to one incarnation and permanently broken once it resets, so after a routine user-DO reset every user-DO-carrying call on the still-open session -- newChat, sendChatMessage, listModels, setPinned, createGadget, connector creation, owner metadata -- kept failing until the WebSocket reconnected. Apply the same fix one layer down: the capability classes store the user ID and mint a fresh stub per call through #owner/#clientUser getters, wrapped in wrapDoStubForTelemetry. The wrapper now takes an optional logger so Overseer-observed resets attribute to workshop.overseer with the gadgetId, distinct from the Worker chokepoint's workshop.server events. The getters must be native #-private: capnweb exposes prototype getters over RPC (instance properties are blocked, getters are the documented opt-in), so a TypeScript-private getter would have handed browsers the raw user-DO stub -- including getChatContext(), which returns API keys. GadgetClient creation sites pass the ID, not a stub: handing a child capability the getter's value would give it a fresh stub that it then stores for its lifetime, reintroducing the bug at gadget scope. Cross-call e-order to the user DO is lost by design, matching the Worker layer: the capability calls are pure reads or guarded writes (updateTitle/updatePinned throw if the record is missing rather than upsert), and the one multi-call sequence in open() awaits sequentially. Deliberately NO retries (per #133) and NO call deadline: a user DO wedged behind a stuck input gate still hangs newChat indefinitely. That needs a bounded-timeout mechanism and is left as a follow-up. The integration test pins the asymmetric reset the retained-stub design could not absorb: the user DO alone is aborted via runInDurableObject (abortAllDurableObjects() would kill the Overseer too), then chat, model listing, and gadget creation succeed through the same retained workspace capability. It fails against the previous overseer.ts with the poisoned-stub rejection and passes with this change.
|
This is consistent. The whitelist matches the I've completed a thorough review. Let me summarize my assessment. ReviewThis is a clean, well-motivated fix. The change mirrors the existing Verification points I checked:
Observations (non-blocking, no action needed):
LGTM! |
Two findings from an audit of the fresh-stub change: The #owner getters read this.impl.ownerId with a non-null assertion, but deleteSelf() clears ownerId ~100ms before the revocation restart aborts the object, so a concurrent call on a still-open session could hit idFromString(undefined) and throw a raw TypeError. Guard with the same "Workspace has been deleted." error #ownerUserStub() uses. Losing cross-call e-order makes one frontend sequence newly reversible: the in-workspace rename (handleSaveTitle) runs on the long-lived session with nothing disabling the save affordance mid-flight, so two rapid saves could land at the user DO in either order and leave the home-page listing title diverged from the workspace title. Ignore saves while one is in flight, matching the in-flight guards on the blueprint pin and quick-model toggles.
|
The Note I've completed a thorough review. Let me consolidate my findings. Review SummaryThis is a well-motivated, correctly-implemented fix. The core change replaces retained user-DO stubs (captured once at Points I verified:
Non-blocking observations (no action needed):
LGTM! |
When a user DO resets under an open workspace, the session stays broken until the browser reconnects. Chat sends fail, the model picker and gatekeeper setup wedge until a reload, and pin/title updates stop propagating. The cause is the same one #133 fixed at the Worker layer: the Overseer minted its user-DO stubs once at
open()and reused them for the whole session, and a stub is permanently broken once its incarnation dies.Changes
The session capability classes now store the user ID and create a fresh stub for each call, through the same telemetry wrapper the Worker uses.
Deliberately out of scope
No retries, matching #133. No call deadline either: a user DO wedged behind a stuck input gate hangs
newChatsilently for an unbounded-but-usually-finite window (i.e. the 30s DO timeout), then rejects (and now recovers). Will add as a followup if we think its necessary