Typed user interactions and durable flow continuations share one SQLite store
(data/confirmations.db, core/confirmation_store.py).
The versioned pawflow.user-interaction.v1 record is the durable semantic
contract. Webchat, PawCode, and VS Code render that record according to their
capabilities; presentation is never the authority for the answer or continuation.
Every client restores pending records after reconnect and submits answers through
the generic respond_interaction server action.
An agent or a flow asks the user something — yes/no, a single choice, or several choices from a list. The request is durable: it survives page reloads and server restarts, and the user answers whenever they want (minutes, hours, or days later). Answering resumes the requester.
- Agent tool
request_confirmation(message, mode=confirm|choice|multi, options?, title?, expires_in?, wait_seconds?)— approval-exempt (it is a user interaction). Default behavior is fully asynchronous: the tool returns "pending" and the agent ends its turn; when the user answers, the agent is woken through the PollScheduler with the answer in its wake reason and continues from where it left off.wait_seconds(max 120) optionally polls for an immediate answer first.expires_inaccepts"2h","3d","1mo"...ask_userremains for quick live questions in an active exchange;request_confirmationis for decisions that must not be lost. - Flow task
requestConfirmation(message, mode, options comma-separated, title, expires_in) — publishes into the conversation of the deploy runtime context (or an explicitconversation_id), stampsconfirmation.request_idandconfirmation.signal_idon the FlowFile, and passes it on. Chain adurableWaitto suspend the branch until the answer (see below).
The requestUserInput flow task extends confirmations without creating a second
store or continuation mechanism. It supports confirm, choice, multi,
text, multiline, integer, decimal, date, datetime, file, and
structured form input. response_schema carries validation constraints such as
text length, numeric bounds, choice options, required form fields, and field
types. The server validates and normalizes every answer before atomically moving
the request out of pending; invalid answers do not resume the requester.
The task accepts only the user and conversation scope injected by the flow
runtime. Configuration and FlowFile attributes cannot redirect a request to
another user or conversation. It stamps interaction.request_id and
interaction.signal_id; chain durableWait on that signal when the branch must
pause for the response.
Generic actions are list_interactions, respond_interaction, and
cancel_interaction. The legacy confirmation actions remain aliases for the
three original input kinds. Reads are scoped to the authenticated owner; writes
also allow conversation collaborators with write access. Unknown and
unauthorized IDs both return 404 so the endpoint is not an existence oracle.
The schema migration is in-place and additive: existing confirmation rows are
preserved, backfilled as contract version 1, and retain their
confirmation:<request_id> signals. Initialization verifies row count, foreign
keys, and that every pending row has a kind and signal before recording the
migration marker.
- Webchat, PawCode, and VS Code hydrate pending interactions from durable server state after reload or SSE reconnect.
- Each client renders the supported semantic input kinds and returns the typed
value through
respond_interaction; server-side validation remains canonical. - Rich UI surfaces use the separate generic
pawflow.ui-surface.v1capability contract. A client that cannot safely render a rich surface uses its declared semantic fallback or shows an explicit handoff instead of silently accepting.
notifyUser publishes a non-blocking notification in the runtime-injected
conversation. It never parks the FlowFile and routes to sent when a live client
is subscribed, queued when durable replay will deliver it later, or failure
on a delivery error. urgency is low, normal, or high.
- The request renders as an actionable inline block in the conversation
(buttons for confirm/choice, checkboxes + validate for multi) — live via
the
confirmation_requestSSE event AND re-rendered from the store after every history load, so it stays actionable after reloads and restarts. - The pending panel (header ✅ button with badge,
/confirmations, or the openspace poster) lists every pending request of the user across all conversations — the durable inbox for answering days later. - Actions:
list_confirmations,respond_confirmation, andcancel_confirmation(owner or conversation collaborators with write access; unknown and unauthorized ids get the same 404). - On answer/cancel/expiry the
confirmation_answeredSSE event closes the block everywhere, and the durable signalconfirmation:<request_id>fires with{status, answer}.
- Agent requester: a wake is scheduled (
[agent:<name>] [confirmation:<id>] The user answered ...: <answer JSON>), delivered as a system-marked turn by the poller — the standard scheduled-wakeup path. - Flow requester: the durable signal
confirmation:<request_id>resolves anydurableWaitparked on it (answered, cancelled, and expired all release the waiter; the status travels indurable.wait.value).
waitForSignal/notify (SignalRegistry) stay the fast in-memory
primitives for short intra-process synchronization. The durable pair is for
everything longer:
durableWaitparks the FlowFile (content + attributes serialized to the store) on a signal for as long astimeoutallows —"90s","12h","30d","6mo","2y", or absent/0 = forever. The task emits nothing at park time; when the signal fires (or the timeout expires), the FlowFile is restored and re-injected at the wait task itself, which recognizes thedurable.wait.statusattribute and passes it through with:durable.wait.status=signaled|timeoutdurable.wait.signal_iddurable.wait.value= the JSON value delivered by the notify Route downstream withrouteOnAttribute. The signal id comes fromsignal_id(static/expression) or from a FlowFile attribute (signal_id_attribute, defaultconfirmation.signal_id).
durableNotifyfires a signal (signal_idorsignal_id_attribute, value fromvalueorvalue_attribute) — every parked FlowFile on it resumes, across flows and restarts. With no waiter, the latest value is remembered: the nextdurableWaiton that signal passes through immediately (no lost-notify race).notify_signalis also callable from any Python code.- Restart-safe delivery: a resolved wait whose flow instance is not
running stays
resolvedand is retried by the background sweeper (15 s) until the instance is running again (ExecutorRegistry+executor.inject(ff, entry_task_id=<wait task>)). Backpressure at injection is also retried. durableWaitrequires a deployed continuous flow — a one-shot batch run cannot receive the re-injection and fails at execute time with an explicit error.
durableTimer parks a FlowFile until exactly one configured deadline: a
relative duration (30s, 5m, 2h, and the same long-duration units as
durableWait) or an absolute timezone-aware ISO-8601 until timestamp. It
does not sleep in a task worker. The SQLite continuation is swept, restored,
and re-injected at the timer task with durable.timer.status=elapsed and
route.relationship=elapsed; cancellation uses the cancelled relationship.
An undelivered timer or signal continuation prevents idle auto-stop.
requestConfirmation ──> durableWait ──> routeOnAttribute(durable.wait.status /
durable.wait.value)
The user answers from the webchat pending panel — 24 hours later if they want — and the branch resumes with the answer.
core/confirmation_store.py— versioned typed interaction validation and migration, store, sweeper (expiry, wait timeouts, delivery retries), agent wake routing, and timeout parsing.core/handlers/user_interaction.py—request_confirmationtool.tasks/control/durable_confirm.py— typed input, notification, confirmation, timer, wait, and notify flow tasks.tasks/ai/actions/confirmations.py— authenticated generic interaction and compatibility confirmation actions.tasks/io/chat_ui/confirmations_panel.js— inline blocks, pending panel, badge, hydration.pawflow_cli/andpawflow-vscode/— terminal and editor interaction restoration/rendering/response paths.- Tests:
tests/test_confirmation_store.py,tests/test_chat_ui_confirmations.py,tests/test_pawcode_event_dispatch.py, andtests/test_ui_surface_clients.py.