When Iris creates a single reminder, there's no friction. "Remind me to call the dentist tomorrow at 11" - done, one row, instant confirmation. But real usage doesn't look like that. It looks like: "Remember that I'm allergic to shellfish, I prefer window seats, my anniversary is March 14th, and remind me about the dentist next Tuesday and to pay rent on the 5th." Five things. Three types. The old behaviour was fire-and-forget: the model called tools one at a time, each created its entity immediately, and you found out what happened after the fact. No review, no take-backs.
The batch memory work from yesterday made multi-item storage possible. But storing five things instantly when one of them has the wrong date isn't much better than storing them one at a time. The missing piece was a checkpoint between "the AI decided what to create" and "things actually exist in the database."
Proposals as a staging area
Think of it like a shopping basket. Items go in, you review them, you remove the ones you don't want, then you check out. A BatchProposal holds every item the model wants to create - reminders, memories, scheduled tasks - with a pending status. Nothing gets persisted until you explicitly confirm.
The threshold is 3 items. Below that, tools execute normally. At 3 or more, an AutoProposesItems trait intercepts and bundles everything into a proposal instead. The response includes a [PROPOSAL:id] marker that the chat layer picks up and renders as a review card instead of plain text. The model doesn't even know proposals exist. It calls tools the same way it always did. The trait handles the routing.
There's also a ProposeBatch tool for the explicit case where the model wants to propose a mixed batch - reminders and memories and tasks in one go. Same staging area, same review flow.
The review UI
On the web, proposals render inline in the chat. Each item shows a type badge (Reminder, Memory, Task), the content, and scheduling details where relevant. Per-item buttons: approve, skip, or edit. Edit opens an inline form pre-filled with the proposed data so you can fix a time or reword a memory before confirming. Skip marks the item to be ignored. Then one confirm button processes the whole batch in a single pass, respecting every choice you made.
This is the same progressive-disclosure pattern from the sub-agent panels. Show the summary, let the user drill into details only when they want to.
Isolated failures
Each item gets its own try/catch during execution. A failed reminder (unparseable time, for instance) doesn't block a successful memory. The proposal's final status reflects reality: confirmed if everything worked, partial if some failed. Item-level error messages land on the individual rows. "Could not parse reminder time: next Caturday" tells you exactly what went wrong and where.
This was a deliberate choice. The batch memory storage from yesterday already had the same principle - skip duplicates, report them, keep going. Proposals extend that to every item type.
Keeping it clean
Proposals expire after 24 hours. A scheduled purge job runs hourly and flips stale ones to expired. No ghost proposals accumulating in the database. Same hygiene pattern as the conversation queue cleanup.
What changed in practice
Before: "Set reminders for all of these" created 5 reminders instantly. One had the wrong time, one you didn't actually want. You'd have to go find them in the reminders list and delete them manually. After: you see all 5 laid out, skip the one you don't need, fix the time on another, then confirm. One deliberate action instead of cleanup after the fact. The AI proposes, you decide.