Cap #rss get summaries to one mesh packet (v0.20.1)
CI / build + unit (pull_request) Successful in 42s
CI / build + unit (pull_request) Successful in 42s
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.
This commit is contained in:
@@ -5,7 +5,7 @@ import { DatabaseSync } from 'node:sqlite';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { LlmService } from '../llm/llm.service.js';
|
||||
import { RssStore } from './rss.store.js';
|
||||
import { RssService } from './rss.service.js';
|
||||
import { fitOnePacket, RssService } from './rss.service.js';
|
||||
|
||||
const FEED_A = `<rss version="2.0"><channel>
|
||||
<item><title>Alpha headline</title></item>
|
||||
@@ -158,9 +158,31 @@ describe('RssService.handle', () => {
|
||||
|
||||
expect(reply).toBe('Alpha and beta happened.');
|
||||
const [messages, opts] = llm.chat.mock.calls[0];
|
||||
expect(opts).toEqual({ maxTokens: 200 });
|
||||
expect(messages[0].content).toContain('Alpha headline');
|
||||
expect(messages[0].content).toContain('Beta headline');
|
||||
// headroom for the server's reasoning tokens (they count against max_tokens) — see rss.service.ts
|
||||
expect(opts).toEqual({ maxTokens: 2500 });
|
||||
const prompt = messages[0].content;
|
||||
expect(prompt).toContain('at most 3 short bullet lines');
|
||||
expect(prompt).toContain('each under 45 characters');
|
||||
expect(prompt).toContain('no intro, no other text');
|
||||
expect(prompt).toContain('Alpha headline');
|
||||
expect(prompt).toContain('Beta headline');
|
||||
});
|
||||
|
||||
it('get <name>: cuts an over-length LLM reply at bullet boundaries so it fits one packet', async () => {
|
||||
stored().add({ name: 'myfeed', url: 'https://a.example/rss', tags: [] });
|
||||
fetchMock.mockResolvedValueOnce(feedResponse(FEED_A));
|
||||
// 5 bullets × 46 chars = 234 joined — over the 190-char one-packet cap
|
||||
const tooLong = ['a', 'b', 'c', 'd', 'e']
|
||||
.map((c) => `-${c}`.padEnd(46, c))
|
||||
.join('\n');
|
||||
llm.chat.mockResolvedValueOnce(tooLong);
|
||||
|
||||
const reply = await svc.handle(ctx('#rss get myfeed'));
|
||||
|
||||
expect(reply.length).toBeLessThanOrEqual(190);
|
||||
// the first 4 bullets fit (47*4-1=187), the 5th does not — and only complete lines survive
|
||||
expect(reply.split('\n')).toHaveLength(4);
|
||||
for (const line of reply.split('\n')) expect(line.length).toBe(46);
|
||||
});
|
||||
|
||||
it('get #tag: collects items across all sources with the tag; one dead feed does not sink it', async () => {
|
||||
@@ -204,3 +226,26 @@ describe('RssService.handle', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fitOnePacket (one mesh packet ≤ 190 chars)', () => {
|
||||
it('passes through replies that already fit, including exactly 190 chars', () => {
|
||||
const short = '- a\n- b';
|
||||
expect(fitOnePacket(short)).toBe(short);
|
||||
const exact = 'x'.repeat(190);
|
||||
expect(fitOnePacket(exact)).toBe(exact);
|
||||
});
|
||||
|
||||
it('keeps complete bullet lines that fit and drops the rest', () => {
|
||||
// 46-char bullets: 4 fit (47*4-1=187), 5 do not (234)
|
||||
const five = ['a', 'b', 'c', 'd', 'e']
|
||||
.map((c) => `-${c}`.padEnd(46, c))
|
||||
.join('\n');
|
||||
expect(fitOnePacket(five).split('\n')).toHaveLength(4);
|
||||
expect(fitOnePacket(five).length).toBeLessThanOrEqual(190);
|
||||
});
|
||||
|
||||
it('hard-cuts a non-bulleted over-length paragraph rather than replying empty', () => {
|
||||
const para = 'w'.repeat(250);
|
||||
expect(fitOnePacket(para)).toBe('w'.repeat(190));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user