Replies >~190 chars fragment into fire-and-forget packets recipients
often never receive; old prompt produced ~326-334 char replies.
Prompt now asks for at most 3 short bullet lines, each under 45 chars,
bullets only (verified live: exactly 3 compliant bullets, ~110 chars).
fitOnePacket() backstop cuts any over-length reply at complete-bullet
boundaries so delivery is one packet even if the model ignores the prompt.
maxTokens 200 -> 2500 on the summary chat(): empirically verified that
this server's reasoning tokens count against max_tokens (cap 150 ->
completion 150 of which 105 were reasoning; cap 500 fully consumed by
reasoning, zero visible content) and thinking is uncontrollable
server-side (/no_think, chat_template_kwargs.enable_thinking,
thinking_budget all ignored), observed 32..1776 tokens for this task.
2500 covers every observed run + ~80 content tokens, worst case ~42 s
under the 60 s LLM timeout.
#rss subcommands on the command channel: help / add <url> <name> /
remove <name> / tags / sources / get <name|#tag>. Sources are stored in
a new rss_sources table in the existing MESH_DB_PATH file (second
node:sqlite connection, case-insensitive names); feeds parsed with
fast-xml-parser (RSS 2.0 + Atom); AI tags and headline summaries via
injected LlmService.chat() with bounded maxTokens (50/200).
CommandService gains prefix registration so one handler owns the whole
#rss family while #ping stays an exact match; wired in CommandModule's
factory — still no constructor-level cycle, no forwardRef. One dead feed
does not sink a tag summary.
LlmService wraps the local OpenAI-compatible server (default
http://192.168.1.70:1111/v1, no auth) with native fetch only — no new
dependencies. listModels() -> data[].id; chat(messages, opts?) ->
choices[0].message.content (non-2xx throws with status + body snippet);
summarize(text) = chat() with a concise-summary system prompt.
Env: LLM_API_BASE / LLM_MODEL (qwen3.8-27b) / LLM_TIMEOUT_MS (60000,
AbortSignal.timeout). Plain feature module wired into app.module.ts;
docs synced (AGENTS.md, README env list).
Event-driven CommandService dispatches exact case-sensitive #command
matches on COMMAND_CHANNEL (default 2) through a single-registration-point
registry; first command #ping -> #pong!. Replies ride COMMAND_CHANNEL:
directed DM back to the sender when present, else broadcast. Failing
handler/send only logs a warn.
DI: CommandModule's factory injects MeshService and attaches itself via
MeshService.attachCommands() — a constructor-level cycle leaves one side
null in NestJS 12's injector (verified), so no forwardRef is used.
Tests 106 -> 121; docs synced (AGENTS.md, MESHTASTIC.md, README).