Closes audit doc section A.9 + finding #5. The last Phase-1 pipeline
state that lived only in instance memory now writes through to SQLite,
and ToggleFilter changes propagate without a container restart.
Schema:
v10.sql adds inhibit_state(key PK, rank, expires_at, updated_at) and
grouper_held(group_key PK, event_json, hold_until_at, updated_at).
Indexes on expires_at / hold_until_at support the prune sweeps.
SCHEMA_VERSION 9 -> 10.
Migration runner:
Fixed the alphabetical-vs-numeric sort bug v10 surfaced -- the runner
now sorts pending migrations by their integer version, not by
filename, so v10.sql correctly applies AFTER v9.sql (was applying
after v1 alphabetically, which made schema_meta stick at 9).
Inhibitor (meshai/notifications/pipeline/inhibitor.py):
- __init__ restores non-expired keys from inhibit_state on construct.
- handle() write-throughs every (key, rank, expires_at) tuple.
- _prune_expired DELETEs the same expired keys from disk.
- clear() (test path) drops the table.
Grouper (meshai/notifications/pipeline/grouper.py):
- __init__ restores non-expired held events from grouper_held; the
Event is rebuilt via Event.from_dict(json.loads(event_json)).
- handle() write-throughs (group_key, event_json, hold_until_at).
- tick() and flush_all() DELETE on emit.
ToggleFilter (meshai/notifications/pipeline/toggle_filter.py):
- new refresh(config) method re-reads config.notifications.toggles and
rebuilds the enabled set.
Live wiring:
- meshai/dashboard/api/config_routes.py adds a POST
/api/notifications/refresh-toggles endpoint that reaches into
app.state.bus._pipeline_components["toggle_filter"] and calls
refresh(app.state.config). The frontend pings this after PUT
/api/config/notifications so toggles take effect on the next event.
- meshai/main.py stashes self.event_bus on the dashboard FastAPI
app.state after build_pipeline so the route can reach it.
- Inhibitor.ttl_seconds and Grouper.window_seconds already read from
adapter_config.pipeline.{inhibitor_ttl_seconds, grouper_window_seconds}
via the v0.6-3b None-default wiring (rows seeded in v0.6-3a.1).
Tests (tests/test_pipeline_persistence.py, 11 cases):
- v10 tables present
- Inhibitor: state persists across simulated restart; expired rows
not restored; prune removes from disk; clear() wipes both.
- Grouper: state persists across restart; tick() clears disk;
expired rows not restored.
- ToggleFilter: refresh() picks up new enabled set; refresh(None)
is a no-op; disabling a family in config + refresh drops it.
Test count: 819 -> 830 (+11 pipeline persistence cases + schema test
bump).