Send #rss get summaries as 3-5 separate mesh messages (v0.21.0)
CI / build + unit (pull_request) Successful in 1m20s

The single-message summary was too small (owner request). The summarizer
prompt now asks for exactly 3-5 lines, one item per line, in the format
'<short header>: <one or two short sentences>' with a <=20-char header,
no intro/outro/numbering. maxTokens stays 2500 (reasoning-token headroom).

The reply is parsed into items: drop empty lines and lines without a ':'
header separator, cap at 5; fewer than 3 valid lines sends what we got
(no failure/retry), zero valid lines sends nothing. Each item goes out as
its own mesh message via respond(id, line) - all threaded to the trigger
packet id on its channel. Per-item backstop fitOneLine() hard-cuts any
line >185 chars at a word boundary near 180 so every item fits one packet;
the old whole-reply fitOnePacket() cut is gone.

CommandService sends array replies line by line through the same routing
as single replies (respond per line, or legacy sendText fallback per line
when there is no packet id). LLM failure path unchanged: single
'summary failed' reply.
This commit is contained in:
2026-09-25 20:09:16 +00:00
parent 172e7bed9c
commit cdd52a3ddb
7 changed files with 183 additions and 75 deletions
+16 -13
View File
@@ -14,10 +14,10 @@ export interface CommandContext {
id?: number;
}
/** A command handler: returns the reply text to send back, or void/undefined for no reply. */
/** A command handler: returns the reply text(s) to send back — an array sends one message per line — or void/undefined for no reply. */
export type CommandHandler = (
ctx: CommandContext,
) => string | undefined | Promise<string | undefined>;
) => string | string[] | undefined | Promise<string | string[] | undefined>;
@Injectable()
export class CommandService {
@@ -83,17 +83,20 @@ export class CommandService {
try {
const reply = await handler({ text: trimmed, from, id });
if (reply === undefined || reply === null) return; // void = no reply
if (id !== undefined && this.mesh) {
// respond() auto-routes the reply: rides the original record's channel with replyId = id (threaded quote)
await this.mesh.respond(id, reply);
} else {
// defensive fallback when the trigger has no packet id: legacy routing on COMMAND_CHANNEL
await this.mesh?.sendText(
from ?? BROADCAST_ADDR,
reply,
undefined,
this.commandChannel,
);
// an array reply sends one message per line, all threaded to the same trigger packet id
for (const line of Array.isArray(reply) ? reply : [reply]) {
if (id !== undefined && this.mesh) {
// respond() auto-routes each line: rides the original record's channel with replyId = id (threaded quote)
await this.mesh.respond(id, line);
} else {
// defensive fallback when the trigger has no packet id: legacy routing on COMMAND_CHANNEL
await this.mesh?.sendText(
from ?? BROADCAST_ADDR,
line,
undefined,
this.commandChannel,
);
}
}
} catch (err) {
this.logger.warn(