Three integrated pieces that ship together because they were designed as one safety story: (1) PERSISTENCE FOUNDATION -- new meshai/persistence/ module with SQLite db.py, schema migration framework (v1), 13 tables covering all adapter event shapes (traffic_events, fires, firms_pixels, quake_events, nws_alerts, gauge_readings, swpc_events) + mesh state (mesh_nodes, mesh_telemetry, mesh_positions, mesh_messages_in, mesh_broadcasts_out, mesh_health_events) + cross-cutting event_log + schema_meta. WAL mode for reader concurrency, single-writer pattern, MESHAI_DB_PATH env var, mounted at /data/meshai.sqlite via existing docker-compose meshai_data volume. .gitignore updated. (2) WFIGS HANDLER -- meshai/central/wfigs_handler.py implements the first per-adapter handler that uses the persistence layer. Format: MEDIUM style with town/landclass/county fallback chain, lat/lon at 3-decimal precision, New:/Update: prefix. 8h-rate-limited change-detection per IRWIN via fires.last_broadcast_at. Skips tombstones and perimeters silently (logged to event_log with handled=0). Acres fallback chain DailyAcres -> IncidentSize -> raw.DiscoveryAcres -> raw.FinalAcres -> N/A. Pass-through Initial Attack auto-numbered names (IA 1, IA 2). (3) UNIVERSAL COLD-START GRACE -- meshai/notifications/pipeline/dispatcher.py grows a configurable grace window (cold_start_grace_seconds, default 60s, GUI-editable per Rule 17). Anchored to first-event-seen (not container boot), so the grace activates the moment broadcasts could fire. Suppresses mesh delivery during the window; handler-side persistence (fires UPSERT, event_log) still happens normally. New _cold_start_dropped counter exposed in dispatch_stats(). Designed to protect against JetStream backlog spam at toggle-flip time, applies universally to ALL adapters. (4) WFIGS HANDLER CALLBACK REFACTOR -- New:/Update: prefix now keys on fires.last_broadcast_at IS NULL (not row-missing), and last_broadcast_* field updates moved to a post-broadcast commit callback that the dispatcher invokes ONLY on successful delivery. This means: cold-start-suppressed events leave fires.last_broadcast_at NULL, so when they eventually broadcast post-grace, they correctly render as New: (first ACTUAL delivery for that IRWIN), not Update:. event_log.handled and mesh_broadcasts_out audit row also gated on the same callback -- decoupling persistence rows from broadcast rows for an honest audit trail. New tests: 15 in test_wfigs_handler.py, 15 in test_persistence.py, additional cold-start grace tests in test_dispatcher.py (+4 WFIGS callback scenarios). Synthetic probes wfigs-cleaned-samples.md (initial) and wfigs-cleaned-samples-v2.md (cold-start verification) generated against isolated temp SQLite databases. CT108 /data/meshai.sqlite untouched during build. Master stays off. No live toggle flips. Test count: was 535 (v0.5.7 baseline) -> 566 (persistence) -> 581 (wfigs handler) -> 589 expected (cold-start grace).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four independent low-risk fixes from the deferred list. Bundled in a single
commit because none are large enough to warrant their own tag and none
touch the safe-mode-sensitive paths (dispatcher / consumer / toggle config).
1) .gitignore: change bare `env/` to `/env/` so the rule anchors at the
repo root only. The unanchored form was matching `meshai/env/` (the
adapter package directory) and forced `git add -f` workarounds during
2.14 / 2.16.1. Verified post-fix: `git check-ignore -vn meshai/env/test.py`
reports no pattern match; `git check-ignore -v env/foo` still matches
the new `/env/` rule.
2) meshai/env/ducting.py: health_status.event_count was hardcoded `0`
from before Phase 2.13 added real event emission. Replaced with
`len(self._events)`, which is the pattern every other env adapter
already uses (fires/firms/nws/swpc/traffic/roads511/usgs/usgs_quake/
avalanche). Flows through env.store.health_status → /api/env/status
so the dashboard counter starts reflecting reality.
3) meshai/config_loader.py save_section: list-section secret stripping.
The path landed in C.2.1 fed list items into check_secrets() with
path="" or with `<field>[<i>]` syntax, neither of which matched the
`mesh_sources.*.api_token` / `notifications.rules.*.smtp_password`
regexes in SECRET_FIELDS (where `*` matches a single dotted token).
Result: a raw secret submitted on a list-section save could slip
through to the YAML file. Fix uses dotted-index form `<field>.<i>.<key>`
for both nested-list (notifications.rules) and top-level-list
(mesh_sources) paths. Also extended _raw_section construction +
_ondisk_ref to walk list-shaped on-disk YAML by integer index so
the C.3.1 ${VAR}-placeholder preservation now works for list sections
too. Three new tests round-trip the mesh_sources placeholder case,
the mesh_sources raw-secret rejection, and the nested-list
notifications.rules placeholder case.
4) meshai/notifications/pipeline/severity_router.py: deleted.
The fork-by-severity routing it implemented was never wired in
production -- _tee in build_pipeline does the dispatcher+digest
fanout directly. The class had two test references in
tests/test_pipeline_skeleton.py that exercised "no matching rule"
and "unknown severity" paths; those guarantees are now covered by
tests/test_v052_dispatcher.py (stats counters) and the existing
Dispatcher-class tests. Removed the file, the __init__.py imports
and __all__ entries (SeverityRouter + StubDigestQueue both), the
two test methods, and the docstring mention.
Verification:
- py_compile clean on all four touched modules.
- `grep -rn SeverityRouter meshai/ tests/` returns zero.
- pytest 328 passed (was 327 at v0.5.4; net: -2 SeverityRouter tests,
+3 secret-preservation tests = +1).
- .gitignore anchor diagnosed via `git check-ignore -vn`.
Safe-mode preserved -- no toggle enabled, no master enabled, no central
enabled, no adapter feed_source flipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-time renormalization pass under the .gitattributes added in the
previous commit. Every tracked text file now uses LF. No semantic
changes — verified via git diff --cached --ignore-all-space showing
zero real differences. Future diffs will only show real content
changes.
This commit will appear huge in git log --stat but represents zero
behavior change. Use git log --follow --ignore-all-space or
git blame -w when archaeologically tracing through this commit.
- Update .gitignore for v0.3 multi-file layout
- Add config/.env.example template for secrets
- Add config/local.yaml.example for operator values
- Wire main.py to use new config_loader
- Support both legacy and new layouts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Features:
- Multi-backend LLM support (OpenAI, Anthropic, Google)
- Rolling summary memory for token optimization (~70-80% reduction)
- Per-user conversation history with SQLite persistence
- Bang commands (!help, !ping, !reset, !status, !weather)
- Meshtastic integration via serial or TCP
- Message chunking for mesh network constraints (150 char limit)
- Rate limiting to prevent network congestion
- Rich TUI configurator
- Docker support
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>