The app is not the only way in
A desktop client has one door: the window. A phone has several, and two of them matter here — the system share sheet, which lets any other app hand something to Pippa, and the microphone, which lets Dad speak instead of typing.
Both doors turned out to teach the same lesson, from opposite directions: the honest report of a partial result beats a clean-looking total one.
What a share sheet actually hands you
The first defect is subtler than it looks. When another app shares something, the system describes each attachment by the types it conforms to — and a shared file also conforms to url, because it has a location on disk. So a naive chain that asks "is this a link?" first, and a photo second, takes every photo as a link and drops the image.
That is a real bug a sibling app shipped first. The fix is to ask what each provider is — ranking the registered types, with link considered last — before loading anything. The shared kit now owns that ranking, so the whole household gets the answer once.
The extension cannot open its host
An iOS share extension is a separate process with a real constraint: it cannot launch the app it belongs to. So the honest promise it can make is not "posted" — it is "saved to Pippa".
The mechanism is a drop box in a shared container: one directory per share, media written first, the manifest written last through a single atomic write. Manifest-last means a half-written share is invisible rather than corrupt — there is no moment where a readable manifest names bytes that are not there yet. On the next foreground, the app lists what is pending and offers Post as is, Edit or Discard.
The partial share, and where the words live
Here is the part worth the lesson. Loading an attachment can time out. In the first version, one attachment past its deadline threw out of the collection step — and the extension wrote nothing at all. Eight photos shared, one slow, zero saved.
The policy now: save what arrived, and record every attachment that did not, with why. Timeouts, unreadable items, items over the limit, and the app's own staging drops — an undecodable photo, a ninth photo when eight is the limit, a second clip. The banner shows that line above the three buttons.
The same instinct governs the reading side. When the app lists pending shares it returns the readable ones and the manifests that would not parse, each with its reason — and the Stream tab shows a line saying so. Nothing is deleted. A queue that is quietly wrong is worse than one that says it is wrong; that sentence has now earned its place twice in this track, once in the outbox and once here.
One more small piece of durability: a share record decodes with defaults for every field except the three that identify it. So a manifest written by an older version of the extension still reads after the app gains a field. The extension and the app update on the user's schedule, not yours.
The microphone, and where the audio goes
Voice input is the second door. The button records, the file goes to cwkPippa's own transcription route, and the transcript lands in the composer for Dad to read before sending.
The interesting decision is where the audio travels. It does not go from the phone to a speech vendor. It goes to the household engine, which hands it to the voice sibling that already holds the family's credentials for exactly this. One place in the house knows that key; the phone knows the house.
And the language is the composer's pick, forwarded explicitly — never auto-detected. Auto-detection on a bilingual household is a coin flip that fails silently on short utterances, and a wrong-language transcript looks like a bad microphone rather than a wrong setting. Two microphone buttons, Korean and English, and the ambiguity never exists.