A Second Engine Beside the Canvas
On 2026-10-04 Dad ruled that Ariandel should get Cinder's features for the canvas: a Generate panel where the family image engine, cwkEmber, makes pictures from his drawing as reference. The app calls Ember directly, at its address on the tailnet, with no Ariandel engine in between and no token (Ember trusts tailnet callers). Ember owns every catalog, route, credential and bill. Ariandel renders what Ember answers and keeps no model or provider list of its own. The one list it does keep is about size, and you can see it in the panel: the source width row, 512 to 1,024 pixels (Cinder's row), plus a 1K, 2K, 4K fallback for a Pro provider whose catalog names no sizes. Ember also hears who's calling, though not one way for every request. A local generation or an Auto preview carries client_meta.caller = "ariandel" with the artwork's id; Pro's prompt step and its submission carry source_metadata.source_app = "ariandel"; an upscale carries only an ariandel tag; a plain Describe call and the catalog reads carry none.
The panel works from the flattened canvas (the same frame my look sees) or from the selection's bounding box, sent as a JPEG over white. From there: img2img with ControlNet, one to four takes, versions across models and machines, Describe (Ember writes a prompt from the drawing), Auto previews that follow his strokes, upscale, and Pro Generate through Ember's API generators. That's a lot of power next to a painting. Two rules keep it on the right side of the line.
Invariant 13
The rule reads: Ember's pictures reach a layer only through Dad's Stamp, and nothing that can cost is sent unseen.
The first half is about where pictures live. Candidates stay beside the artwork, in the app container under Generations/<id>/, never inside the artwork package, until Dad deletes them or prunes the list to the most recent few (ten by default). A candidate becomes part of the painting only when Dad taps Stamp: it's laid in as a new layer over the area it was made from, fitted inside that area with its own aspect, centered, and opened in Transform so he places it himself. Stamp is his tap, never mine. When I help with Generate, it's the way the previous lesson described: I look at the candidates with my canvas look (subject: candidates gives me a numbered sheet of the newest eight, subject: candidate one of them beside its source with its model, prompt and seed), and if a direction would help, I offer a prompt block. His Insert fills the panel's prompt, with Revert to undo it. It never generates. He presses Generate. The broker even tells me what I'm looking at: Ember's pictures, "references for his own hand, not his painting".
Nothing That Can Cost Is Sent Unseen
Local generation runs on the family's own machines. Pro Generate can route to API generators that bill by use, so it gets two steps. First Ember prepares a prompt from the canvas and Dad reads it; nothing has been sent to a generator yet. Then submitPro asks Ember's automatic route which provider it would use and pins that answer. If the provider's billing isn't one of subscription, credits or included, nothing is sent until Dad confirms that exact provider. The alert says it in plain words: "Spend on <provider>? … billed as <billing>, for this request. Nothing has been sent." Confirmation is bound to the provider by name, so if the route would pick a different one, it asks again.
And a Pro submission is never retried by itself. The comment in the catch block gives the reason better than I could: Ember may have billed it even when the answer was lost. A failed Pro job ends with "Nothing was retried." and a human decides what happens next. A retry loop is fine for a free, idempotent request. A billed request that might have succeeded isn't either of those.
Auto, and Its Guards
Auto is the panel's live preview. Turning it on asks for a preview at once, and opening the panel with Auto already on asks for one 1.2 seconds later. After that, each committed change, never mid-stroke, schedules a preview, debounced by 1.2 seconds. If there's no prompt yet, Ember describes the drawing first, so no job ever goes out with an empty prompt. An unchanged JPEG is never sent twice. Each preview carries a revision number, and a late answer older than the newest revision is dropped however late it lands. Only the newest Auto card is kept; Keep turns it into an ordinary candidate.
Now read where those guards actually sit, because it's narrower than "only while the panel is open and Auto is on" sounds. noteChange and scheduleAuto check both flags when a preview is scheduled. The waiting task checks only that it wasn't cancelled, and turning Auto off or closing the panel cancels nothing: neither setter touches autoTask. So a preview already waiting out its 1.2 seconds, or waiting on Ember's Describe, still goes out after Auto is off or the panel has closed. Only leaving the canvas (close()) cancels it. It's a local, ephemeral job on a family machine, so the leak costs one preview's GPU time, never money. The shape is the thing to keep: a guard checked when work is scheduled is not a guard when the work fires.
The Bug Dad's iPad Found
On 2026-10-04 Dad reported three things from the iPad: Auto did nothing, Describe was missing and the prompt went out empty, and it was unclear which machine ran which model. Commit b14f113 fixed all three, and the first two had different causes worth keeping apart.
Auto died with the first stroke. beginStroke and setMode closed every open panel, the right behavior for a popover. Generate closed, the panel-open flag went false, and Auto's guard returned early on every change after that. Nothing errored; Auto simply had no panel to serve. The fix is a property: CanvasPanel.isDocked, true for Pippa and Generate, the two panels that sit beside the canvas on iPad. A stroke or a tool change never closes a docked panel.
Jobs went out with empty prompts. This one came from the other side of the wire. Ember's own log showed Dad's two jobs sent with empty prompts and no Describe call. Hence the rule above: Generate and Auto describe the drawing first when the prompt is empty. Notice which witness told the truth. From inside Ariandel the state looked fine. The engine on the other end had the receipt.