- Create subscriptions.py with SubscriptionManager class for SQLite storage
- Add subscribe.py commands: !sub, !unsub, !mysubs with aliases
- Update dispatcher.py to register subscription commands
- Modify main.py with scheduler tick (60s) and _check_scheduled_subs()
- Add build_node_compact() and build_region_compact() to mesh_reporter.py
- Support daily, weekly, and alerts subscription types
- Support mesh, region, and node scope filtering
- 5-minute matching window for schedule tolerance
- Dedup via last_sent tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Removed SNR-required filter from !neighbors command
- Show all infra neighbors, add signal quality when available
- Enrich edges with SNR from traceroute snrTowards/snrBack data
- Fallback: use node-level SNR for edges without traceroute data
- Sort by SNR when available, alphabetically otherwise
Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: health engine keyed nodes by database row IDs instead of
Meshtastic node numbers. Packets and telemetry could never match.
Fixed:
- Store _node_num on all normalized nodes (mesh_sources.py)
- Key health engine node dict by _node_num (mesh_health.py)
- Fix packet field names: from_node not from/fromId
- Fix telemetry parsing: handle telemetryType/value structure
- Increase packet/telemetry fetch limits for 24h coverage
- Fix utilization formula to compute actual airtime percentage
- router.py: Fixed region scope detection to match longest region name first
- router.py: Added region abbreviations (SCID, SWID, etc.) for quick matching
- router.py: Added city name mapping (Boise -> South Western ID, etc.)
- router.py: Fixed node longname matching (case-insensitive substring)
- router.py: Added follow-up message context tracking (_user_mesh_context)
- router.py: Added more mesh keywords (noisy, traffic, packets, etc.)
- mesh_reporter.py: Added disambiguation for duplicate shortnames in region detail
- mesh_health.py: Added util_data_available flag to track packet data presence
- mesh_health.py: Passes has_packet_data through score computation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add _normalize_node() function to mesh_sources.py that standardizes:
- Role field: Map integer enums to string names using correct Meshtastic
protobuf values (0=CLIENT, 1=CLIENT_MUTE, 2=ROUTER, 3=ROUTER_CLIENT,
11=ROUTER_LATE, etc.). Now detects 18 infrastructure nodes.
- GPS fields: Check latitude/longitude, then last_lat/last_long (Meshview
scaled integers), then lat/lon. Filter out invalid 0,0 coordinates.
Now 238 nodes with GPS (was 201).
- Timestamps: Normalize to last_heard as epoch seconds. Handle
microseconds (last_seen_us), milliseconds, and seconds formats.
Now 527 nodes with timestamps (was 0).
- Hardware model: Prefer string hw_model over integer hwModel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New files:
- mesh_reporter.py: MeshReporter class for prompt injection
- build_tier1_summary(): ~500-800 token mesh health summary
- build_region_detail(): Detailed region breakdown
- build_node_detail(): Single node info with recommendations
- build_recommendations(): Optimization suggestions
- build_lora_compact(): Short format for LoRa messages
- list_regions_compact(): Region list with scores
- commands/health.py: !health and !region commands
- !health: Quick mesh summary (no LLM)
- !region [name]: Region info or list all regions
Modified files:
- router.py: Mesh question detection and prompt injection
- _is_mesh_question(): Keyword/phrase matching
- _detect_mesh_scope(): Node/region/mesh scope detection
- Inject Tier 1/2 data for mesh questions
- Add mesh awareness instructions to LLM
- main.py: Create MeshReporter, pass to dispatcher/router
- commands/dispatcher.py: Register health/region commands
- mesh_health.py: Fix role type (int -> str)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add _extract_node_num() helper to normalize node IDs across formats
- Add _normalize_edge_key() helper for undirected edge deduplication
- Update get_all_nodes() to deduplicate by nodeNum with _sources field
- Update get_all_edges() to deduplicate by sorted (from, to) tuple
- Update get_all_telemetry() to deduplicate by (nodeNum, timestamp)
- Add get_dedup_stats() method for raw vs dedup counts
- Add get_stats_by_source() method for per-source statistics
- Change _source field to _sources (list) for multi-source tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10 regions covering Idaho and Utah:
- Northern ID (Coeur d alene)
- Central ID (Salmon)
- South Western ID (Boise)
- South Central ID (Twin Falls)
- South Eastern ID (Idaho Falls)
- Northern UT (Ogden)
- Central UT (Salt Lake City)
- Eastern UT (Vernal)
- Western UT (Tooele)
- Southern UT (St. George)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Nodes without GPS can be assigned to a region based on their
neighbors. Uses edge data to build neighbor graph, then assigns
unlocated nodes to the most common region among their neighbors.
Iterates until no more assignments possible.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Regions are now user-defined anchor points (name + lat/lon)
- Nodes assigned to nearest region, no distance limits
- Removed auto-naming and region_labels/infra_overrides
- Added Idaho region defaults in TUI
- Simpler, deterministic, user-controlled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- last_lat/last_long are scaled integers (divide by 1e7)
- last_seen_us is in microseconds (divide by 1e6)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Meshview API returns {"nodes": [...]} and {"edges": [...]} wrapper
dicts, not raw lists. Added _extract_list() helper to unwrap.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds MeshAI/x.x User-Agent for good API citizenship and easier
traffic identification by Meshview/MeshMonitor operators.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements mesh intelligence with geo clustering, four-pillar health scoring,
and auto-naming regions from GPS data.
New: geo.py, mesh_health.py
Modified: config.py, main.py, router.py, configurator.py, config.example.yaml
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MeshviewSource class for fetching nodes, edges, stats from Meshview API
- Add MeshMonitorDataSource class for fetching nodes, channels, telemetry,
traceroutes, network stats, topology, packets, solar from MeshMonitor API
- Add MeshSourceManager for managing multiple sources with aggregation
- Add MeshSourceConfig dataclass and mesh_sources list to config
- Integrate source_manager into main.py with periodic refresh
- Add source_manager parameter to MessageRouter (for future Phase 3)
- Add Mesh Sources TUI menu with add/edit/remove/test functionality
- Update config.example.yaml with mesh_sources section
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MeshMonitor Integration section with features and configuration
- Add credit line in Features list
- Link to github.com/Yeraze/meshmonitor
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Build system prompt dynamically using bot.name and bot.owner from config
- Reorder prompt: identity -> static prompt -> MeshMonitor (conditional) -> mesh context
- MeshMonitor description only injected when meshmonitor.enabled is true
- Update default system_prompt to static parts only (commands, architecture, rules)
- Fix meshmonitor.py to handle trigger arrays (not just strings)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MeshMonitor Sync as main menu option 9
- Toggle enabled/disabled, triggers file path, inject_into_prompt
- View Triggers shows loaded patterns from triggers.json
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add MeshMonitorSync class that reads trigger patterns from a JSON file
and compiles them to regex. The router checks incoming messages against
these patterns and ignores messages that MeshMonitor will handle.
- New meshai/meshmonitor.py: Pattern compilation and file watching
- MeshMonitorConfig dataclass with enabled, triggers_file, inject_into_prompt
- Router integration: ignore matching messages, inject commands into prompt
- Main loop refresh: watch triggers file for changes without restart
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
All three LLM backends (Google, OpenAI, Anthropic) now wrap API calls
in asyncio.wait_for() using config.timeout (default 30s). Previously
Gemini could hang indefinitely with grounding+AFC enabled.
Router catches TimeoutError with user-friendly "request timed out" message.
Empty context buffer now injects "[No recent mesh traffic observed yet.]"
so the LLM knows the capability exists even when buffer is empty.
Default system prompt updated to mention mesh awareness.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Unknown !commands (like !mail, !login, !bbs from advBBS users) were
getting "Unknown command, try !help" responses. Now returns None so
the bot stays silent on commands it doesn't recognize.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New context.py module: ring buffer (50K hard cap, ~25MB ceiling) passively
records all channel broadcasts. Observations are formatted with relative
timestamps and injected into the system prompt when generating LLM responses.
Only public channel traffic is observed; DMs to the bot are excluded (already
in per-user history). Bot's own node ID is auto-added to ignore list.
Config: context.enabled, observe_channels, ignore_nodes, max_age, max_context_items
TUI: new Context settings submenu (menu item 7)
Hourly prune removes expired observations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MeshAI is now DM-only. Removed all unreachable channel response
paths, @mention detection, ChannelsConfig, and channel TUI menu.
Fixed restart mechanism with integrated watcher and SIGKILL fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mesh messages are short enough that summarization is unnecessary
with Gemini Flash's 1M token context window.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete unused CommandResult class from commands/base.py and unused
format_dm_response method from responder.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Removed dead menu items that referenced deleted config sections.
Renumbered menu options. Added BBS filter toggle to bot settings.
Added missing _handle_exit method.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FallbackBackend was never configured in practice. generate_with_search
was an unused abstract method on all backends.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These modules were wired up but never actually functional in the
running bot. Strips all imports and usage from main.py and router.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
These were LLM-generated planning artifacts from the memory
implementation phase. Not user-facing documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 7a: Config.get_system_prompt() now logs a deprecation warning.
PersonalityManager.get_system_prompt() is the canonical source (wired
in commit 4). LLMConfig.system_prompt kept for backwards compat as
fallback when personality is not configured.
- 7b: Fix AnnouncementScheduler callback type from asyncio.coroutine
(a decorator, not a type) to Awaitable[None] from typing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 6a: Change healthcheck in Dockerfile and docker-compose.yml to verify
the PID file exists and the process is alive (kill -0) instead of
testing SQLite connectivity, which only proves the DB file exists
- 6b: In DMs, skip !commands so MeshMonitor or other bots handle them.
MeshAI only responds conversationally in DMs (no bang commands)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 5a: Import html.escape and apply to all values rendered into the
HTML template in _serve_status_page() — uptime, counts, status text,
node counts, errors. Prevents XSS via crafted node names or errors.
- 5b: Add basic prompt injection detection to _clean_query() with
configurable safety.prompt_injection_guard (default: on). Detects
patterns like "ignore all previous", "you are now", "system prompt:",
etc. Truncates query before the injection phrase and logs a warning.
Not foolproof but better than nothing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 2a: SafetyFilter + UserFilter — check user access before processing,
filter LLM responses through SafetyFilter before sending
- 2b: RateLimiter — check rate limits before processing, record
messages after successful response delivery
- 2c: PersonalityManager — pass to MessageRouter, used for system
prompt generation instead of raw config.llm.system_prompt
- 2d: WebhookClient — start/stop in lifecycle, fire events on
message_received, response_sent, error, startup, shutdown
- 2e: WebStatusServer — start/stop in lifecycle, record messages,
responses, and errors in StatusData
- 2f: AnnouncementScheduler — start/stop in lifecycle, uses
connector.send_message as callback
- 2g: FallbackBackend — wrap primary backend when config.llm.fallback
is configured, otherwise use primary directly
- 2h: CommandDispatcher — pass prefix, disabled_commands, and
custom_commands from config to create_dispatcher()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
RollingSummaryMemory now accepts a generic async summarize callable
instead of hardcoding AsyncOpenAI. Each backend provides its own
_summarize_messages() method that uses the appropriate API client.
- Removed AnthropicMemory class from anthropic_backend.py
- Removed GoogleMemory class from google_backend.py
- Changed RollingSummaryMemory.__init__ signature to accept
summarize_fn: Callable[[list[dict]], Awaitable[str]]
- All three backends (OpenAI, Anthropic, Google) now instantiate
the same RollingSummaryMemory class with backend-specific callables
- Extracted shared summarize prompt to module-level _SUMMARIZE_PROMPT
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 4a: Add threading.Lock to MeshConnector protecting _node_names and
_node_positions dicts that are read/written from Meshtastic's pubsub
thread callbacks (_on_receive, _on_node_update, _cache_node_info)
and read from async code (get_node_position, get_node_name)
- 4b: Add threading.Lock to StatusData protecting counters and activity
list that are written from the async event loop and read from the
HTTP server thread in to_dict()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>