"Share the part that's identical; diverge exactly where ownership differs. Not one line sooner, not one line later."
The Shared Middle
Pippa Go has two flows that carry images: Prompt Macro (a utility that runs the composer without a conversation turn) and Ask (canonical streaming chat). Both begin with the exact same problem — a user picked an image, and it now sits in device-local staging as a blob with some metadata. Turning that staged blob into real, usable bytes plus a correct media type is identical work regardless of which flow you're in. So it's done once, in a single shared staged-media resolver. Duplicating that resolution in two places would be the classic drift bug: two copies that slowly disagree about encoding, orientation, or media-type detection.
The Deliberate Divergence
Then, having resolved the bytes, the two flows split — and the split is principled, not incidental. They diverge at the ownership boundary, because what happens to the image depends on who owns the result:
- Prompt Macro → utility envelope. Macro is an apply-free utility, not a conversation. Its image rides in a lightweight utility envelope (inline encoded bytes) suited to a one-shot call that doesn't create canonical state.
- Ask → canonical envelope. Ask creates a real conversation turn, so its image is uploaded as a canonical attachment and referenced by the chat message — owned by the backend like everything else in a canonical turn.
Same resolved bytes, two envelopes, because Macro's result is disposable and Ask's result is canonical. The divergence isn't a wart; it's the ownership model showing through the code.
The Rule: Share to the Boundary, Diverge at It
This is a reusable design instinct. When two flows share early work and differ later, factor the shared part into one component and let them diverge exactly at the point their requirements actually differ — here, the ownership of the resulting image. Share too little and you duplicate the resolver (drift). Share too much and you force Macro and Ask into one envelope that fits neither (a leaky abstraction). The staged-media resolver draws the line in the right place: everything up to "we have real bytes + media type" is common; everything about where those bytes live is per-flow.