Skip to content

AIDA via Open WebUI (aida-mesh), #aida mentions, thinking notice, runaway guard - #173

Open
zvx-echo6 wants to merge 6 commits into
mainfrom
aurora-openwebui-backend
Open

zvx-echo6 wants to merge 6 commits into
mainfrom
aurora-openwebui-backend

Conversation

@zvx-echo6

Copy link
Copy Markdown
Owner

Summary

  • Adds an OpenAI-compatible backend path (openai_backend.py) so meshai can talk to Open WebUI, routing to the aida-mesh model.
  • Adds #aida mention detection, a "thinking" notice while a response is generated, and a runaway-response guard (truncation/sources-line handling) in the router.
  • Adds dashboard Config UI controls for the new mention/thinking-notice/backend settings.
  • Extensive new test coverage: mention detection, channel history key separation, channel reply prefix/threading, MeshCore channel sender parsing, OpenAI backend, router LLM truncation, router sources line, router thinking notice, and channel-mention response gating.

This is the branch backing the meshai → AIDA (Open WebUI) cutover. Opening this PR only to get CI signal on the branch (workflow only triggers on push/PR to main); not merging.

🤖 Generated with Claude Code

zvx-echo6 and others added 6 commits September 24, 2026 01:47
…d config UI

- openai_backend: strip think/citation tags; treat truncated finish_reason
  or empty completion as LLMTruncatedError
- router: channel @-mentions (respond_to_channel_mentions, mention_channels,
  mention_names, incl. continuations), history_key separation from the
  reply-thread key, per-sender/channel mention cooldown, thinking-notice
  dispatch, LLM_TRUNCATED_TEXT fallback, Sources-line stripped but kept in
  history, continuation budget fix
- config: BotConfig mt_node/mt_mesh_name/mc_mesh_name identity fields +
  respond_to_channel_mentions/mention_channels/mention_names/
  channel_reply_cooldown_seconds; ResponseConfig thinking_notice_seconds/
  thinking_notice_text; MeshCoreContextConfig.mention_channels;
  parse_mt_node_id/parse_mt_node_num helpers
- transport/connector/mesh_reporter/chunker/responder: reply_id plumbing,
  MeshCore sender parsing, channel_name propagation
- dashboard: Config.tsx now exposes all of the above plus a new MeshCore
  Context section (previously configurable only via raw YAML)
- tests: new coverage for mention detection, channel reply threading,
  history-key separation, LLM truncation handling, thinking notice, and
  MeshCore channel-sender parsing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When someone sends !addme (case-insensitive, trailing text ok) in a
configured MeshCore channel (default #aida), AIDA sends a flood
self-advert (rate-limited), resolves the sender's pubkey via its own
contact list then CoreScope (live.mwmesh.com), adds them as a contact
(signed CMD 0x12 import when CoreScope has a signature-valid advert,
falling back to the existing unsigned CMD 0x09 import_contact), and
DMs them after a delay so the advert has time to propagate.

Detection lives directly in MeshCoreTransport._on_channel_event
(meshai/meshcore_addme.py), independent of respond_to_channel_mentions
and observe_channels, so it works even when those are off. The
registered !addme CommandHandler (meshai/commands/addme.py) exists only
for !help visibility and is a deliberate no-op everywhere else (does
nothing on Meshtastic or a MeshCore DM).

New MeshCoreContextConfig fields (addme_enabled, addme_channels,
addme_advert_cooldown_seconds, addme_dm_delay_seconds,
addme_per_user_cooldown_seconds, addme_dm_text), a new
addme_provenance audit table (migration v32), and matching dashboard
Config.tsx controls under MeshCore Context.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ddme DM

The !addme DM text (185 bytes formatted) plus the meshcore lib's 13-byte
DM header (cmd+type+attempt+timestamp+6-byte dst prefix) produced a
~194-byte CMD_SEND_TXT_MSG frame, over openhop's companion hard cap of
MAX_FRAME_SIZE=176 bytes. The companion logs "Frame too long: 194" and
disconnects the client, dropping meshai's entire MeshCore link for the
~2.5 min idle-reap window -- not just the one !addme reply.

- transport/meshcore_transport.py: add split_text_to_frame_limit() and
  MESHCORE_DM_MAX_TEXT_BYTES (153) / MESHCORE_CHANNEL_MAX_TEXT_BYTES (159),
  derived from openhop's MAX_FRAME_SIZE=176 minus the meshcore lib's actual
  per-kind header (13 bytes DM/room, 7 bytes channel) minus a 10-byte
  safety margin. send_message_async() -- the single choke point for every
  MeshCore text send (DM, room, channel broadcast, and LLM replies via
  responder.py/chunker.py) -- now measures UTF-8 bytes and splits at
  sentence/word boundaries (never mid-codepoint) before any frame reaches
  the wire, instead of letting an oversized one take the link down.
- meshcore_addme.py / config.py: shorten DEFAULT_ADDME_DM_TEXT and the
  matching addme_dm_text config default so a typical formatted message
  (even a 20-char emoji name) clears the DM budget in one frame.
- Logging: _send_dm_once_async and _do_mc_broadcast_async now log
  repr(exc) instead of str(exc) -- a bare asyncio.TimeoutError's str() is
  "", which read as a blank, silent failure line. meshcore_addme's
  _delayed_dm now logs "send failed (<reason>)" for a raised exception,
  distinct from "sent, no ACK" for a clean best-effort return.
- Tests: split_text_to_frame_limit (ASCII, exact boundary, multibyte/emoji
  near the limit, a single glued word forcing a hard cut, the addme
  default text with a long emoji name), send_message_async's DM/channel
  guard wiring through the real send path, and the addme DM text
  assertion updated to the new wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Live response.thinking_notice_seconds was set to 30 via the dashboard
API hot-apply (response is not in RESTART_REQUIRED_SECTIONS and the
router re-reads self.config.response fresh per message, so no restart
was needed). Bring the ResponseConfig dataclass default in line and
update the stale doc comment in the thinking-notice test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three fixes for the AIDA triple-DM incident (Matt's MeshCore "Thanks hows
it going?"), plus the mention-name gap it surfaced:

1. Inbound de-dupe (meshai/dedupe.py, InboundDedupeCache): drops a
   client's automatic resend of a DM/channel-mention message (same
   sender/text/sender_timestamp, or same Meshtastic packet id) before it
   reaches should_respond()'s rest, route(), the LLM, or history a second
   time. Bounded (500 entries), TTL 10 min for id-keyed entries, 90s
   fallback window when no stable id is available.

2. Mention matching: the mention matcher now also accepts AIDA's own live
   MeshCore self-name (read fresh from self_info() every check) and
   bot.mc_mesh_name/mt_mesh_name, so "@[AIDA-MC]" (what MeshCore apps
   insert when tap-mentioning a device named "AIDA-MC") is recognized
   alongside "@aida"/"@[AIDA]". The mention regex's end-of-token boundary
   was widened to also treat '-'/'_' as identifier characters, so a
   hyphenated name must match in full ("@AIDA-MC" matches, "@AIDA-MCX"
   does not).

3. meshcore_context hot-apply: PUT /api/config/meshcore_context now calls
   the transport's (previously dead) set_context_config(), fixing the
   stale-reference bug where MeshCoreTransport._mc_context kept enforcing
   boot-time respond_to_dms/ignore_contacts/observe_channels/addme_* until
   a restart, even though the section isn't restart-required. bot.*
   mention/channel settings were already reading the live shared Config
   object and needed no fix.

Root-cause fix for the actual triple-DM (outbound, not inbound): two
independent retry state machines were stacked -- meshai's own no-ACK
path-discovery-and-resend AND MeshMonitor's virtual node's own ACK
tracker/RF retries. meshai now sends each DM exactly once by default
(ConnectionConfig.meshcore_client_retry, default False) and only logs
whether the ACK arrived; meshcore_ack_wait_seconds default raised 6->30s
to clear MeshMonitor's own retry window. meshcore_client_retry=True
restores the old resend-on-no-ACK behavior for a bare MeshCore link with
no external retry layer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diagnosis from Matt's MeshCore chat (13:08-13:23 UTC): a reply whose whole
body was a stripped "Sources:" line sent an empty DM (chunk_response("")
-> [""]); a separate reply truncated at the 1024-token cap slipped through
because Open WebUI didn't surface finish_reason=="length".

- Responder.send_response drops empty/whitespace-only chunks before
  transmitting; if nothing is left, it logs a WARNING and sends nothing.
- router.py: when the body is empty after _strip_sources_line, send the
  citations as the visible answer ("Sources: X; Y" -> "Source: X; Y")
  instead of blank text; if there's no usable citation content either,
  fall back to LLM_TRUNCATED_TEXT like any other failed generation.
- New LLMConfig.require_sources_line (default False): when enabled, a
  reply missing its required trailing "Sources:" line is treated as
  truncated (LLMTruncatedError path) rather than relayed as-is.
- openai_backend.generate() now also treats usage.completion_tokens at or
  above 98% of max_tokens as a truncation signal, even when finish_reason
  isn't "length", and logs which signal fired.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant