2026-09-27·by Sijie Wang#standmeet#architecture#design#events

async-response-contract

Response contract once work is async

Parent: events

Status: released in v0.1.76 (2026-09-27) — design and as-built record in docs/design/event-bus-outbox-webhooks.md in the StandMeet repo.

A response never claims what has not happened. It promises only what is committed. Work still in flight is represented by a receipt (an event id or a job id) whose state can be read. Where a caller needs to read its own write, the request waits briefly, then falls back to the durable job.

Per side effect

Side effectWhat the response promisesHow the caller learns the rest
Search index (search right after write)corpus.create / corpus.update / corpus.promote wait up to 2 s and return indexed: true; on timeout indexed: false plus index_job_idMCP or UI reads tasks.get; an AI that sees false knows to search again later
Access-request notification"Submitted", never "the owner has been notified"The owner sees the mail job in the tasks-panel; a final discarded raises an alert
Approval mailThe code (issued synchronously) plus the mail job; MCP access_requests.approve waits up to 2 sThe request row shows the mail state: sending, sent or failed (completion-hooks)
Booking notificationThe booking succeeded (the booking row is committed); the notification state is separateSame as the access-request notification
Obsidian importThe number of notes imported (committed)The per-note trigger events index each note; the Tasks panel shows the jobs
Microsite buildQueued, plus the build idWaiters wake on microsite.build.settled (NOTIFY keyed by owner), no longer an in-process signal
jobs.fetch_newThe listings when every source finished within 20 s; otherwise {job_ids, pending: true}jobs.fetch_result {job_ids} returns the same shape
WebhooksA write response to the owner promises no deliveryThe delivery log

Wait up to 2 s, then hand back a receipt

The wait has three steps: find the note's latest corpus.note.changed event (LatestFor), wait for the relay to fan it out (AwaitFanout, woken by NOTIFY standmeet_events_fanned), then wait for the corpus.index job (Wait, woken by NOTIFY standmeet_job_final). Each wait is served by one shared LISTEN connection per process, not one polling connection per request. At most 64 waiters per channel; beyond the cap, the request returns indexed: false at once instead of queueing (concurrency-control). The completion signal is a pg_notify, not an outbox event (completion-hooks).

In the common case a receipt costs the index queue's poll (≤ 100 ms, concurrency-control) plus River's batch completer, which records completions every 250 ms.

The copy rule

The same rule governs copy. UI and MCP text states only confirmed facts. "Sent" appears only after the job is completed.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →