• ctrl/chat_llm.ini exec/ch

    From Rob Swindell (on Windows@VERT to Git commit to main/sbbs/m on Fri May 29 15:31:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f5f55249bd04c9133041aee7
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm_irc.js
    Log Message:
    chat_llm: fix stock chat_llm.ini doc drift + wire opening system prompt

    Cleanups to the config shipped in 39502cc1f:

    - Wire opening_system_prompt_file = chat_llm_opening_persona.utf8 so the
    bundled opening-turn persona is actually used; it shipped but was never
    referenced, so the opening greeting fell back to the full system prompt.
    Matches the working live config.
    - Drop textdir/sourcecode/gitlab/git_log from the index_sources comment;
    only msgbase/filebase/dokuwiki crawlers ship in exec/llm_index/.
    - Replace the stale "v0 sketch -- no RAG, no persistent memory yet" header;
    both RAG and persistent memory now exist.
    - chat_llm_irc.js: header/inline comments said irc_channel; the code reads
    irc_channels (comma-separated list of channels).

    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri May 29 23:59:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/3ef83bcbfc9ed41607b63774
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js
    Log Message:
    chat_llm: add configurable num_ctx (Ollama context window)

    Adds a num_ctx knob (read from chat_llm.ini, per-persona or [default])
    passed through to Ollama's request options on all three /api/chat
    builds (initial, pre-tool, follow-up). 0/unset => omitted via
    `cfg.num_ctx || undefined` (JSON.stringify drops an undefined value),
    so Ollama keeps its server default and existing installs are unaffected.

    Why: a large system prompt plus retrieved RAG context can exceed
    Ollama's ~4096-token default; llama.cpp then keeps the prompt's tail and truncates the FRONT, dropping the identity/style rules at the top of the
    system prompt and making the bot answer as a generic "AI assistant".
    Observed with a 4698-token persona prompt capped at 4096. Documents the
    knob (commented, off by default) in the stock chat_llm.ini.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Fri May 29 23:59:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/143338a4deabfd87458fee78
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js
    Log Message:
    chat_llm: endpoint failover (endpoint_fallback)

    Add an optional endpoint_fallback. dispatch() now wraps the provider
    call: when the primary endpoint throws (transport error / non-200), it
    retries once against the fallback, and a ~60s cooldown routes straight
    to the fallback on subsequent turns so an outage doesn't add the
    primary's full timeout to every reply (re-probes the primary after the cooldown). No fallback configured => unchanged behavior. Documents the
    knob (commented) in the stock chat_llm.ini.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun May 31 19:06:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/f4630b0a17237b8f8ef08728
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js chat_llm_irc.js llm_index.js exec/llm_tools/relay_message.js
    Log Message:
    chat_llm: per-name data/chat subdirectory layout

    Move per-persona chat_llm files out of the flat data/chat/ namespace into per-guru subdirectories data/chat/<name>/ (name = persona code before ':'), eliminating filename-prefix collisions and the safe_id ':'<->'_' ambiguity (guru:irc vs guru_irc) the flat scheme allowed.

    data/chat/<name>/ holds:
    - llm_rag.idx RAG index, shared across the guru's modes
    (IRC bot + terminal guru)
    - irc_relay.json, irc_mute.json, irc_norelay.json, irc_seen.json
    IRC state, proto-prefixed
    - <speaker>[.<proto>].json per-speaker memory; the proto tag keeps a
    speaker's terminal vs IRC memory from colliding Naming: '_' joins fixed name-parts, '.' separates the variable proto/ext.

    - chat_llm.js: persona_parts()/persona_dir() helpers; memory_path() ->
    data/chat/<name>/<speaker>[.<proto>].json; index_output default ->
    chat/<persona>/llm_rag.idx. User-numbered speaker ids zero-padded to
    %04u (user_0001) to match Synchronet's data/user/0001.* convention.
    - llm_index.js: writes chat/<name>/llm_rag.idx, keyed on the NAME part so
    `llm_index.js guru` and `... guru:irc` agree with load_index().
    - chat_llm_irc.js: PERSONA_NAME/PROTO/DIR; IRC state under PERSONA_DIR;
    chat log + .announce stay in data root (short hand-touch paths).
    - relay_message.js: off-session defaults under data/chat/guru/; the
    recipient history-scan now reads the persona dir (from env.relay_path).
    - ctrl/chat_llm.ini: index_output = chat/<persona>/llm_rag.idx.

    Live data migrated into the new layout (33 files; index moved by rename,
    no rebuild). Validated: path resolution, full regression unit suite, and end-to-end relay opt-out honored from the new path.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Sun May 31 19:06:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/348e8ae9f0468c124b827d82
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js chat_llm_irc.js chat_llm_routes.js exec/llm_tools/relay_message.js
    Log Message:
    chat_llm: faithful relays -- verbatim routing, perspective rewrite, join delivery

    Rework message-relay so a relayed line reaches the recipient as intended
    rather than paraphrased or pronoun-inverted:

    - Broaden the deterministic relay router (chat_llm_routes.js) to catch many
    more phrasings -- please/can-you preambles, and ask / remind / leave a
    message for / send / give / message / say-hi-to / if-you-see / when-X-is-
    back -- so the body is stored VERBATIM instead of falling through to the
    model, which rewrote it (3rd person, added preamble, once a whole poem).
    Pronoun/article guards keep "let me know", "say hi to everyone", etc. from
    false-matching as relays.

    - Perspective rewrite: a relay phrased from the sender's seat ("tell Dan you
    love his work") is re-anchored to the recipient's point of view ("I love
    your work") before it's stored. Gated on pronoun presence -- pronoun-free
    bodies ("the build is fixed") stay byte-verbatim with NO model call.
    relay_rewrite_model points this small, infrequent call at a stronger model
    (subtle reference task a 7B does unreliably), kept transient via keep_alive
    0 so the faster chat model stays the resident, warm one on a host that
    can't hold both in VRAM.

    - Deliver pending relays on JOIN too, not just on speech: a relay request
    says "next time you SEE X", and re-joining counts. deliver_pending() drains
    the queue, so this is idempotent with the speak-triggered path.

    - Delivery line quotes the relayed body; acknowledgement wording is now
    "joins or speaks". Bot log timestamps switch to local-time ISO-8601 via
    strftime() instead of UTC Date.toISOString().

    New ctrl/chat_llm.ini keys (documented in the stock file): relay_rewrite, relay_rewrite_model, relay_rewrite_keep_alive.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Thu Jun 11 15:38:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/135f08f0f5e1146d28b3c9ed
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js chat_llm_irc.js
    Log Message:
    chat_llm: stop the IRC guru volunteering wrong/unsolicited answers

    Two changes to make the guru a far more reluctant, more accurate
    unprompted participant on IRC, prompted by live #synchronet logs where
    it (a) answered questions one user had explicitly aimed at another, and
    (b) confidently restated others' mistakes as fact.

    1. directed_at_other() (chat_llm_irc.js): a question addressed to a
    specific other participant ("nelgin: why are you changing ip?") is a
    person-to-person exchange, not a room question -- the bot no longer
    arms an intervention on it. is_nick_ping only caught a BARE "Nick?";
    this catches "Nick: <content>" / "Nick, <content>" where Nick is a
    seen channel member that isn't the bot.

    2. Two-layer confidence gate for unprompted interventions only (direct
    questions are answered best-effort as before):
    - In-prompt SKIP escape: the volunteering generation is told to answer
    only if the retrieved docs explicitly support a correct answer and
    to never restate an asker's assumption as fact; otherwise it emits
    SKIP and the bot stays silent (and persists nothing). No extra
    round-trip. (ctx.volunteering -> build_messages; is_volunteer_abstain
    nulls the reply in chat_session.)
    - 14B fact-check (verify_volunteer_answer): a 7B feels "grounded" off a
    merely-related chunk and confirms false premises anyway (observed: a
    bogus mods/exec load path off a chunk about mods shadowing exec). Any
    non-declined answer is fact-checked by a stronger model that must
    return VERIFIED; fails closed (any error/ambiguity -> silence). Reuses
    relay_rewrite_model, kept transient (keep_alive 0) so the warm chat
    model isn't evicted. New ini knobs: intervention_verify[_model|_prompt].

    Also adds an anti-sycophancy clause to the IRC grounding for all replies:
    don't agree with or repeat a user's claim unless the docs support it.

    Live-validated: the false-premise "mods/exec" question now abstains
    (3/3), an ungroundable question abstains, and legitimate groundable
    questions (SBBSecho, transfer protocols, BinkP) still answer.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Mon Aug 10 03:17:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/17846dff73b55f7d3303f588
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js
    Log Message:
    chat_llm: stop discarding known-good URLs and repeating whole replies

    Three defects in the reply pipeline, all visible in a single 2026-08-09 #synchronet exchange where the bot cited a wiki page correctly, then
    answered "what were those links again?" four times running with the
    link silently deleted from every reply.

    strip_fake_urls() validated citations against ctx._valid_wiki_urls,
    which is rebuilt each turn from that turn's BM25 hits alone. A URL the
    bot had already cited was therefore treated as fabricated the moment
    retrieval moved on -- and a back-reference question ("what was that
    link again?") carries no content words, so it retrieves unrelated pages
    by construction and could never re-validate the link it was asking
    about. URLs appearing verbatim in the transcript the model was handed
    now count as valid: quoting itself is not fabrication. The transcript
    holds post-strip text, so a stripped URL never enters it and cannot
    launder itself into the allowlist, and memory is per-speaker, so one
    user's transcript cannot validate another's citations.

    "Cite this URL verbatim:" is RAG scaffolding prepended to wiki chunks
    to make their URLs visible to the model. qwen2.5:7b echoes it back,
    and with the adjacent URL stripped the bare instruction was all that
    reached the channel. It is now removed from replies unconditionally --
    it is an instruction to the model and has no business in output.

    A cornered small model also re-emits its previous answer nearly
    verbatim; two consecutive replies to the same user were token-identical,
    and the same closing boilerplate ended three of six replies in a row.
    Raising temperature (the earlier mitigation) varies the wording without noticing the repetition. Each finished reply is now compared against
    that speaker's recent bot turns by word-set overlap; on a match the bot regenerates once with an explicit don't-repeat instruction, and if that
    is still a duplicate it answers from the guru file instead -- the
    pre-LLM Guru's keyword-matched responses, so a bot with nothing to add
    deflects in character rather than repeating itself. A second
    deflection in a row stays silent.

    The 0.75 default is word-set Jaccard, calibrated against real repeats
    in data/guru_irc_chat.log: observed duplicates scored 0.83-1.00,
    genuinely-new answers on the same topic 0.42-0.62. Replies under 12
    words are exempt, since "ok" and "yep, that works" are legitimately
    repeatable. Explicit requests to repeat bypass dedup entirely, and it
    is skipped in streaming (private 1:1) mode, where the first attempt is
    already on the user's screen and swapping the stored string would leave
    them reading one answer while the log records another.

    The guru file parser follows sbbs_t::guruchat()/guruexp() in src/sbbs3/chat.cpp, with two deliberate divergences. Sections carrying
    an access-requirement test are skipped: an IRC nick has no user record
    to evaluate SEX/FLAG/LEVEL against, and guessing would put the stock
    file's gendered variants in front of the wrong people. Chat-control
    codes are stripped rather than obeyed, since `q ends the session and `h
    hangs up, neither of which a bot may do to a channel. Dropped codes
    leave a space, because the file uses `_ pauses between words for timing
    ("write to`_`_Rush Limbaugh") where deleting them outright runs the
    words together.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net
  • From Rob Swindell (on Debian L@VERT to Git commit to main/sbbs/m on Mon Aug 10 03:17:00 2026
    https://gitlab.synchro.net/main/sbbs/-/commit/2ff8d37d997adb52be2b332c
    Modified Files:
    ctrl/chat_llm.ini exec/chat_llm.js llm_index.js exec/llm_index/dokuwiki.js
    Log Message:
    chat_llm: index wiki pages per section and inject the matching passage

    Retrieval was selecting the right page and the injector was discarding
    the part that answered the question. Wiki pages were indexed as one
    document each and then cut to their first 800 characters for the
    prompt. On one live corpus, 384 of 541 wiki pages (71%) were longer
    than that, mean page length 4932 characters -- so the model typically
    saw the first sixth of a page and nothing else.

    The failure this produced is worth spelling out, because it looks like
    a model problem and is not. Asked whether a semaphore file could
    recycle the mail server, retrieval ranked the semaphore-files page
    FIRST; its "Recycle Semaphore Files" section begins 3388 characters in
    and was cut. What survived was that page's generic example, "touch /sbbs/data/dothing.now". The model then answered with an invented "mail_servers_recycle_now.sem", patterned on the one example it had
    been shown, and defended the invention when challenged. Truncation
    chose what the model confabulated from.

    Five changes:

    Wiki pages are split into one chunk per heading. Splitting happens on
    the raw text, before markup stripping -- afterwards a heading is indistinguishable from body text. Each chunk leads with a breadcrumb
    so a subsection still carries its page's subject, and cites a #anchor
    deep link so an answer points at the section it came from.

    What survives the per-chunk cap is now the passage matching the query
    rather than the head of the document.

    Sections of one page are capped in the result set, default 2. A page
    used to occupy exactly one slot by construction; per-section chunks
    removed that guarantee, and a single long page's sections could fill
    the whole window -- one query came back with eight sections of the
    same page and none of the page that held the answer.

    Tokens are folded to a singular form. There was no stemming at all,
    so "recycles" could not match "recycle": the page holding the answer
    to one phrasing ranked 76th. Folding is plurals only -- verb-suffix
    chopping ("recycling" -> "recycl") does not converge on the form
    "recycle" folds to, so it would lose as many matches as it gained.
    tokenize() is duplicated between chat_llm.js and llm_index.js and the
    two MUST agree, so both copies change together; a short exception list
    covers tokens whose trailing s is integral ("news", "windows").

    The appended citation follows the retrieved chunk that best overlaps
    the reply instead of the top-ranked hit. BM25 ranks against the
    question, and once pages are split the highest-scoring section is
    frequently not the one the model used. Overlap is scored by Jaccard,
    not a raw count, because a long chunk has more distinct tokens and so
    more accidental matches.

    An unlabeled wiki link now keeps only its page component, so indexed
    prose reads "the file ctrl/recycle" rather than "dir:ctrl/recycle" --
    the namespace prefix was reaching users as though it were part of the
    path.

    Note for anyone tuning thresholds afterwards: per-token scores are on
    a different scale once chunks get shorter, so a threshold carried over unchanged is markedly less selective than it was. Measured on real
    traffic across this change, the median more than doubled and a
    volunteer-answer gate went from firing on 15% of turns to 48%.

    ---
    þ Synchronet þ Vertrauen þ Home of Synchronet þ [vert/cvs/bbs].synchro.net