Consolidation inventory: 101 items

Updated · View the entry on sijie.xyz ↗

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 full sweep of backend, app, sdk, builder, im-bridge, infra/plugins and the updater found 101 items of hand-wired "after X, also do Y", and each one got exactly one target. Targets: E emit an event plus a subscriber, J durable job, P periodic job on River, W exposed as a webhook event type, K keep. Row numbers refer to the sweep (as of origin/main 6eb8c0a64). The "As built" column says where each move landed. Phases are in events-roadmap.

pie showData
  title Where the 101 items go (an item may count in several)
  "E emit event" : 22
  "J durable job" : 22
  "P periodic job" : 9
  "W webhook type" : 23
  "K keep" : 44

Moves: after-write side effects

# What Where it was Failed by Target As built
1 Access request → email the owner access_requests.go:59 → request_notify.go:44 blocked the request through retries; lost on failure; Redis throttle dropped silently E access_request.created + J owner.notify (owner/subscriber/mail.go); the 5-per-hour burst cap is a Postgres slot and stays a deliberate drop
2 Approve request → mail the code + mark replied access_approval.go:86-89 mail sent but status not written = inconsistent E + J access_request.approval_mail enqueued with the code in one transaction; replied written after the send
4 Email-change confirmation email_change.go:138 failed send left a dangling pending row J owner.email_confirmation; the link token is minted at send time
5 Booking → notify owner booker-mcp.js:458 → invoke_background.go:53 detached goroutine, lost on restart E booking.created + J the booking.record host op writes booking.* and a booking_notices row in one transaction; owner.notify sends and deletes it
7 Booking persist failed → delete calendar event (compensation) booker-mcp.js:309,337 failed delete left an orphan event, never retried J durable supplier.invoke
8–10 Meili upsert / delete / reindex after publish corpus_crud.go, subjectivity.go, corpus.go:139, output.go:61, seo.go:126,140 skipped calls, slow requests, process-local dirty flag E corpus.note.changed + J corpus_notes trigger + corpus.index (P1)
11 Full rebuild after Obsidian import obsidian.go:115,133 goroutine, lost on restart J per-note trigger events; goroutine deleted (P1)
14 Build settled → auto-publish homepage builds.go:242 failure only logged, never retried E microsite.build.settled + J owner.SettleBuild writes the build row, the event and pg_notify('standmeet_build_settled', owner) in one transaction; subscriber microsite.homepage_publish
15 Build settled → recompute asset refs builds.go:247,256 same E + J subscriber microsite.asset_refs
16 Build settled → wake preview long-poll builds.go:193 → buildnotify in-process broadcast, broke with replicas E (LISTEN/NOTIFY) pgstore.Listener keyed by owner; the version is the durable ms timestamp of the owner's latest settle; infra/buildnotify deleted

Moves: goroutines, periodic jobs, calls inside requests

# What Where it was Failed by Target As built
22 InvokeBackground (background supplier call + retry) invoke_background.go:49-65 lost on restart, no dead letter, single process J supplier.invoke internal/infra/sideeffect/supplier; invoke_background.go and notifyPolicy deleted (P4)
23 Obsidian rebuild goroutine obsidian.go:135 same as #11 J deleted (P1)
30 The periodic scheduler itself periodic.go:47-92 ran on every replica; state in memory P River periodic jobs (leader, run on start); the in-process scheduler deleted (P1)
31 Meili 8 s reconcile corpus_index_periodic.go:40 relied on a process-local flag delete deleted (P1)
32–36 public conversation prune, gas refill, resume-draft sweep, traffic retention, usage cleanup each *_periodic.go ran on every replica P declared as data, run on River (P1)
38 Boot-time Meili index + full backfill wire/search_index.go:22 slowed boot J corpus.reindex enqueued at boot (P1)
39, 62 Job-source fetch (all sources serially inside the request) sources_write.go:133, jobfetch.go:149 slow; could hit the 30 s write timeout one J per source + E jobs.fetched jobs.fetch_source on the fetch queue; jobs.fetch_result. No scheduled fetch: job-loop.md rejects an automatic daily fetch (P4)
63 Outbound mail (retry.Do inside the request) mail_retry.go:49, outbound_sender.go:103 request blocked through backoff J (covers #1, #2, #4) mail port internal/infra/sideeffect/mail; internal/infra/retry deleted (P4)
98 Mail throttle mailthrottle.go:82 dropped silently throttle hit snoozes the per-recipient cap (30/h) snoozes to the next window (P4)
101 Background-jobs list in the monitor panel jobreg_registry.go:24 reset on restart reads River jobs JobRegistry deleted; instance.jobs reads River (tasks-panel) (P1)

Client polling → pushed events (Phase 5, not built)

# Polling Where Interval Target
50 Microsite list long-poll use-microsites.ts:127 held request SSE
51 Poll a build until settled use-microsites.ts:290 1.5 s SSE
52 Sidebar access-request badge use-sidebar-badges.ts:39 60 s SSE
54 Wait for Google Calendar after OAuth use-gcal.ts:161 1 s × 15 SSE
56 SDK recovery of a dropped turn agent-adapters.ts:186 1 s × 6 SSE

Webhook event types (all thin; none subscribed by default)

The sweep listed 23 rows. As declared they are 37 types, plus corpus.note.changed and webhook.test: 39 in all, every one Webhook-exposed (event-model). See webhooks for delivery.

# Events Phase
75–76 access_request.created / .approved / .status_changed P2
77–78 code.issued / .revoked / .redeemed P2
79–82 conversation.started / .message / .pruned, ghost.accepted P2
83 booking.created / .cancelled / .rescheduled P4
84–85 application.committed, jobs.fetched P4
86–88 writing.published / .unpublished, corpus.note.changed, vault.imported P2
89–90 microsite.build.settled, page.promoted_live / .rolled_back / .unpublished, microsite.store.doc_inserted P4
91–93 api_key.issued / .revoked, supplier.connected / .disconnected / .activated, block.installed / .failed P2
94–97 gas.exhausted / .refilled, instance.upgrade_requested, owner.login / .email_changed / .recovery_requested, ip_ban.added P2

Keep (K)

Kind Items
Writes into the same database that are themselves the source of truth #12 cross-link rebuild, #13 import receipt, #17 block-failure record, #19 inference usage, #20 dialog, card and ghost writes, #21 seen job ids
Process plumbing and servers #24–#29
Per-node sandbox cleanup #37
The updater's file signal and polling (it deliberately does not touch the database) #44–#46
Telegram long-polling and in-memory im-bridge sessions #48–#49
External calls whose result a visitor needs on the spot (now sent once, no in-request retry) #64 calendar, #66 media fetch, #67 model listing, #68 spec validation, #71 OAuth, #72 captcha, #73 system probes, #74 inference
Session and cache invalidation #99–#100
Waiting for restart after an upgrade (the server is down during the restart) #55
Traffic recording (decided) #18

Open items, resolved as proposed

# Item Resolution Why
3 Recovery-phrase email K, synchronous The user waits on the login page and must learn on the spot whether it was sent
6 Visitor booking confirmation email K, synchronous The visitor needs the confirmation now; the compensating delete moves to J (#7)
41–43 Build queue (hand-written SKIP LOCKED + lease) K for the queue; its settle hooks go through E The builder is a cross-process Node sidecar and the queue is already durable; little gain from moving it
47 im-bridge polls its config every 15 s K; SSE considered in P5 The config rarely changes
53 System info refreshed every 1 s K Live metrics are naturally polled
57 Visitor tools "appear on the next poll" find the caller before P5 The sweep did not find the client call site
59–61 PDF render for application commit, draft, public report K, synchronous The user wants that PDF now; commit renders first on purpose
69 Marketplace install K The result is needed on the spot
18 Traffic recording K Decided
75–97 Webhook exposure all subscribable, none by default, all thin These are facts of the owner's own instance; security events suit alerting

Fixed along the way (not in the plan)

  • SDK hydration: BlockWidget and use-chat-session read localStorage in the first render (React #418 on prerendered microsites); they now read it after mount.
  • A data race on plugin/mount knownToolSpecs; Meili Index takes a fresh index handle per call; Tasks detail acted on a stale job after switching rows.
  • Three places where the wiki was right and the code wrong: schema.sql lacked visit_event / visit_viewer (found by the schema parity UT); a stale Typst comment in drafts_edit.go; the manual-upgrade copy named a nonexistent STANDMEET_REDEPLOY_HOOK (8 locales).

Four of the moved operations had callers that waited for the result; their completion signals are in completion-hooks and async-response-contract.