Synchronet Git Commit Log

This is a log of the 500 most recent pushes to the master branch of the Synchronet Git repository.
If you want to view more pushes/commits, you can by passing ?<number> in URL.
  1. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 23:37:57 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/menues.c diff
    syncduke: declutter the in-game help footer Drop the verbose "- PAGE n OF 2 : PRESS A KEY ... -" prompt on both help pages for a minimal "PAGE n / 2" indicator -- the page affordance stays, the clutter goes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  2. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 23:35:50 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/menues.c diff
    src/doors/syncduke/Game/src/player.c diff
    src/doors/syncduke/syncduke_input.c diff
    xtrn/syncduke/controls.msg diff
    syncduke: complete the control bindings + redesign the in-game help Comparing the door's keymap against Duke's default key table turned up real gaps (R was stolen for AutoRun, no inventory cycle, no chase view). Keymap (syncduke_input.c): - R now reaches Duke's Steroids hotkey; AutoRun moved to Ctrl-R (frees the natural key, matching Duke). - [ / ] -> inventory select (reaches Scuba/Boots, no direct hotkey). - F7 / Ctrl-G -> chase view (extended the Ctrl-A..F->F1..F6 block to Ctrl-G; also map xterm F7 = CSI 18~). Engine already toggles on sc_F7. Ctrl-O mouse-steering toggle now flashes "MOUSE STEERING ON/OFF" on screen (player.c), via the same FTA quote path Duke uses for its own toggles -- the door raises a flag, the engine prints it (slot 122, unused in SP). In-game GAME CONTROLS chart (menues.c case 707) redesigned: two pages (movement/weapons/terminal, inventory/view) instead of one cramped sheet, colorized like Duke's F1 help -- yellow section headings, orange keys, blue actions (gametextpal pal 7 / pal 2) -- with ^X notation for the control-key shortcuts. Reflects the new R/Ctrl-R/[ ]/F7 bindings. controls.msg (lobby quick-ref) synced to the full, correct key set. Live-confirmed: help pages, R=Steroids, Ctrl-O popup, F7/Ctrl-G chase (incl. F7 on Contour w/ kitty-keys, which still sends legacy CSI 18~). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  3. Deucе
    Mon Jun 29 2026 22:56:04 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    xpbeep: cap pathological ALSA buffer latency Open ALSA once with the existing minimal parameter negotiation so user and .asoundrc defaults remain authoritative when they produce a reasonable buffer. If the negotiated ALSA buffer exceeds 300 ms, reopen and retry with a CoreAudio-like 3 x 1024-frame target. If the capped retry fails, fall back to the original uncapped configuration rather than dropping ALSA entirely. This should kick in for whatever weird-assed distro DigitalMan uses where the mixer latency with the default ALSA device is allegedly around 30s. Co-Authored-By: OpenAI Codex <codex@openai.com>
  4. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 22:33:23 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke_stubs.c diff
    src/doors/termgfx/audio.c diff
    src/doors/termgfx/audio.h diff
    src/doors/termgfx/audio_mgr.c diff
    src/doors/termgfx/audio_mgr.h diff
    syncduke: stereo pan for positional audio (ambiences + one-shots) Builds on the distance-attenuation fix: now sounds also track DIRECTION. SyncTERM's A;Volume APC accepts separate VL/VR (live per-channel left/right, not just mono V), so a running loop can be re-panned every frame with no re-queue click -- our wrapper only emitted V=, so add a VL=/VR= builder. - audio.c/.h: termgfx_audio_volume_lr() -- live per-side channel volume. - audio_mgr.c/.h: termgfx_audio_loop_volume() takes a pan, computes VL/VR (side toward the source full, opposite ramps -- the original audiolib law), and suppresses unchanged L/R so per-frame calls stay cheap. - syncduke_stubs.c: sd_angle_to_pan() replicates MV_CalcPanTable's triangle exactly (front=center, hard right at angle 8, behind=center, hard left at 24). FX_Pan3D pushes volume+pan to loops; one-shots (FX_PlayVOC3D/WAV3D) are panned at queue time via the existing pan slot. Pan is discrete per-frame (no T= ramp yet). Live-confirmed by ear: the cinema projector pans as you cross it, weapons/enemies have direction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  5. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 22:33:23 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke_stubs.c diff
    src/doors/termgfx/audio_mgr.c diff
    src/doors/termgfx/audio_mgr.h diff
    syncduke: 3D distance attenuation for looping ambiences (projector et al.) The engine re-pans every active positional voice each frame (sounds.c calls FX_Pan3D), so ambiences like the E1L1 cinema projector should swell as the player approaches. FX_Pan3D was a no-op stub, freezing each loop at the volume it had when first triggered -- the projector started at vol=7 (player far away) and stayed inaudible no matter how close you walked. - termgfx_audio_loop_volume(): update a running loop's channel volume live, suppressing redundant sends so it's safe to call every frame. - FX_Pan3D: map the per-frame distance to a volume (same curve as sd_loop_play) and push it to the loop. One-shots never reach here (fire-and-forget leaves Sound[].num==0, so the engine's pan loop skips them). Pan/stereo still not wired -- distance attenuation is what governs audibility here. Live-confirmed: projector now audible on approach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  6. Deucе
    Mon Jun 29 2026 20:31:13 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/syncterm_cache.js diff
    If lst is undefined, fail the upload.
  7. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 20:25:29 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/audio_midi.c diff
    src/doors/termgfx/audio_midi.h diff
    Modified Files:

    src/doors/syncduke/.gitignore diff
    src/doors/syncduke/CMakeLists.txt diff
    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/syncduke/syncduke_stubs.c diff
    src/doors/termgfx/CMakeLists.txt diff
    src/doors/termgfx/audio.c diff
    src/doors/termgfx/audio.h diff
    src/doors/termgfx/audio_mgr.c diff
    src/doors/termgfx/audio_mgr.h diff
    syncduke: OPL3 MIDI music + digital-audio polish Builds on the SFX path (5b29253803 diabetes-21-onto) with in-process music and a round of audio fixes, all through termgfx's SyncTERM audio-APC manager: - Music: render the GRP's MIDIs via libADLMIDI (OPL3), peak-normalized, cached as OGG/Vorbis (libsndfile, build-gated TERMGFX_WITH_SNDFILE) under a content- addressed m/<track>.ogg so a track uploads once; raw-WAV fallback otherwise. - SFX: transcode the VOCs libsndfile rejects (multi-block) to 8-bit WAV. - Setup Sound volume sliders wired (FX + music master) and SOUND/MUSIC toggles. - Looping ambient (FX_PlayLooped*) on dedicated channels; stop on door exit. - soundonce flood fix: a door-side per-sound rate limit. Returning a real voice handle so the engine's Sound[].num gate works would deadlock newgame()'s busy-wait on the skill-announce voice (the engine assumes an async audiolib completion source our APC path lacks), so instead we drop a same-sound re-dispatch inside an 8-tick window -- collapsing a per-tic soundonce stream to one burst while leaving real weapon fire untouched. New termgfx audio module (audio_midi.{c,h}) + audio.c/audio_mgr.c additions; syncduke wires them through its FX_/MUSIC_ stubs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  8. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 20:25:29 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/libADLMIDI/AUTHORS diff
    src/doors/termgfx/libADLMIDI/CMakeLists.txt diff
    src/doors/termgfx/libADLMIDI/LICENSE diff
    src/doors/termgfx/libADLMIDI/LICENSE.GPL-3.txt diff
    src/doors/termgfx/libADLMIDI/LICENSE.LGPL-2.1.txt diff
    src/doors/termgfx/libADLMIDI/cmake/FindLIBVLC.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/djgpp/djgpp-cmake-core.sh diff
    src/doors/termgfx/libADLMIDI/cmake/djgpp/djgpp-cmake.sh diff
    src/doors/termgfx/libADLMIDI/cmake/djgpp/toolchain-djgpp.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/mingw-dlls.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom-dos/custom-h/mymap.h diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom-dos/custom-h/myset.h diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom-dos/ow-cmake.sh diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom-dos/toolchain-ow.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom/Linux-OpenWatcom-C.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom/Linux-OpenWatcom-CXX.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom/Linux-OpenWatcom.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom/ow-cmake.sh diff
    src/doors/termgfx/libADLMIDI/cmake/openwattcom/toolchain-ow.cmake diff
    src/doors/termgfx/libADLMIDI/cmake/win-ci/lib-sdk.cmd diff
    src/doors/termgfx/libADLMIDI/cmake/win-ci/vlc-plugin.cmd diff
    src/doors/termgfx/libADLMIDI/cmake/win-ci/winmm-drivers.cmd diff
    src/doors/termgfx/libADLMIDI/include/adlmidi.h diff
    src/doors/termgfx/libADLMIDI/libADLMIDI.pc.in diff
    src/doors/termgfx/libADLMIDI/src/adlmidi.cpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_bankmap.h diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_bankmap.tcc diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_cvt.hpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_db.h diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_load.cpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_midiplay.cpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_midiplay.hpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_opl3.cpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_opl3.hpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_private.cpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_private.hpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_ptr.hpp diff
    src/doors/termgfx/libADLMIDI/src/adlmidi_sequencer.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/common/mutex.hpp diff
    src/doors/termgfx/libADLMIDI/src/chips/common/ptr.hpp diff
    src/doors/termgfx/libADLMIDI/src/chips/dosbox/dbopl.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/dosbox/dbopl.h diff
    src/doors/termgfx/libADLMIDI/src/chips/dosbox_opl3.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/dosbox_opl3.h diff
    src/doors/termgfx/libADLMIDI/src/chips/java/JavaOPL3.hpp diff
    src/doors/termgfx/libADLMIDI/src/chips/java_opl3.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/java_opl3.h diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked/nukedopl3.c diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked/nukedopl3.h diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked/nukedopl3_174.c diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked/nukedopl3_174.h diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked_opl3.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked_opl3.h diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked_opl3_v174.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/nuked_opl3_v174.h diff
    src/doors/termgfx/libADLMIDI/src/chips/opal/LICENSE.txt diff
    src/doors/termgfx/libADLMIDI/src/chips/opal/README.old diff
    src/doors/termgfx/libADLMIDI/src/chips/opal/opal-pan.diff diff
    src/doors/termgfx/libADLMIDI/src/chips/opal/opal.hpp diff
    src/doors/termgfx/libADLMIDI/src/chips/opal_opl3.cpp diff
    src/doors/termgfx/libADLMIDI/src/chips/opal_opl3.h diff
    src/doors/termgfx/libADLMIDI/src/chips/opl_chip_base.h diff
    src/doors/termgfx/libADLMIDI/src/chips/opl_chip_base.tcc diff
    src/doors/termgfx/libADLMIDI/src/cvt_mus2mid.hpp diff
    src/doors/termgfx/libADLMIDI/src/cvt_xmi2mid.hpp diff
    src/doors/termgfx/libADLMIDI/src/file_reader.hpp diff
    src/doors/termgfx/libADLMIDI/src/fraction.hpp diff
    src/doors/termgfx/libADLMIDI/src/inst_db.cpp diff
    src/doors/termgfx/libADLMIDI/src/midi_sequencer.h diff
    src/doors/termgfx/libADLMIDI/src/midi_sequencer.hpp diff
    src/doors/termgfx/libADLMIDI/src/midi_sequencer_impl.hpp diff
    src/doors/termgfx/libADLMIDI/src/oplinst.h diff
    src/doors/termgfx/libADLMIDI/src/structures/pl_list.hpp diff
    src/doors/termgfx/libADLMIDI/src/structures/pl_list.tcc diff
    src/doors/termgfx/libADLMIDI/src/wopl/wopl_file.c diff
    src/doors/termgfx/libADLMIDI/src/wopl/wopl_file.h diff
    termgfx: vendor libADLMIDI (OPL3/Nuked OPL3 MIDI synthesizer) Pinned snapshot, used to render Duke Nukem 3D's MIDI music to PCM in-process (OPL3 emulation, embedded instrument banks) for the SyncTERM audio-APC path. Vendored like Chocolate Duke3D -- not tracked upstream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  9. Rob Swindell (on Debian Linux)
    Mon Jun 29 2026 20:25:28 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncduke/lobby.msg diff
    Updated lobby art (it still sucks though)
  10. Deucе
    Mon Jun 29 2026 14:56:14 GMT-0700 (PDT)
    Modified Files:
    

    3rdp/win64.release/libsndfile/bin/libsndfile.a diff
    Rebuild Win64 libsndfile to use FLAC__NO_DLL
  11. Deucе
    Mon Jun 29 2026 14:46:32 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    Library order and shlwapi.dll for Windows
  12. Deucе
    Mon Jun 29 2026 14:38:15 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    Add Vorbis path to library search path for Windows
  13. Deucе
    Mon Jun 29 2026 14:37:01 GMT-0700 (PDT)
    Modified Files:
    

    3rdp/darwin.release/flac/lib/libFLAC++.a diff
    3rdp/darwin.release/flac/lib/libFLAC.a diff
    3rdp/darwin.release/lame/lib/libmp3lame.a diff
    3rdp/darwin.release/libsndfile/lib/libsndfile.a diff
    3rdp/darwin.release/mpg123/lib/libmpg123.a diff
    3rdp/darwin.release/mpg123/lib/libout123.a diff
    3rdp/darwin.release/mpg123/lib/libsyn123.a diff
    3rdp/darwin.release/mpg123/lib/pkgconfig/libmpg123.pc diff
    3rdp/darwin.release/mpg123/lib/pkgconfig/libout123.pc diff
    3rdp/darwin.release/mpg123/lib/pkgconfig/libsyn123.pc diff
    3rdp/darwin.release/ogg/lib/libogg.a diff
    3rdp/darwin.release/opus/lib/libopus.a diff
    3rdp/darwin.release/vorbis/lib/libvorbis.a diff
    3rdp/darwin.release/vorbis/lib/libvorbisenc.a diff
    3rdp/darwin.release/vorbis/lib/libvorbisfile.a diff
    Update to older Darwin version So CI builds
  14. Deucе
    Mon Jun 29 2026 14:26:35 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    And win32 needs to link with the Vorbis libs too Not enough to just toss then in some random place in the file system I guess.
  15. Deucе
    Mon Jun 29 2026 14:23:20 GMT-0700 (PDT)
    Added Files:
    

    3rdp/win32.release/vorbis/bin/libvorbis.a diff
    3rdp/win32.release/vorbis/bin/libvorbisenc.a diff
    3rdp/win32.release/vorbis/include/vorbis/codec.h diff
    3rdp/win32.release/vorbis/include/vorbis/vorbisenc.h diff
    3rdp/win32.release/vorbis/include/vorbis/vorbisfile.h diff
    3rdp/win64.release/vorbis/bin/libvorbis.a diff
    3rdp/win64.release/vorbis/bin/libvorbisenc.a diff
    3rdp/win64.release/vorbis/include/vorbis/codec.h diff
    3rdp/win64.release/vorbis/include/vorbis/vorbisenc.h diff
    3rdp/win64.release/vorbis/include/vorbis/vorbisfile.h diff
    Add libvorbis v1.3.7 too Windows needs it to link, but doesn't need it to build libsndfile apparently.
  16. Deucе
    Mon Jun 29 2026 14:11:36 GMT-0700 (PDT)
    Added Files:
    

    3rdp/darwin.release/flac/include/FLAC++/all.h diff
    3rdp/darwin.release/flac/include/FLAC++/decoder.h diff
    3rdp/darwin.release/flac/include/FLAC++/encoder.h diff
    3rdp/darwin.release/flac/include/FLAC++/export.h diff
    3rdp/darwin.release/flac/include/FLAC++/metadata.h diff
    3rdp/darwin.release/flac/include/FLAC/all.h diff
    3rdp/darwin.release/flac/include/FLAC/assert.h diff
    3rdp/darwin.release/flac/include/FLAC/callback.h diff
    3rdp/darwin.release/flac/include/FLAC/export.h diff
    3rdp/darwin.release/flac/include/FLAC/format.h diff
    3rdp/darwin.release/flac/include/FLAC/metadata.h diff
    3rdp/darwin.release/flac/include/FLAC/ordinals.h diff
    3rdp/darwin.release/flac/include/FLAC/stream_decoder.h diff
    3rdp/darwin.release/flac/include/FLAC/stream_encoder.h diff
    3rdp/darwin.release/flac/lib/libFLAC++.a diff
    3rdp/darwin.release/flac/lib/libFLAC.a diff
    3rdp/darwin.release/flac/lib/pkgconfig/flac++.pc diff
    3rdp/darwin.release/flac/lib/pkgconfig/flac.pc diff
    3rdp/darwin.release/lame/include/lame/lame.h diff
    3rdp/darwin.release/lame/lib/libmp3lame.a diff
    3rdp/darwin.release/libsndfile/include/sndfile.h diff
    3rdp/darwin.release/libsndfile/include/sndfile.hh diff
    3rdp/darwin.release/libsndfile/lib/libsndfile.a diff
    3rdp/darwin.release/libsndfile/lib/pkgconfig/sndfile.pc diff
    3rdp/darwin.release/mpg123/include/fmt123.h diff
    3rdp/darwin.release/mpg123/include/mpg123.h diff
    3rdp/darwin.release/mpg123/include/out123.h diff
    3rdp/darwin.release/mpg123/include/syn123.h diff
    3rdp/darwin.release/mpg123/lib/libmpg123.a diff
    3rdp/darwin.release/mpg123/lib/libout123.a diff
    3rdp/darwin.release/mpg123/lib/libsyn123.a diff
    3rdp/darwin.release/mpg123/lib/pkgconfig/libmpg123.pc diff
    3rdp/darwin.release/mpg123/lib/pkgconfig/libout123.pc diff
    3rdp/darwin.release/mpg123/lib/pkgconfig/libsyn123.pc diff
    3rdp/darwin.release/ogg/include/ogg/config_types.h diff
    3rdp/darwin.release/ogg/include/ogg/ogg.h diff
    3rdp/darwin.release/ogg/include/ogg/os_types.h diff
    3rdp/darwin.release/ogg/lib/libogg.a diff
    3rdp/darwin.release/ogg/lib/pkgconfig/ogg.pc diff
    3rdp/darwin.release/opus/include/opus/opus.h diff
    3rdp/darwin.release/opus/include/opus/opus_defines.h diff
    3rdp/darwin.release/opus/include/opus/opus_multistream.h diff
    3rdp/darwin.release/opus/include/opus/opus_projection.h diff
    3rdp/darwin.release/opus/include/opus/opus_types.h diff
    3rdp/darwin.release/opus/lib/libopus.a diff
    3rdp/darwin.release/opus/lib/pkgconfig/opus.pc diff
    3rdp/darwin.release/vorbis/include/vorbis/codec.h diff
    3rdp/darwin.release/vorbis/include/vorbis/vorbisenc.h diff
    3rdp/darwin.release/vorbis/include/vorbis/vorbisfile.h diff
    3rdp/darwin.release/vorbis/lib/libvorbis.a diff
    3rdp/darwin.release/vorbis/lib/libvorbisenc.a diff
    3rdp/darwin.release/vorbis/lib/libvorbisfile.a diff
    3rdp/darwin.release/vorbis/lib/pkgconfig/vorbis.pc diff
    3rdp/darwin.release/vorbis/lib/pkgconfig/vorbisenc.pc diff
    3rdp/darwin.release/vorbis/lib/pkgconfig/vorbisfile.pc diff
    Modified Files:

    src/syncterm/GNUmakefile diff
    src/syncterm/re1/backtrack.c diff
    src/syncterm/re1/pike.c diff
    src/syncterm/re1/regexp.h diff
    src/syncterm/re1/sub.c diff
    And add static macOS fat libs too macOS has been tested, Windows has not.
  17. Deucе
    Mon Jun 29 2026 11:47:44 GMT-0700 (PDT)
    Added Files:
    

    3rdp/win32.release/flac/bin/libFLAC.a diff
    3rdp/win32.release/flac/include/FLAC++/all.h diff
    3rdp/win32.release/flac/include/FLAC++/decoder.h diff
    3rdp/win32.release/flac/include/FLAC++/encoder.h diff
    3rdp/win32.release/flac/include/FLAC++/export.h diff
    3rdp/win32.release/flac/include/FLAC++/metadata.h diff
    3rdp/win32.release/flac/include/FLAC/all.h diff
    3rdp/win32.release/flac/include/FLAC/assert.h diff
    3rdp/win32.release/flac/include/FLAC/callback.h diff
    3rdp/win32.release/flac/include/FLAC/export.h diff
    3rdp/win32.release/flac/include/FLAC/format.h diff
    3rdp/win32.release/flac/include/FLAC/metadata.h diff
    3rdp/win32.release/flac/include/FLAC/ordinals.h diff
    3rdp/win32.release/flac/include/FLAC/stream_decoder.h diff
    3rdp/win32.release/flac/include/FLAC/stream_encoder.h diff
    3rdp/win32.release/lame/bin/libmp3lame.a diff
    3rdp/win32.release/lame/include/lame/lame.h diff
    3rdp/win32.release/libsndfile/bin/libsndfile.a diff
    3rdp/win32.release/libsndfile/include/sndfile.h diff
    3rdp/win32.release/libsndfile/include/sndfile.hh diff
    3rdp/win32.release/mpg123/bin/libmpg123.a diff
    3rdp/win32.release/mpg123/bin/libout123.a diff
    3rdp/win32.release/mpg123/bin/libsyn123.a diff
    3rdp/win32.release/mpg123/include/fmt123.h diff
    3rdp/win32.release/mpg123/include/mpg123.h diff
    3rdp/win32.release/mpg123/include/out123.h diff
    3rdp/win32.release/mpg123/include/syn123.h diff
    3rdp/win32.release/ogg/bin/libogg.a diff
    3rdp/win32.release/ogg/include/ogg/config_types.h diff
    3rdp/win32.release/ogg/include/ogg/ogg.h diff
    3rdp/win32.release/ogg/include/ogg/os_types.h diff
    3rdp/win32.release/opus/bin/libopus.a diff
    3rdp/win32.release/opus/include/opus/opus.h diff
    3rdp/win32.release/opus/include/opus/opus_custom.h diff
    3rdp/win32.release/opus/include/opus/opus_defines.h diff
    3rdp/win32.release/opus/include/opus/opus_multistream.h diff
    3rdp/win32.release/opus/include/opus/opus_projection.h diff
    3rdp/win32.release/opus/include/opus/opus_types.h diff
    3rdp/win64.release/flac/bin/libFLAC.a diff
    3rdp/win64.release/flac/include/FLAC++/all.h diff
    3rdp/win64.release/flac/include/FLAC++/decoder.h diff
    3rdp/win64.release/flac/include/FLAC++/encoder.h diff
    3rdp/win64.release/flac/include/FLAC++/export.h diff
    3rdp/win64.release/flac/include/FLAC++/metadata.h diff
    3rdp/win64.release/flac/include/FLAC/all.h diff
    3rdp/win64.release/flac/include/FLAC/assert.h diff
    3rdp/win64.release/flac/include/FLAC/callback.h diff
    3rdp/win64.release/flac/include/FLAC/export.h diff
    3rdp/win64.release/flac/include/FLAC/format.h diff
    3rdp/win64.release/flac/include/FLAC/metadata.h diff
    3rdp/win64.release/flac/include/FLAC/ordinals.h diff
    3rdp/win64.release/flac/include/FLAC/stream_decoder.h diff
    3rdp/win64.release/flac/include/FLAC/stream_encoder.h diff
    3rdp/win64.release/lame/bin/libmp3lame.a diff
    3rdp/win64.release/lame/include/lame/lame.h diff
    3rdp/win64.release/libsndfile/bin/libsndfile.a diff
    3rdp/win64.release/libsndfile/include/sndfile.h diff
    3rdp/win64.release/libsndfile/include/sndfile.hh diff
    3rdp/win64.release/mpg123/bin/libmpg123.a diff
    3rdp/win64.release/mpg123/bin/libout123.a diff
    3rdp/win64.release/mpg123/bin/libsyn123.a diff
    3rdp/win64.release/mpg123/include/fmt123.h diff
    3rdp/win64.release/mpg123/include/mpg123.h diff
    3rdp/win64.release/mpg123/include/out123.h diff
    3rdp/win64.release/mpg123/include/syn123.h diff
    3rdp/win64.release/ogg/bin/libogg.a diff
    3rdp/win64.release/ogg/include/ogg/config_types.h diff
    3rdp/win64.release/ogg/include/ogg/ogg.h diff
    3rdp/win64.release/ogg/include/ogg/os_types.h diff
    3rdp/win64.release/opus/bin/libopus.a diff
    3rdp/win64.release/opus/include/opus/opus.h diff
    3rdp/win64.release/opus/include/opus/opus_custom.h diff
    3rdp/win64.release/opus/include/opus/opus_defines.h diff
    3rdp/win64.release/opus/include/opus/opus_multistream.h diff
    3rdp/win64.release/opus/include/opus/opus_projection.h diff
    3rdp/win64.release/opus/include/opus/opus_types.h diff
    Modified Files:

    src/syncterm/GNUmakefile diff
    Add Windows libraries for libsndfile... libsndfile v1.2.2 FLAC v1.5.0 LAME v3.100 mpg123 v1.33.6 ogg v1.3.6 opus v1.6.1
  18. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 23:50:43 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/i_termsound.c diff
    Modified Files:

    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/i_sound.c diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: digital sound effects via SyncTERM audio APC Wire SyncDOOM's SFX through the shared termgfx audio module (added for SyncDuke in the prior commit): a Doom sound_module_t (i_termsound.c) registered in i_sound.c's sound_modules[], so the engine's normal S_StartSound -> I_StartSound path drives it with no game-code changes. It builds the DS<name> lump name, reads the DMX header (rate + 8-bit PCM) and hands the samples to termgfx_audio_sfx, which WAV-wraps and ships them to SyncTERM for libsndfile decode + mixing. syncdoom.c creates the manager in DG_Init, fires the libsndfile cap probe, and feeds inbound bytes in pump_input (with a one-shot "audio: tier=N" dlog). Gated on a libsndfile-capable SyncTERM (tier 1); a silent no-op otherwise. Validated live on SyncTERM 1.10a. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  19. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 23:33:54 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/audio.c diff
    src/doors/termgfx/audio.h diff
    src/doors/termgfx/audio_mgr.c diff
    src/doors/termgfx/audio_mgr.h diff
    Modified Files:

    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/syncduke/syncduke_stubs.c diff
    src/doors/termgfx/CMakeLists.txt diff
    syncduke: digital sound effects via SyncTERM audio APC Add a termgfx audio module and wire SyncDuke's SFX through it: * termgfx/audio.{c,h} -- I/O-free SyncTERM:A / SyncTERM:Q builders (Store, Load, Queue, Synth, Flush) + the libsndfile capability parser. * termgfx/audio_mgr.{c,h} -- per-session policy: capability tier, an upload-once sample cache, per-play slot Load+Queue (a Queue empties its slot, so each play re-Loads -- the file stays cached, no re-transfer), round-robin channel pool, reserved music channel. NULL/tier<1 = silent. * syncduke FX_PlayVOC3D/WAV3D ship each in-memory VOC/WAV to SyncTERM, which decodes it via libsndfile and mixes it; the cap probe rides the startup handshake and the reply is parsed from the input stream. Gated on a libsndfile-capable SyncTERM (audio APC, tier 1); a no-op on older or non-SyncTERM clients. Validated live on SyncTERM 1.10a -- weapon/menu/world SFX play and repeat. Music (MIDI), looped/ambient SFX and 3D pan are follow-ups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  20. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:59:29 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/syncdoom.c diff
    src/doors/syncduke/README.md diff
    syncdoom: kitty keyboard protocol -- true key-up, hold-to-move, native turn Port SyncDuke's kitty keyboard support to SyncDOOM. On a terminal that speaks the kitty keyboard protocol (e.g. Contour) the door queries support at startup (CSI?u), pushes the progressive-enhancement flags on the reply (CSI>11u), and pops them on exit -- so it gets explicit key press/repeat/release instead of faking key-up with a timeout. Gated: terminals that don't answer keep the existing key-up-synthesis scheme, byte-for-byte unchanged. Under kitty: - parse_byte() handles the CSI-u key events plus the disambiguated F-key/nav forms Contour sends (F1=CSI P, F2=CSI Q, F3=13~, F4=CSI S, F5=15~, F6=17~; Home=CSI H, End=CSI F; numpad Private-Use-Area codepoints incl. numpad Enter). kitty_dispatch() routes door hotkeys (F4 tier-cycle, Ctrl-S/T/O/U/P) to key_seen() on press only and game keys to explicit keyq_push down/up -- bypassing the s_active grace machinery, so movement holds and Doom's native turn-accel ramp runs. - The Ctrl-S stats strip shows "kbd:kitty". - Options shows the now-moot TAP/HOLD/TURN/FAST-TURN knobs as "NATIVE" (the byte-path key-feel tuning does nothing with real key-up). - Home/End in a menu jump to the first/last item. Also document the kitty behavior in both door READMEs. Validated on Contour; SyncTERM and other terminals unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  21. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncduke/tests/test_kitty.c diff
    Modified Files:

    src/doors/syncduke/Game/src/menues.c diff
    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/syncduke/tests/test_keymap.c diff
    syncduke: kitty keyboard protocol -- true key-up, hold-to-move, native turn Terminals that speak the kitty keyboard protocol (e.g. Contour) report explicit key press/repeat/release, so the door no longer has to fake key-up with a timeout. The door queries support at startup (CSI?u); on a reply it pushes the progressive- enhancement flags (CSI>11u: disambiguate + event-types + report-all-keys) and pops them on exit (CSI<u). Negotiation is gated -- terminals that don't answer keep the existing byte-path behavior, unchanged. Under kitty: - Movement (WASD/strafe/space) and the turn arrows drive Duke's real key state (kitty_press/kitty_release), held until the actual key-up -- crisp hold-to-move, and turning uses the engine's native turn-accel ramp instead of the door's synthetic rate, so it feels like the original. expire() still times out momentary press() keys (F-keys, Center) while leaving kitty-held keys down (far release_at). - Full key map for Contour's encodings: printables/numbers/space + numpad (the Private-Use-Area keypad codepoints, incl. numpad Enter) via CSI-u; arrows as CSI A/B/C/D with release; F1=CSI P, F2=CSI Q, F3=13~, F4=CSI S, F5=15~, F6=17~; PgUp/PgDn look, Home/End center. F-key release events are gated press-only so nothing double-fires. - Ctrl-A..F aliases now mirror the door's F-keys (Ctrl-A=help, Ctrl-D=tier cycle, Ctrl-B/C/E/F=Duke save/load) instead of raw Duke scancodes. - Home/End in a MENU jump to the first/last item (probeXduke); in gameplay they still Center_View. - The Ctrl-S stats strip shows "kbd:kitty" when negotiated. - Setup Controls greys + disables the byte-path feel knobs (KEY TAP/HOLD, TURN SPEED, FAST TURN) and shows a contextual "native key timing" note on hover, since those only compensate for terminals without key-up. tests/test_kitty.c drives the real pump with Contour's byte sequences and asserts the decoded scancodes / actions; test_keymap.c gains a stub for the new output path. Live-validated on Contour (hold-to-move, turn, F-keys, numpad, menus) with SyncTERM unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  22. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke_config.c diff
    xtrn/CLAUDE.md diff
    syncduke: put the debug log in data/syncduke/; document SBBS env vars A bare [debug] log filename now goes in <SBBSDATA>/syncduke/ (the door's shared data dir, alongside the games registry) rather than the per-user dir, so co-op nodes' logs sit together and don't clutter user storage. Uses the SBBSDATA env var SBBS sets for external programs (no hardcoded path, no new door arg); still node-tagged via sbbs_my_node(); an explicit path or a dev run (no SBBSDATA) is unchanged. Also document in xtrn/CLAUDE.md the five env vars SBBS exports to external programs (SBBSCTRL/SBBSDATA/SBBSEXEC/SBBSNODE/SBBSNNUM), the dir-vs-number distinction, and the don't-derive / dev-fallback caveats. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  23. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/game.c diff
    syncduke: allow 11-char player names; strip whitespace when over-long getnames() and the type-6 receive both capped the netgame name at 10 chars (the stock fragbar-field limit), truncating an 11-char alias like "Digital Man" to "DIGITAL MA". Raise both ends to 11 (= MAXPLAYERNAMELENGTH; user_name[] is 32 and the name packet is variable-length, so the arrays/protocol already have room). For an alias longer than 11, drop whitespace first so the cap keeps more alphanumerics ("Digital Man Jr" -> "DIGITALMANJ"). The deathmatch fragbar column is drawn for 10, so an 11th char sits a touch wide there (cosmetic; co-op's player list has room). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  24. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/player.c diff
    syncduke: fix co-op desync from non-synced terminal look (horiz) The terminal PgUp/PgDn "look" applied a per-node variable (syncduke_pitch_step) straight to ps[snum].horiz in processinput, which runs for every player -- so the looking player's node moved that player's pitch while the other node's copy of the variable was 0. horiz diverged between the two sims and the lockstep syncval check tripped "OUT OF SYNC". (Root-caused by logging the syncval components on both nodes and diffing: horiz was the lone diverging field.) Route the pitch through the synced input, like the mouse-steer turn already is: getinput folds one notch/tic into the FIFO's horz field, and processinput applies sync[snum].horz to the correct player on both sims (in either aim mode, after the auto-center so a tap holds). Verified: extended 2-player co-op play with look, no desync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  25. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke_config.c diff
    syncduke: per-node debug-log filename so co-op nodes don't clobber one log Two co-op doors run by the same user on different nodes share one per-user dir, so both wrote the same [debug] log and clobbered it (which made the SIGSEGV above hard to read). Tag the configured log filename with the node number from sbbs_my_node() (termgfx; reads SBBSNNUM that SBBS sets for every external program, falling back to SBBSNODE's trailing digits) -> syncduke.<node>.log. Respects [debug] log on/off; no node env (dev/standalone) leaves the name unchanged. sbbs_node.c is already in the termgfx library SyncDuke links, so this is just an #include. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  26. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke_io.c diff
    syncduke: bound the sixel encode to its buffer (fix full-res co-op SIGSEGV) The full-res sixel opt-in (5cbc69c24) encodes sxh=sdh without the half-res /2, so on a large canvas (sdw at the 1024 width cap, sdh ~691 with the aspect/stretch fit) sxw*sxh exceeded the fixed syncduke_scaled[1024*640] buffer and overran into the adjacent globals -- g_out got clobbered with palette bytes, and the next out_put memcpy faulted (SIGSEGV in syncduke_present, seen at co-op level entry). Root-caused from the core: &g_out sits 48 bytes past the buffer end. Hard-bound the encoded area to the buffer (sxw*sxh <= OUT_W_MAX*OUT_H_MAX) so it can never overrun again, and raise OUT_H_MAX 640->768 so a full-res encode on a maxed window isn't needlessly shrunk by that clamp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  27. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    src/doors/syncduke/syncduke_io.c diff
    syncduke/syncdoom: full-resolution sixel F4 opt-in for pan-ignoring terminals Default non-SyncTERM sixel encodes at half vertical resolution and relies on the terminal's "2;1 raster aspect to double it. Some terminals (e.g. WezTerm) ignore the aspect ratio and render the image at half height. Add a "sixel-full" stop to the F4 video-tier cycle (offered only on non-SyncTERM) that encodes at full vertical resolution (vsc=1, no terminal scaling) -- correct on any sixel terminal at ~2x the wire bytes. Default stays the cheap half-res; the choice is per-user sticky (SyncDOOM: <home>/syncdoom.ini [video] sixel_fullres; SyncDuke: a syncduke.fullres flag-file in the per-user dir). SyncTERM is unaffected (JXL tier; the vsc=1 path is gated off there). Also: shorten the F4 cycle banner to "Video: <tier>" (drop the app name and the "(F4 to cycle)" tip -- it only shows on an F4 press) and fix its centering to use the real cell width instead of out_w/8 (a wide font over-counted columns and ran the label off-screen); show "sixel-full" in the Ctrl-S stats strip in both doors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  28. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/termgfx/term.c diff
    src/doors/termgfx/term.h diff
    termgfx: correct the DECSDM ?80 rationale in the comments The term.h/term.c comments claimed mode 80 "defaults to SET" and that ?80l "pins the sixel to top-left" -- both wrong. SyncTERM/cterm reversed mode 80 in rev 1.328 (2026) to match real VT-340 hardware (it defaults to RESET), and the load-bearing effect of ?80l for us is POSITIONING, not scroll-prevention: under ?80l a non-SyncTERM sixel terminal draws the image at the text cursor (so the door centers it), while SyncTERM's cterm ignores the cursor and anchors top-left. Scroll-prevention is handled separately by keeping the image off the last text row (the bottom-cell reserve). Comments only; no code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  29. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Added Files:
    

    exec/load/game_lobby.js diff
    xtrn/syncduke/controls.msg diff
    xtrn/syncduke/get-binary.js diff
    xtrn/syncduke/lobby.js diff
    xtrn/syncduke/lobby.msg diff
    xtrn/syncduke/syncduke_lib.js diff
    Modified Files:

    src/doors/syncduke/Game/src/game.c diff
    src/doors/syncduke/Game/src/menues.c diff
    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_config.c diff
    src/doors/syncduke/syncduke_door.c diff
    src/doors/syncduke/syncduke_game.c diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_net.c diff
    src/doors/syncduke/tests/test_keymap.c diff
    xtrn/syncduke/.gitignore diff
    xtrn/syncduke/install-xtrn.ini diff
    xtrn/syncduke/syncduke.example.ini diff
    syncduke: LAN co-op lobby, chat-sync + alias fixes, door config The v1 co-op feature set on top of the UDP mmulti transport: - Lobby (xtrn/syncduke/lobby.js over the shared exec/load/game_lobby.js): Create/Join a 2-player LAN game, play single-player, or view controls. It spawns the native door directly (-s socket, no drop file), so it registers as a JS module (?lobby), not a DOOR32 native. install-xtrn wires it plus get-binary.js (pre-exec, required) so the door never registers without a runnable executable. - Door net args (-netrole/-netport/-netpeer) parsed and stripped before the engine's command-line parser; syncduke.ini [net] (advertise, port range, stale) and [video] (sixel_max_width, use_cell_size) knobs. - Chat OOS fix: typemode() built the message in a local buffer but sent the stale global tempbuf, corrupting the move FIFO -> "OUT OF SYNC". Send the built text. - Alias fix: seed the player name from the door's -name (the BBS alias) instead of the engine default "XDUKE". - Controls: SyncDOOM-style action layer (Space=fire, WASD move/strafe, arrows turn) gated on gameplay so menus and Talk get literal keys. - Cell-size probe (ESC[16t) for canvas sizing on non-SyncTERM terminals; main-menu version line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  30. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    src/doors/syncduke/syncduke_io.c diff
    syncduke/syncdoom: fill the terminal window without the last-row sixel white bar The image tiers now scale up to fill the probed window (sixel width cap raised 640 -> 1024) instead of a small centered image. To do that safely, reserve one text-cell row at the bottom of the sixel fit: a terminal that ignores DECSDM ?80l (e.g. Windows Terminal) scrolls a sixel that reaches its last text row, scrolling blank/white lines in below it -- the image renders short with a white bar beneath. Keeping the sixel off the last row avoids the scroll. JXL/PPM (positioned by APC pixel offset, not a text cell) are unaffected and keep the full scale_max. Both doors fit a 1.6:1 frame through the same path, so the change is mirrored in syncduke_io.c and syncdoom.c. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  31. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 22:01:25 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncduke/syncduke_net.c diff
    Modified Files:

    src/doors/syncduke/CMakeLists.txt diff
    src/doors/syncduke/syncduke_stubs.c diff
    syncduke: LAN co-op — fill the mmulti seam with a UDP transport The vendored Build/Duke netcode (game.c getpackets() + the move-FIFO lockstep) was intact; only the mmulti transport seam was stubbed single-player. syncduke_net.c fills it for 2-player LAN co-op: - getpacket/sendpacket carry raw game packets between two door instances over UDP, reporting the sender's player index from the source address (broadcast when other<0). - initmultiplayers does a tiny master/join handshake to set the mmulti globals (numplayers / myconnectindex / the connect list) before the engine reads them. - env-configured, single-player-safe when unset: SYNCDUKE_NET=master SYNCDUKE_NET_PORT=<port> -> player 0 SYNCDUKE_NET=join SYNCDUKE_NET_PEER=host:port -> player 1 - own UDP (no new dep): Chocolate-Duke's ENet mmulti is the protocol oracle but the ENet lib was removed from the tree; this matches SyncDOOM's from-scratch transport. Validated end-to-end with two live instances on the loopback driven into a shared E1L1 cooperative level (slash warp args /v1 /l1 /c2 -- the engine's parser only honors '/' options, not '-'): the full Duke netgame negotiation flows bidirectionally (weapon order, version/CRC match, names), both pass every waitforeverybody() sync barrier, both complete enterlevel() (ready2send=1), and the move-FIFO lockstep then pumps -- ~320 per-tic sync/input packets each way with the simulation stepping in lockstep (ototalclock advancing tic for tic on both). Sampled packet logging (first 16 then every 64th, gated on SYNCDUKE_LOG) is left in as netplay bring-up diagnostics. genericmultifunction stays a no-op stub. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  32. Deucе
    Sun Jun 28 2026 19:13:23 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm_dec.c diff
    Fix sixel row background mask initialization Clear the reusable sixel row mask when starting each raster line and initialize the current raster span with the sixel background colour. This prevents unset pixels from reusing mask and pixel data left over from the previous sixel row buffer. Track the right edge of horizontally-scaled sixel cells so the full cell width is flushed to the backend. This should fix ticket 258. Co-Authored-By: OpenAI Codex <codex@openai.com>
  33. Deucе
    Sun Jun 28 2026 17:35:46 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm_dec.c diff
    Parse complete sixel numeric commands incrementally Co-Authored-By: OpenAI Codex <codex@openai.com>
  34. Deucе
    Sun Jun 28 2026 16:46:34 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.adoc diff
    src/conio/cterm.c diff
    src/conio/cterm_cterm.c diff
    src/conio/cterm_dec.c diff
    Reverse DECSDM meaning DEC mode 80 is the "Sixel Display Mode" which, when set, *disables* sixel scrolling, and defaults to reset. This is the opposite of what the VT-340 manual says, but experiments on a real VT-340 in 2021 show this behaviour is correct. Bump revision.
  35. Deucе
    Sun Jun 28 2026 13:47:01 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/WrenTODO.md diff
    Trim completed Wren TODO entries Keep WrenTODO focused on live work by removing completed audit history and resolved implementation notes. Co-Authored-By: OpenAI Codex <codex@openai.com>
  36. Deucе
    Sun Jun 28 2026 13:40:06 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/WrenTODO.md diff
    src/syncterm/bbslist.h diff
    src/syncterm/conn.c diff
    src/syncterm/conn.h diff
    src/syncterm/scripts/auto/connected/status_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/syncterm.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    Move connection timing out of BBS entries Track live connection elapsed time in the connection layer and expose it to Wren as Conn.elapsedSeconds. Remove the runtime timing fields from BBS and update the default status bar/docs accordingly. Co-Authored-By: OpenAI Codex <codex@openai.com>
  37. Deucе
    Sun Jun 28 2026 12:34:34 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/host_popup.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/ui_popup.wren diff
    src/syncterm/scripts/ui_popup_test.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    Route terminal popups through Wren alerts Co-Authored-By: OpenAI Codex <codex@openai.com>
  38. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:24:55 GMT-0700 (PDT)
    Modified Files:
    

    exec/sbbslist.js diff
    Verify finger service on BBSes as well
  39. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:24:09 GMT-0700 (PDT)
    Modified Files:
    

    exec/filescancfg.js diff
    exec/load/shell_lib.js diff
    exec/msgscancfg.js diff
    Make the "Config:" and "Info:" prompts unique between main/msg and file menu
  40. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: client-side sixel scaling (bandwidth reduction, like SyncDuke) emit_frame_sixel sent a full-resolution sixel (pan=1) at the display size; now it encodes at 1/SIXEL_SCALE and emits the "pan;pad raster aspect so the terminal scales it back up -- SyncTERM integer-doubles (pad=2), other sixel terminals get the 2:1 pixel aspect (pad=1). The encoded height is clamped to whole 6-row bands (a partial final band garbles under pan>1). Lossless on SyncTERM: Doom's indexed buffer (I_VideoBuffer) is natively 320x200, so a SyncTERM frame encodes at native res, not a downscale. ~1/4 the sixel bytes on SyncTERM (320x198 vs 640x400), ~1/2 on other sixel terminals -- and the two doors now emit byte-identical sixel frames. Reuses the shared termgfx sixel_encode_aspect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  41. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/termgfx/pace.c diff
    src/doors/termgfx/pace.h diff
    termgfx: share the DSR RTT-sample update between the doors Second slice of the present/pace unification. Both doors fold each DSR round-trip into a 3/4 RTT EMA, latch rt_high at 40ms, and track a min-RTT baseline, then run the AIMD -- the same logic, with two small divergences: SyncDOOM also rejects a stale sample (a late reply for a reclaimed frame, rtt < EMA/3) and re-seeds the baseline after an 8s window; SyncDuke does neither. Extract it as termgfx_rtt_sample() parameterized by those two knobs (stale_reject, rtt_min_window_ms): Duke passes (0, 0), Doom passes (1, RTT_MIN_WINDOW). The function returns whether the sample was accepted so each door runs the AIMD only then. The DSR send-time ring + inflight + the reclaim/gate stay per-door. Pure refactor -- proven bit-identical to each door's old inline RTT logic across 14160 replayed samples (spanning stale/normal/spike RTT and the 8s window), 0 mismatches. Adds a rtt_min_at to SyncDuke (written by the helper, unused there since its window is 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  42. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/pace.c diff
    src/doors/termgfx/pace.h diff
    Modified Files:

    src/doors/syncdoom/syncdoom.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/termgfx/CMakeLists.txt diff
    termgfx: share the AIMD pipeline-depth controller between the doors SyncDuke and SyncDOOM had byte-identical copies of the delay-based AIMD that settles the DSR-ack frame pipeline depth from the measured round-trip (same thresholds, same 3/4 RTT EMA feeding it, same ceiling = one frame per 40ms of baseline RTT capped at 8, same sticky floor at rt_high). Extract it once into termgfx/pace.c (termgfx_aimd_update) and have both doors' auto_depth_update call it; the per-door tuning (the DEPTH_MAX cap, initial depth, the RTT bookkeeping and reclaim policy) stays in each door and is passed in. First slice of the present/pace unification, and the trickiest shared algorithm. Pure refactor -- proven bit-identical to the old inline controller across 25600 replayed (rtt, baseline, latch, time, cap, start-depth) steps, 0 mismatches. The DSR ring + RTT EMA (with the doors' small divergences -- RTT-min re-seed window, stale-sample guard) and the deadline-reclaim/gate are the next slices. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  43. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    syncduke: mouse-steer around the actual displayed image, not out_w The mouse steer mapped the pointer column to a turn rate around the center of an out_w x out_h image -- but the displayed image is now the fitted/centered sixel (or the JXL fill), and out_w no longer describes it. On a wide SyncTERM the sixel anchors top-left (cursor ignored under ?80l) yet the steer assumed canvas-center, so the neutral point was off. present() now records the displayed image's actual horizontal center column and half-width each frame -- accounting for tier and terminal (SyncTERM sixel top-left vs JXL/elsewhere centered) -- and the steer reads that (syncduke_hsteer). Replaces syncduke_image_geometry, whose out_w-based centering is gone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  44. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: use shared termgfx_geom_fit/center (phase 2 of the geometry unification) SyncDOOM's compute_geometry computed the fit-to-canvas image size and the center offset inline; SyncDuke now calls the same logic in termgfx/geometry.c. Replace Doom's inline math with termgfx_geom_fit() + termgfx_geom_center() so the two doors share one implementation instead of parallel copies. This is a pure refactor -- verified bit-identical to the old inline code across 8910 (vw, vh, cap) cases, 0 mismatches. The sixel-640 cap selection, the PPM / no-scale-fit branches, and the sixel-without-real-cell-size top-left override all stay in Doom (they're tier/door policy around the shared fit/center core). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  45. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke_io.c diff
    syncduke: aspect-preserve the sixel tier (fit, don't stretch to fill) The sixel tier sized the encoded frame to out_w x out_h -- the terminal's pixel canvas capped at 640x480 -- and nearest-scaled Duke's native 320x200 into it, which is 4:3 on a tall terminal: the game came out vertically stretched (e.g. encoded 320x240 at >=80x50 instead of ~320x200). Run the sixel through the same termgfx_geom_fit/center as the JXL tier: fit 320x200 into the real graphics canvas (XTSMGRAPHICS via syncduke_canvas_w/h, not the rows*16 text-area estimate) preserving 8:5, capped at 640 wide, then encode at 1/scale of that. The displayed sixel is now a constant 640x400 letter-boxed and centered, exactly like SyncDOOM. Side effects, all good: - no vertical stretch (keeps 8:5 on tall terminals); - the prior 80x50/60 over-hang is gone -- a fit is <= canvas by construction, so the frame can no longer scale past the real ~640x400 SyncTERM canvas; - ~18% less sixel bandwidth on tall terminals (320x198 vs 320x240). Both image tiers now share termgfx_geom_fit, lining SyncDuke up with SyncDOOM ahead of wiring Doom to the same helper. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  46. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/geometry.c diff
    src/doors/termgfx/geometry.h diff
    Modified Files:

    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_config.c diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/termgfx/CMakeLists.txt diff
    src/doors/termgfx/term.c diff
    xtrn/syncduke/syncduke.example.ini diff
    syncduke: JXL fill+center via shared termgfx geometry; widescreen fixes Start extracting the doors' image geometry into termgfx so SyncDuke and SyncDOOM stop diverging. New termgfx/geometry.{c,h} holds the scale-to-fill + center math that previously lived only in syncdoom.c (compute_geometry): - termgfx_geom_fit() -- fit a source into the canvas, aspect-preserving, scaled to fill, capped at scale_max, thin-bar stretch. - termgfx_geom_center() -- pixel offset (APC DX/DY) + 1-based cell origin. SyncDOOM will adopt these next; this commit wires SyncDuke. SyncDuke's JXL tier was a small, top-left 640x480 image because it never learned the real graphics canvas and blit 1:1 at DX/DY 0,0. Now: - the shared probe also sends ESC[?2;1S (XTSMGRAPHICS); SyncDuke parses the reply for SyncTERM's true graphics-canvas pixels (syncduke_canvas_w/h). - the JXL tier fills that canvas (capped at [video] scale_max, default 1280 to match SyncDOOM, configurable in syncduke.ini) and centers via the APC DX/DY. - the sixel cursor-centering reuses termgfx_geom_center too. The sixel/out-sizing/mouse paths are unchanged (cell size unknown on SyncTERM -> top-left fallback, exactly as before). Two widescreen fixes while here: - F4 tier switch now clears the screen UNCONDITIONALLY (it used to ride on the popup label, which is suppressed under the Ctrl-S stats overlay) -- a JXL-> sixel switch with stats up no longer leaves the old larger image showing. - the live stats strip right-justifies to the real terminal width (term px / cell) instead of the capped image width, so it sits at the right edge in a wide window instead of mid-canvas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  47. Rob Swindell (on Debian Linux)
    Sun Jun 28 2026 00:21:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/termgfx/term.c diff
    syncduke: center the sixel/image in the terminal canvas (port from syncdoom) SyncDUKE drew its image at the top-left (ESC[H), while SyncDOOM centers it. The difference only shows on terminals that honor the cursor as the sixel origin (e.g. Windows Terminal); SyncTERM ignores the cursor under DECSDM ?80l and anchors 0,0, so both doors look top-left there. Port SyncDOOM's approach: probe the cell-pixel size (ESC[16t) in addition to the window pixels (ESC[14t), then center the out_w x out_h image in the pixel canvas and position the cursor at the resulting cell (ESC[row;colH). Without a cell size (SyncTERM doesn't answer ESC[16t) we fall back to the top-left -- byte-for-byte the old behavior -- so the main platform is unchanged. The mouse steering now derives its center column and half-width from the same shared geometry helper, so pointer-to-turn mapping tracks the centered image instead of assuming a top-left, 8px-cell layout. - termgfx/term.c: add ESC[16t to the shared probe (SyncTERM ignores it). - syncduke_input.c: parse the ESC[16t reply; mouse steer uses image geometry. - syncduke.h: cell-size accessors + syncduke_image_geometry(). - syncduke_io.c: syncduke_image_geometry() + center the image-tier cursor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  48. Deucе
    Sun Jun 28 2026 00:02:09 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/syncterm/CHANGES diff
    Some Prestel fixes 1. Pass ESC through 2. Support "cursor on mode" as documented in the Acorn Prestel manual 3. Fix HOME binding for Prestel 4. Pass through CTRL-L and CTRL-T (Clear screen and delete line) This adds the bits that the online frame editor appears to have made use of (per the Prestel Custom Handbook).
  49. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 23:55:53 GMT-0700 (PDT)
    Modified Files:
    

    install/install.iss diff
    install/upgrade.iss diff
    install: ship OpenSSL libcrypto-3.dll for mail DKIM; bump version to 3.22a The Win32 installer (install.iss and upgrade.iss) now copies libcrypto-3.dll into exec/ so the mail server's DKIM signing (issue #215) works in installed builds. It is sourced from the vcpkg classic-mode install (c:\vcpkg\installed\x86-windows) via a new "vcpkg" define, with the skipifsourcedoesntexist flag so a build made without OpenSSL still produces a valid installer (DKIM simply absent). Also bump MyAppVersion 3.21e -> 3.22a and VersionInfoVersion 3.21.4 -> 3.22.0 to match the current release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  50. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 23:34:33 GMT-0700 (PDT)
    Added Files:
    

    src/build/openssl.props diff
    Modified Files:

    docs/v322_new.md diff
    src/sbbs3/mail_dkim.c diff
    src/sbbs3/mailsrvr.vcxproj diff
    mailsrvr: enable DKIM signing on the Win32 build (issue #215) The outbound DKIM signer added in 32ea273f6 (acting-27-fees) is OpenSSL-only (cryptlib cannot emit a raw PKCS#1 signature) and previously compiled to no-op stubs on Windows, so Win32 mail went out unsigned. Add src/build/openssl.props, imported by mailsrvr.vcxproj (both Win32 configs), sourcing libcrypto from a vcpkg classic-mode install (vcpkg install openssl:x86-windows), defining DKIM_OPENSSL, and linking libcrypto dynamically. The sheet is self-gating: with no vcpkg OpenSSL present it adds nothing and mail_dkim.c falls back to stubs, mirroring how the *nix build gates DKIM on libcrypto. Dynamic linkage lets the mail server share the single libcrypto-3.dll that an OpenSSL-enabled mosquitto.dll will also ship, instead of duplicating crypto via static linking or vendoring a bundle into 3rdp. Load the RSA private key via an in-memory BIO (BIO_new_mem_buf + PEM_read_bio_PrivateKey) instead of handing a FILE* to PEM_read_PrivateKey: on Windows the OpenSSL DLL has its own C runtime, so a FILE* hand-off fatally aborts ("OPENSSL_Uplink: no OPENSSL_Applink"). A memory BIO avoids the CRT boundary and is portable. Update docs/v322_new.md to note DKIM is supported on Windows builds too. Live-validated: Gmail reports dkim=pass (d=synchro.net s=sbbs) and dmarc=pass on DKIM alignment from vert.synchro.net (Win32). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  51. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 03:03:10 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/CMakeLists.txt diff
    src/doors/syncduke/syncduke_log.c diff
    syncduke: trap CRT fast-fails with a stack trace; emit .map + .pdb The crash logger had only an unhandled-exception filter, which CRT fast-fails (c0000409 -- invalid parameter, /GS stack cookie, etc.) and stack overflows bypass entirely, so a gameplay abend left nothing logged. Add a CRT invalid- parameter handler (logs a backtrace, then RETURNS so the bad call fails instead of killing the door), a pure-call handler, SIGABRT/SIGFPE/SIGILL handlers, and a vectored-exception backstop for access violations -- each logging a module+RVA stack trace via RtlCaptureStackBackTrace (resolved at runtime; no dbghelp, since the vendored Game/src/DbgHelp.h shadows the SDK header). Build now emits syncduke.map and a .pdb (/Zi + /DEBUG, optimizations preserved via /OPT:REF,/OPT:ICF) so the traces and any external (procdump) dumps resolve to functions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  52. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 03:02:49 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Engine/src/tiles.c diff
    syncduke: fix sheared/garbage savegame thumbnail (setviewtotile stride) setviewtotile() advanced ylookup by tileWidth while bytesperline was tileHeight, so each rendered row of the savegame thumbnail was stored ~60px too close (shear) and rows 0..tileWidth-1 filled only ~63% of the tile, leaving the bottom third as garbage. Match the row stride to bytesperline (tileHeight). The LOAD GAME preview now renders correctly (existing saves keep their already-captured bad thumbnail). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  53. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 03:02:46 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/menues.c diff
    syncduke: 2-page Controls Help, TURN SPEED label, reset input on load - Controls Help (case 707) is now two pages: page 1 = movement/action + door hotkeys, page 2 = the pass-through keys (Quick Kick, Holoduke/Jetpack/Night Vision/Medkit, use-inventory, turn-around, map-follow, crosshair, auto-aim). A key turns the page; ESC / a second key exits. - Rename the TURN HOLD slider to TURN SPEED (it now sets the continuous turn rate, not a key-hold duration). - loadplayer() calls syncduke_input_reset() so a restored game starts from the saved state rather than the pre-load crouch/turn latches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  54. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 03:02:26 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/player.c diff
    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_input.c diff
    syncduke: responsive keyboard turning + reset input state on load Turning: a terminal can't hold a key and the door refreshes key-down only once per presented frame, so Duke's per-sim-tic turn-key polling produced uneven, frame-tied bursts that no key-hold slider could smooth. Drive a time-windowed continuous turn level from the arrow bytes that getinput() adds to angvel every sim-tic (player.c) -- like the door's mouse steer / SyncDOOM's steady turn. Rate comes from the TURN slider (small floor so 0 ~ barely turns); FAST TURN is now a +50% turbo. Arrows stay raw scancodes in menus for navigation. Load: syncduke_input_reset() (called from loadplayer) clears the sticky-crouch latch, pending key-holds, queued scancodes, and turn/look momentum, so the pre-load session's input no longer bleeds into a restored game (e.g. crouch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  55. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 00:19:08 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/game.c diff
    src/doors/syncduke/Game/src/premap.c diff
    syncduke: restore intro fade-ins; gate demo recording Now that the GRP is cached and loads are near-instant, the splash screens flashed by. Restore the gradual palette fade-in by presenting EACH fade step (each is a distinct frame, so the de-dupe keeps it), keeping the existing holds/animations: - the ENTERING <level> screen (premap.c) -- a visible fade + ~1.5s hold so the level name is readable; - the 3D Realms logo and the title screen (game.c). Also gate opendemowrite() on syncduke_record_enabled() so a stale recstat=1 (from a saved duke3d.cfg) can't write a demo when recording is disabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  56. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 00:18:41 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/menues.c diff
    xtrn/syncduke/syncduke.example.ini diff
    syncduke: menu UX -- slider values, demo/RECORD + RESOLUTION removed, build footer Setup Controls: show each keyboard-feel slider's live 0..63 value right- justified just left of its bar; rename STEER SENSITIVITY -> MOUSE SENSITIVITY and MOUSE STEERING -> MOUSE (the toggle also gates the mouse buttons). Options: drop the RECORD (demo) item from the menu unless syncduke.ini [game] record=true -- a demo file is useless to a user who can't download it and just wastes disk (off by default; documented in syncduke.example.ini). Video Setup: remove the RESOLUTION item (the door renders a fixed 320x200 scaled to the terminal, so it had no effect); the remaining items renumber up. Main menu: show the git hash (lower-left) and build date (lower-right) via the generated git_hash.h, like SyncDOOM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  57. Rob Swindell (on Windows 11)
    Sat Jun 27 2026 00:18:20 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncduke/syncduke_log.c diff
    Modified Files:

    src/doors/syncduke/CMakeLists.txt diff
    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_config.c diff
    src/doors/syncduke/syncduke_door.c diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/syncduke/tests/test_keymap.c diff
    syncduke: debug log, crash handler, and resilient door I/O Add an optional file debug log (syncduke_log.c), independent of the BBS's stderr/syslog capture -- a door that drops back to the BBS otherwise leaves nothing logged, and WER minidumps aren't configured on this host: - enabled via syncduke.ini [debug] log, env SYNCDUKE_LOG, or -log <path>; a relative path lands in each player's per-user dir. Timestamped, flushed per line so a crash/hangup still leaves the tail on disk. - installs a last-resort crash handler (SetUnhandledExceptionFilter on Windows, SIGSEGV/etc on *nix) that records the fault to the log, plus an atexit marker -- so hangups, clean exits, and crashes are all captured. - hangup() and the send/recv error paths log the reason incl. WSA codes. Make transient socket errors non-fatal: send()/recv() returning WSAENOBUFS or WSAEINTR are backpressure (retry next tick), not a dead client. A burst of large frames (e.g. holding a key, which defeats the frame de-dupe) can surface WSAENOBUFS, which previously dropped the player back to the BBS. Also: clamp the mouse-sensitivity slider floor to 1 (0 = no turn at all), and wire the git build-info (synchronet_gitinfo) + the syncduke_record_enabled / [game] record plumbing used by the menu and demo changes that follow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  58. Rob Swindell (on Debian Linux)
    Sat Jun 27 2026 00:09:42 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/salib.js diff
    salib.js: send spamd message from a single read, not file_size()+sendfile() The Content-length announced to spamd was computed from file_size() while the body was delivered separately via sendfile() -- two operations on the same temp file. When that file lives on a soft network mount (e.g. a CIFS loopback), a stat and a later send can disagree under load: the body short-reads while the size call already returned the full length, so spamd rejects the message with "Content-Length mismatch ... protocol error 76" and spamc.js passes it through unscanned. Read the file once into memory and derive Content-length from the bytes actually sent, so the two can never disagree (a short read just scans a shorter message rather than erroring). spamc.js already caps message size, so reading into memory is bounded. Surfaced after enabling mailproc ProcessDNSBL processing tripled the volume flowing through spamc.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  59. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 23:16:05 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/deploy.bat diff
    src/doors/syncdoom/deploy.sh diff
    src/doors/syncduke/deploy.bat diff
    src/doors/syncduke/deploy.sh diff
    Modified Files:

    src/doors/syncdoom/COMPILING.md diff
    src/doors/syncdoom/build.bat diff
    src/doors/syncdoom/build.sh diff
    src/doors/syncduke/README.md diff
    src/doors/syncduke/build.bat diff
    src/doors/syncduke/build.sh diff
    syncduke/syncdoom: split deploy out of the build into deploy.sh/deploy.bat Building no longer auto-installs the binary into a live xtrn dir -- build.sh / build.bat now only produce build/<door> (build-msvc\Release\<door>.exe on Windows), and a new deploy.sh / deploy.bat carries it to the door's xtrn dir(s) on demand. A sysop can rebuild and test before pushing a new binary to a running BBS. This also fixes a 0-byte-binary bug on SYMLINK=1 installs where the live xtrn/<door>/<door> is a server-side symlink back to the build output, exposed over an SMB mount as a plain file on a different device. The old in-build `cp -f "$EXE" live/<door>` had source and dest resolve to the same physical file, but `-ef` and cp's same-file guard compare st_dev/st_ino -- which differ across the mount -- so cp opened the dest O_TRUNC and zeroed the build output to 0 bytes before reading a byte. deploy.sh guards against it two ways: skip when the dest already has identical content (catches the cross-mount self-reference -ef misses, and preserves the symlink), and otherwise copy via a temp file + atomic rename so the source can never be the O_TRUNC victim. deploy.bat skips via `fc /b`. Docs (syncduke README, syncdoom COMPILING) updated for the two-step flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  60. Rob Swindell (on Windows 11)
    Fri Jun 26 2026 22:40:03 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Engine/src/filesystem.c diff
    syncduke: load the GRP into RAM so lump reads don't round-trip over SMB The Build engine read each GRP lump via unbuffered 2-4 byte read()s (kread16/32/8 -> kread -> lseek+read), relying on the OS file cache to make them cheap. That holds on local disk but not over a network share: on an SMB-mounted install every tiny read round-trips to the file server, so door startup (CON compile, art/palette load) took ~13.7s vs ~3.2s on local disk. Slurp the read-only GRP into RAM once at initgroupfile() (folding in the existing CRC32 pass) and serve each lump kread() via memcpy; fall back to the fd path if the allocation fails. The original authors clearly intended this (the commented-out groupfil_memory malloc and the "caches the GRP in memory" note). SMB startup now matches local disk (~3.2s); local is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  61. Rob Swindell (on Windows 11)
    Fri Jun 26 2026 22:01:14 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/game.c diff
    src/doors/syncduke/syncduke_config.c diff
    syncduke: fix two Windows door startup failures (setvbuf, GRP dir) Both killed the Win32 door at launch -- it died straight back to the BBS after the "Loading..." splash; found while bringing up the MSVC build. 1. syncduke_config.c: setvbuf(stdout, NULL, _IOLBF, 0) (used to route the engine's diagnostics to the BBS log) is fine on glibc, but MSVC's CRT rejects _IOLBF with size 0 (it requires size >= 2) -> invalid-parameter fast-fail (c0000409), confirmed from the WER minidump. Use _IONBF on Windows (unbuffered, valid with size 0, flushes immediately); *nix keeps _IOLBF. 2. Game/src/game.c: the Windows findGRPToUse() never consulted syncduke_grpdir (only the *nix branch did), so with -home (CWD = the per-user dir) it scanned the wrong directory and exited with "Can't find 'duke3d*.grp'". Honor syncduke_grpdir first, mirroring the *nix branch, so the GRP opens by absolute path regardless of CWD. Verified on Windows via an inherited-socket harness matching the live launch (-s<socket> -home <userdir>): loads DUKE3D.GRP by absolute path and renders (sixel and JXL tiers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  62. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 18:53:24 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/build.sh diff
    src/doors/syncduke/build.sh diff
    syncduke/syncdoom: build.sh deploys to the live install, not just the repo A sysop reported re-running build.sh without his live door binary updating. build.sh derived its destination purely from the script's own location (../../../xtrn/<door>), which is the REPO's xtrn -- correct only when the live xtrn is the repo's (the recommended SYMLINK=1 *nix install, where xtrn -> repo/xtrn). On a COPY-style install the live xtrn is a separate directory the in-tree copy never reaches, so the running door kept the old binary. The binary is a git-ignored build artifact, so unlike the tracked door files it is not carried to a live install by `git pull` / `update.js` -- build.sh is what must deliver it. Deploy to the in-tree bundle AND, when it is a distinct directory, the live install -- located via $SBBSCTRL (install root = $SBBSCTRL/..) or an explicit $SYNCDUKE_DEST / $SYNCDOOM_DEST override. Same-directory targets (symlinked or build-in-place installs) are detected with -ef and skipped, and the deployed path is printed so a wrong guess/fallback is visible. The -ef guard also fixes a latent abort: cp -f onto a dev symlink (xtrn/<door>/<door> -> build/<door>) is a same-file copy that errors and, under `set -e`, aborted the whole script. Validated for both doors across three layouts: in-tree-only (SBBSCTRL unset), copy install (separate live dir gets the binary, verified byte-identical), and symlinked install (live == in-tree, skipped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  63. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 17:57:42 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncduke/Game/src/game.c diff
    src/doors/syncduke/syncduke_door.c diff
    syncduke: strip door args before the engine's command-line parser A sysop reported SyncDuke dumping the Duke3D command-line help to syslog and never starting. The vendored engine's DOS-era checkcommandline() treats any argv element beginning with '/' as a "/option" and, on an unrecognized one, prints comlinehelp() and exits. On a Unix host the DOOR32.SYS drop-file path (Synchronet's %f) and the -home/-grpdir values are ABSOLUTE paths starting with '/', so the engine misreads them -- e.g. "/home/.../door32.sys" parses as option '/h' -> unknown -> help+exit. It only "worked" when the install path's first letter happened to be a handled option letter (e.g. /sbbs -> 's', which is the skill flag and survives -- as a side effect forcing player skill 0). And it surfaced only once the user had a GRP: GRP init runs before checkcommandline and exits early when no GRP is present, so a GRP-less install hits the GRP error first, masking this. Fix: syncduke_sanitize_cmdline() compacts argv to remove the door's own args (DOOR32.SYS path, -home, -grpdir, -s<fd>) just before checkcommandline, handing the engine an engine-only argv. All of these are already consumed by the pre-main constructors, so stripping them is safe. Path-independent, and it also eliminates the incidental skill-0 side effect. Reproduced (GRP present, /home launch path -> help dump) and verified fixed (loads the GRP, proceeds into init, no help dump); /sbbs path unregressed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  64. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 17:25:55 GMT-0700 (PDT)
    Added Files:
    

    xtrn/syncduke/download.js diff
    Modified Files:

    src/doors/syncduke/.gitignore diff
    xtrn/syncduke/install-xtrn.ini diff
    syncduke: optional installer download of the shareware DUKE3D.GRP SyncDuke ships no game data, so a fresh install has no GRP to run. Add an optional installer step that fetches the freely-redistributable shareware Episode 1 GRP so the door is playable out of the box -- mirroring SyncDOOM's getwads.js Freedoom fetch. download.js downloads 3D Realms' original shareware install package and extracts DUKE3D.GRP from it. The GRP is two archive layers deep (3dduke13.zip -> DN3DSW13.SHR self-extractor -> DUKE3D.GRP); Synchronet's Archive (libarchive) reads both, and the result is byte-identical to the canonical 11,035,779-byte "SHAREWARE 1.3D" GRP. Idempotent (skips a GRP already present), non-fatal if offline, and SpiderMonkey 1.8.5-compatible. install-xtrn.ini runs it as a prompted, non-required [exec:download.js] step after the config copy, so it can read the [grp] dir from syncduke.ini. Named download.js (not getgrp/getmaps) so it can grow more options later (map packs, the registered GRP) once the door can load them -- today it only fetches the base game GRP. .gitignore also gains *.grp so a dropped-in GRP is never accidentally committed (it is proprietary game data, not ours to redistribute from the repo even though the shareware package is). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  65. Deucе
    Fri Jun 26 2026 12:57:39 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    Fix functions that were namespaced in v1.10
  66. Deucе
    Fri Jun 26 2026 10:16:44 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Actually hook up lf_expand
  67. Deucе
    Fri Jun 26 2026 10:16:44 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    src/syncterm/bbslist.c diff
    src/syncterm/bbslist.h diff
    Add LF Expand option to dialing directory Useful for embedded work and such... not really useful for BBSing I hope.
  68. Deucе
    Fri Jun 26 2026 10:16:44 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    Add lf_expand option Expands an LF to CRLF pair
  69. Rob Swindell (on Windows 11)
    Fri Jun 26 2026 10:06:21 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncduke/build.bat diff
    src/doors/syncduke/compat/strings.h diff
    src/doors/syncduke/vcpkg.json diff
    Modified Files:

    src/doors/syncduke/.gitignore diff
    src/doors/syncduke/CMakeLists.txt diff
    src/doors/syncduke/README.md diff
    src/doors/syncduke/syncduke_config.c diff
    src/doors/syncduke/syncduke_door.c diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/syncduke/syncduke_plat.c diff
    syncduke: Win32/MSVC build (build.bat, vcpkg.json, cross-platform shim I/O) Following src/doors/syncdoom, add the files to build SyncDuke for Win32: - build.bat + vcpkg.json: VS 2022 configure/build/install, static libjxl via classic-mode vcpkg (falls back to sixel/text tiers when absent). - CMakeLists.txt: PLATFORM_WIN32 on Windows; guard the GCC-only options under NOT MSVC and add MSVC equivalents (/w, CRT/Winsock deprecation defines, /FORCE:MULTIPLE for the engine tentative-global + xpdev dup symbols, premap.c /Od, ws2_32 + winmm). /FIinttypes.h plus _MSC_STDINT_H_/_MSC_INTTYPES_H_ neutralize the vendored msinttypes shims that clash with MSVC 2022's native headers. - compat/strings.h: satisfies the vendored filesystem.c <strings.h> include on MSVC (maps strcasecmp); #include_next passthrough on *nix. - Shim sources (door/config/io/input/plat): _WIN32 paths using Winsock send/recv/ioctlsocket, QueryPerformanceCounter clocks, Sleep, _fullpath/_stricmp, and .CRT$XCU constructors (reading __argc/__argv) in place of glibc __attribute__((constructor)). input.c/door.c stay xpdev-free so the keymap unit test still links standalone; the *nix paths are unchanged. Verified: builds under MSVC (Win32 x86 console syncduke.exe) and on Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  70. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/mail_dkim.c diff
    src/sbbs3/mail_dkim.h diff
    Modified Files:

    docs/v322_new.md diff
    src/sbbs3/GNUmakefile diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/mailsrvr.h diff
    src/sbbs3/mailsrvr.vcxproj diff
    src/sbbs3/mime.c diff
    src/sbbs3/mime.h diff
    src/sbbs3/objects.mk diff
    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgindex.h diff
    src/sbbs3/scfg/scfgsrvr.c diff
    mailsrvr: DKIM signing of outbound mail (issue #215) Sign outbound messages with a relaxed/relaxed rsa-sha256 DKIM-Signature header so receivers can authenticate them. Enabled via the [Mail] DKIMSign / DKIMDomain / DKIMSelector keys (and the SCFG SendMail Support menu); the RSA private key is loaded from ctrl/dkim_<selector>.pem and the matching public key is published in DNS by the sysop. New mail_dkim.{c,h}: streaming relaxed body hash, relaxed header canonicalization, DKIM-Signature assembly, and OpenSSL EVP signing (cryptlib cannot emit a raw PKCS#1 signature). This is an OpenSSL-only feature, gated on pkg-config libcrypto; without it the entry points compile to no-op stubs and mail goes out unsigned. Integration in sendmail_thread uses a two-pass render: pass 1 captures the message via a thread-local observer on sockprintf (nothing transmitted) to compute the body hash and collect the signed headers, then the DKIM-Signature is prepended and pass 2 transmits while re-hashing the body, aborting on a mismatch. mimegetboundary() gains a seed so both passes produce an identical MIME boundary; the MSG_KILLFILE attachment removal and the msg.subj mutation are guarded/saved so the capture pass has no side effects. POP3 retrieval and the DKIM-disabled path are byte-for-byte unchanged. SCFG gains DKIM Signing / Domain / Selector options under Mail Server -> SendMail Support (scfgindex.h regenerated). Live-validated on mail.synchro.net: Gmail reports dkim=pass, and dmarc=pass for a *.synchro.net From address (relaxed alignment against a single d=synchro.net key). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit d28f2990942da53bfc96e3ed84daeac8277a41e1)
  71. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Added Files:
    

    3rdp/build/cl-visibility.patch diff
    Modified Files:

    3rdp/build/GNUmakefile diff
    cryptlib: hide vendored OpenSSL symbols so libcrypto can coexist cryptlib's libcl.a bundles an ancient OpenSSL and exports ~139 OpenSSL-namespace globals (BN_*, MD5_*, SHA*, RSA_*, sanityCheckBignum, ...). Statically linked into libsbbs.so with those symbols global, they interpose a separately-linked libcrypto: e.g. EVP_RSA_gen's internal BN_free binds to cryptlib's BN_free, which is then handed an OpenSSL BIGNUM of incompatible layout -> crash in sanityCheckBignum. (libcrypto was previously only a transitive dependency via libmosquitto and never called by our own code, so this was latent.) New cl-visibility.patch compiles cryptlib with -fvisibility=hidden and decorates its public C_RET API with visibility("default") -- gated on __GNUC__ && _CRYPT_DEFINED, mirroring the existing Windows dllexport split -- so only the crypt* API is exported and the vendored OpenSSL symbols become local. Verified: crypt* stays exported (the server .so modules still resolve it), BN_*/MD5_*/sanityCheckBignum are hidden, and a full release relink is clean. Enables direct libcrypto use in the mail server (DKIM signing) and closes the latent interposition risk against mosquitto's libcrypto. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 57656a4c55e84faefcdcca66cebf8df65fc4a8c6)
  72. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Modified Files:
    

    exec/lbshell.js diff
    lbshell.js: beep instead of crash on stray keys in the xtrn menu When a keystroke that isn't a valid menu item leaks through the lightbar getval() in the external-programs ('x') handler, parseInt() of it yields NaN or an out-of-range index, and the code dereferenced the array element without checking it existed: - Section selection fed parseInt(x_sec) straight into new Xtrnsec(), whose constructor immediately reads xtrn_area.sec_list[sec].prog_list -> the "sec_list[sec] is undefined" TypeError seen in error.log. - Program selection's own guard was the crash: it read .number off prog_list[parseInt(x_prog)] which was undefined. Validate the section index before constructing the submenu, and look the program up once with a null-check before dereferencing. A stray keystroke now just beeps instead of throwing and dropping to the default shell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 074785210f01bdc40ecff0782984c7cfe02a5330)
  73. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Modified Files:
    

    docs/v322_new.md diff
    exec/load/salib.js diff
    exec/spamc.js diff
    spamc.js/salib.js: let SpamAssassin see the originating relay IP Enable the synthetic Received-header injection (set msg.hello_name from the mailproc hello_name global) so spamd can identify the connecting client and run sender-IP DNSBLs (Spamhaus, etc.) and SPF -- previously every message scored with NO_RELAYS/NO_RECEIVED, neutering those checks. Strip the synthetic Received from the re-written message so the stored mail doesn't duplicate the Received the mail server adds at delivery (the duplicate that caused this to be disabled in f886a41 / only-3-strip). Strip it on its own: SA consumes the injected Return-Path, so a combined match never hit. Validated live: NO_RELAYS/NO_RECEIVED gone, SPF_PASS + RCVD_IN_* now firing, stored messages carry exactly one Received header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> (cherry picked from commit 567c45486b2cb958d70a2fabe2bd9f29e2c8ade0)
  74. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncduke/.gitignore diff
    src/doors/syncduke/CMakeLists.txt diff
    src/doors/syncduke/README.md diff
    src/doors/syncduke/build.sh diff
    src/doors/syncduke/compat/SDL.h diff
    src/doors/syncduke/syncduke.h diff
    src/doors/syncduke/syncduke_config.c diff
    src/doors/syncduke/syncduke_door.c diff
    src/doors/syncduke/syncduke_game.c diff
    src/doors/syncduke/syncduke_input.c diff
    src/doors/syncduke/syncduke_io.c diff
    src/doors/syncduke/syncduke_plat.c diff
    src/doors/syncduke/syncduke_stubs.c diff
    src/doors/syncduke/tests/save_load_test.sh diff
    src/doors/syncduke/tests/test_keymap.c diff
    xtrn/syncduke/.gitignore diff
    xtrn/syncduke/install-xtrn.ini diff
    xtrn/syncduke/syncduke.example.ini diff
    Modified Files:

    src/doors/syncduke/Engine/src/draw.c diff
    src/doors/syncduke/Engine/src/draw.h diff
    src/doors/syncduke/Engine/src/engine.c diff
    src/doors/syncduke/Engine/src/unix_compat.h diff
    src/doors/syncduke/Game/src/actors.c diff
    src/doors/syncduke/Game/src/config.c diff
    src/doors/syncduke/Game/src/duke3d.h diff
    src/doors/syncduke/Game/src/game.c diff
    src/doors/syncduke/Game/src/gamedef.c diff
    src/doors/syncduke/Game/src/global.c diff
    src/doors/syncduke/Game/src/menues.c diff
    src/doors/syncduke/Game/src/player.c diff
    src/doors/syncduke/Game/src/premap.c diff
    src/doors/syncduke/SEAM.md diff
    syncduke: headless door shim -- single-player Duke Nukem 3D over a BBS terminal Replace Chocolate Duke3D's SDL video/input/timer back end with a headless shim (syncduke_*.c/h) that captures the Build engine's 8-bit framebuffer and renders it to the user's terminal via termgfx -- auto-selecting JPEG XL (on SyncTERM), sixel, or the text/block fallback (so even a no-sixel terminal like conhost works), with F4 to cycle tiers and DSR-ack auto-depth pacing. Includes the 64-bit (LP64) port of the vendored engine (CON VM + renderer pointer widening), DOOR32.SYS client-socket I/O, the SyncDOOM-style control scheme + mouse steering + Controls Help, per-user save/config dirs (-home), 64-bit-safe save/load, the build script + installer + example config, and a keymap unit test. v1 is SINGLE-PLAYER ONLY (no multiplayer/lobby/audio yet), shareware Episode 1. See README.md / SEAM.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  75. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncduke/CHOCOLATE_DUKE3D_README.md diff
    src/doors/syncduke/DESIGN.md diff
    src/doors/syncduke/Engine/src/Makefile.am diff
    src/doors/syncduke/Engine/src/build.h diff
    src/doors/syncduke/Engine/src/cache.c diff
    src/doors/syncduke/Engine/src/cache.h diff
    src/doors/syncduke/Engine/src/display.c diff
    src/doors/syncduke/Engine/src/display.h diff
    src/doors/syncduke/Engine/src/draw.c diff
    src/doors/syncduke/Engine/src/draw.h diff
    src/doors/syncduke/Engine/src/dummy_multi.c diff
    src/doors/syncduke/Engine/src/engine.c diff
    src/doors/syncduke/Engine/src/engine.h diff
    src/doors/syncduke/Engine/src/filesystem.c diff
    src/doors/syncduke/Engine/src/filesystem.h diff
    src/doors/syncduke/Engine/src/fixedPoint_math.c diff
    src/doors/syncduke/Engine/src/fixedPoint_math.h diff
    src/doors/syncduke/Engine/src/icon.h diff
    src/doors/syncduke/Engine/src/macos_compat.h diff
    src/doors/syncduke/Engine/src/mmulti.c diff
    src/doors/syncduke/Engine/src/mmulti.cpp diff
    src/doors/syncduke/Engine/src/mmulti_stable.cpp diff
    src/doors/syncduke/Engine/src/mmulti_stable.h diff
    src/doors/syncduke/Engine/src/mmulti_unstable.h diff
    src/doors/syncduke/Engine/src/multi.c diff
    src/doors/syncduke/Engine/src/network.c diff
    src/doors/syncduke/Engine/src/network.h diff
    src/doors/syncduke/Engine/src/platform.h diff
    src/doors/syncduke/Engine/src/tiles.c diff
    src/doors/syncduke/Engine/src/tiles.h diff
    src/doors/syncduke/Engine/src/unix_compat.h diff
    src/doors/syncduke/Engine/src/win32_compat.h diff
    src/doors/syncduke/Engine/src/windows/inttypes.h diff
    src/doors/syncduke/Engine/src/windows/stdint.h diff
    src/doors/syncduke/Game/src/DbgHelp.h diff
    src/doors/syncduke/Game/src/Makefile.am diff
    src/doors/syncduke/Game/src/_functio.h diff
    src/doors/syncduke/Game/src/_rts.h diff
    src/doors/syncduke/Game/src/actors.c diff
    src/doors/syncduke/Game/src/animlib.c diff
    src/doors/syncduke/Game/src/animlib.h diff
    src/doors/syncduke/Game/src/audiolib/Makefile.am diff
    src/doors/syncduke/Game/src/audiolib/_al_midi.h diff
    src/doors/syncduke/Game/src/audiolib/_blaster.h diff
    src/doors/syncduke/Game/src/audiolib/_guswave.h diff
    src/doors/syncduke/Game/src/audiolib/_midi.h diff
    src/doors/syncduke/Game/src/audiolib/_multivc.h diff
    src/doors/syncduke/Game/src/audiolib/_pas16.h diff
    src/doors/syncduke/Game/src/audiolib/_sndscap.h diff
    src/doors/syncduke/Game/src/audiolib/adlibfx.c diff
    src/doors/syncduke/Game/src/audiolib/adlibfx.h diff
    src/doors/syncduke/Game/src/audiolib/al_midi.c diff
    src/doors/syncduke/Game/src/audiolib/al_midi.h diff
    src/doors/syncduke/Game/src/audiolib/assert.h diff
    src/doors/syncduke/Game/src/audiolib/awe32.c diff
    src/doors/syncduke/Game/src/audiolib/awe32.h diff
    src/doors/syncduke/Game/src/audiolib/blaster.c diff
    src/doors/syncduke/Game/src/audiolib/blaster.h diff
    src/doors/syncduke/Game/src/audiolib/ctaweapi.h diff
    src/doors/syncduke/Game/src/audiolib/debugio.c diff
    src/doors/syncduke/Game/src/audiolib/debugio.h diff
    src/doors/syncduke/Game/src/audiolib/dma.c diff
    src/doors/syncduke/Game/src/audiolib/dma.h diff
    src/doors/syncduke/Game/src/audiolib/dpmi.c diff
    src/doors/syncduke/Game/src/audiolib/dpmi.h diff
    src/doors/syncduke/Game/src/audiolib/dsl.c diff
    src/doors/syncduke/Game/src/audiolib/dsl.h diff
    src/doors/syncduke/Game/src/audiolib/fx_man.c diff
    src/doors/syncduke/Game/src/audiolib/fx_man.h diff
    src/doors/syncduke/Game/src/audiolib/gmtimbre.c diff
    src/doors/syncduke/Game/src/audiolib/gus.c diff
    src/doors/syncduke/Game/src/audiolib/gusmidi.c diff
    src/doors/syncduke/Game/src/audiolib/gusmidi.h diff
    src/doors/syncduke/Game/src/audiolib/guswave.c diff
    src/doors/syncduke/Game/src/audiolib/guswave.h diff
    src/doors/syncduke/Game/src/audiolib/interrup.h diff
    src/doors/syncduke/Game/src/audiolib/irq.c diff
    src/doors/syncduke/Game/src/audiolib/irq.h diff
    src/doors/syncduke/Game/src/audiolib/leetimbr.c diff
    src/doors/syncduke/Game/src/audiolib/linklist.h diff
    src/doors/syncduke/Game/src/audiolib/ll_man.c diff
    src/doors/syncduke/Game/src/audiolib/ll_man.h diff
    src/doors/syncduke/Game/src/audiolib/memcheck.h diff
    src/doors/syncduke/Game/src/audiolib/midi.c diff
    src/doors/syncduke/Game/src/audiolib/midi.h diff
    src/doors/syncduke/Game/src/audiolib/mpu401.c diff
    src/doors/syncduke/Game/src/audiolib/mpu401.h diff
    src/doors/syncduke/Game/src/audiolib/multivoc.c diff
    src/doors/syncduke/Game/src/audiolib/multivoc.h diff
    src/doors/syncduke/Game/src/audiolib/music.c diff
    src/doors/syncduke/Game/src/audiolib/music.h diff
    src/doors/syncduke/Game/src/audiolib/mv_mix.asm diff
    src/doors/syncduke/Game/src/audiolib/mv_mix.c diff
    src/doors/syncduke/Game/src/audiolib/mv_mix16.asm diff
    src/doors/syncduke/Game/src/audiolib/mvreverb.asm diff
    src/doors/syncduke/Game/src/audiolib/mvreverb.c diff
    src/doors/syncduke/Game/src/audiolib/myprint.c diff
    src/doors/syncduke/Game/src/audiolib/myprint.h diff
    src/doors/syncduke/Game/src/audiolib/newgf1.h diff
    src/doors/syncduke/Game/src/audiolib/nodpmi.c diff
    src/doors/syncduke/Game/src/audiolib/nomusic.c diff
    src/doors/syncduke/Game/src/audiolib/pas16.c diff
    src/doors/syncduke/Game/src/audiolib/pas16.h diff
    src/doors/syncduke/Game/src/audiolib/pitch.c diff
    src/doors/syncduke/Game/src/audiolib/pitch.h diff
    src/doors/syncduke/Game/src/audiolib/sndcards.h diff
    src/doors/syncduke/Game/src/audiolib/sndscape.c diff
    src/doors/syncduke/Game/src/audiolib/sndscape.h diff
    src/doors/syncduke/Game/src/audiolib/sndsrc.c diff
    src/doors/syncduke/Game/src/audiolib/sndsrc.h diff
    src/doors/syncduke/Game/src/audiolib/standard.h diff
    src/doors/syncduke/Game/src/audiolib/task_man.c diff
    src/doors/syncduke/Game/src/audiolib/task_man.h diff
    src/doors/syncduke/Game/src/audiolib/user.c diff
    src/doors/syncduke/Game/src/audiolib/user.h diff
    src/doors/syncduke/Game/src/audiolib/usrhooks.c diff
    src/doors/syncduke/Game/src/audiolib/usrhooks.h diff
    src/doors/syncduke/Game/src/audiolib/util.h diff
    src/doors/syncduke/Game/src/config.c diff
    src/doors/syncduke/Game/src/config.h diff
    src/doors/syncduke/Game/src/console.c diff
    src/doors/syncduke/Game/src/console.h diff
    src/doors/syncduke/Game/src/control.c diff
    src/doors/syncduke/Game/src/control.h diff
    src/doors/syncduke/Game/src/cvar_defs.c diff
    src/doors/syncduke/Game/src/cvar_defs.h diff
    src/doors/syncduke/Game/src/cvars.c diff
    src/doors/syncduke/Game/src/cvars.h diff
    src/doors/syncduke/Game/src/develop.h diff
    src/doors/syncduke/Game/src/duke3d.h diff
    src/doors/syncduke/Game/src/dukeunix.h diff
    src/doors/syncduke/Game/src/dukewin.h diff
    src/doors/syncduke/Game/src/dummy_audiolib.c diff
    src/doors/syncduke/Game/src/file_lib.h diff
    src/doors/syncduke/Game/src/funct.h diff
    src/doors/syncduke/Game/src/function.h diff
    src/doors/syncduke/Game/src/game.c diff
    src/doors/syncduke/Game/src/game.h diff
    src/doors/syncduke/Game/src/gamedef.c diff
    src/doors/syncduke/Game/src/gamedefs.h diff
    src/doors/syncduke/Game/src/global.c diff
    src/doors/syncduke/Game/src/global.h diff
    src/doors/syncduke/Game/src/joystick.h diff
    src/doors/syncduke/Game/src/keyboard.c diff
    src/doors/syncduke/Game/src/keyboard.h diff
    src/doors/syncduke/Game/src/menues.c diff
    src/doors/syncduke/Game/src/midi/Makefile.am diff
    src/doors/syncduke/Game/src/midi/sdl_midi.c diff
    src/doors/syncduke/Game/src/mouse.h diff
    src/doors/syncduke/Game/src/names.h diff
    src/doors/syncduke/Game/src/player.c diff
    src/doors/syncduke/Game/src/premap.c diff
    src/doors/syncduke/Game/src/premap.h diff
    src/doors/syncduke/Game/src/rts.c diff
    src/doors/syncduke/Game/src/rts.h diff
    src/doors/syncduke/Game/src/scriplib.c diff
    src/doors/syncduke/Game/src/scriplib.h diff
    src/doors/syncduke/Game/src/sector.c diff
    src/doors/syncduke/Game/src/sounddebugdefs.h diff
    src/doors/syncduke/Game/src/soundefs.h diff
    src/doors/syncduke/Game/src/sounds.c diff
    src/doors/syncduke/Game/src/sounds.h diff
    src/doors/syncduke/Game/src/types.h diff
    src/doors/syncduke/Game/src/util_lib.h diff
    src/doors/syncduke/PLAN.md diff
    src/doors/syncduke/SEAM.md diff
    syncduke: vendor pristine Chocolate Duke3D snapshot + design docs Pinned upstream Chocolate Duke3D (Duke Nukem 3D source (c) 3D Realms + the Build engine (c) Ken Silverman), GPLv2, unmodified, plus the v1 design spec, implementation plan, and engine/platform seam notes. The headless door shim and our engine patches land in the next commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  76. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: serve the JXL and text tiers via termgfx Use the extracted termgfx encoders/transport (jxl, apc, caps, text) instead of in-tree copies; emit_cached_image / emit_frame_jxl become thin wrappers and the text tier renders via termgfx's rt_render_frame. CMake propagates WITH_JXL + the libjxl include to the termgfx target. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  77. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/README.md diff
    src/doors/termgfx/apc.c diff
    src/doors/termgfx/apc.h diff
    src/doors/termgfx/caps.c diff
    src/doors/termgfx/caps.h diff
    src/doors/termgfx/jxl.c diff
    src/doors/termgfx/jxl.h diff
    src/doors/termgfx/term.c diff
    src/doors/termgfx/term.h diff
    src/doors/termgfx/text.c diff
    src/doors/termgfx/text.h diff
    Modified Files:

    src/doors/termgfx/CMakeLists.txt diff
    src/doors/termgfx/sixel.c diff
    src/doors/termgfx/sixel.h diff
    termgfx: add JPEG XL, APC transport, cap-probe, and text-block tiers Extend the shared door-graphics library beyond sixel: a libjxl encoder (jxl.c, behind WITH_JXL), the SyncTERM APC cached-image transport (apc.c), the Q;JXL capability cap-probe (caps.c), and the ANSI text/block-character render tiers (text.c, moved from syncdoom/render_text.c and made framebuffer- source-agnostic). All I/O-free: the encoders build bytes, the door sends them. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  78. Rob Swindell (on Debian Linux)
    Fri Jun 26 2026 01:23:36 GMT-0700 (PDT)
    Added Files:
    

    src/doors/termgfx/CMakeLists.txt diff
    src/doors/termgfx/sbbs_node.c diff
    src/doors/termgfx/sbbs_node.h diff
    src/doors/termgfx/sixel.c diff
    src/doors/termgfx/sixel.h diff
    Modified Files:

    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: extract reusable bits into shared libtermgfx First slice of the door-library extraction: move the two already-standalone, engine-agnostic modules out of the SyncDOOM door into a new shared static library, src/doors/termgfx/, so future framebuffer doors (e.g. a Duke Nukem 3D port) can link them instead of copy/pasting. - sixel.{c,h} -- the indexed DECSIXEL encoder (was render_sixel) - sbbs_node.{c,h} -- door-native node list / paging / who's-online Files in the dedicated termgfx/ dir carry no redundant prefix (the directory is the namespace); the lib keeps sbbs_node's meaningful prefix. libtermgfx exposes its own dir as a PUBLIC include (so the door gets the headers) and keeps the sbbs3/xpdev header paths sbbs_node needs PRIVATE (headers only -- xpdev symbols still resolve at the door's final link). SyncDOOM drops the two sources + the now-orphaned sbbs3 include and links the lib. No logic change; builds clean. More of syncdoom.c's renderer/pacing/caps/overlay/input will move into termgfx behind a termgfx.h engine interface over time (exported symbols there prefixed termgfx_). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  79. Rob Swindell (on Windows 11)
    Thu Jun 25 2026 17:54:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/filterfile.hpp diff
    src/sbbs3/findstr.c diff
    src/sbbs3/findstr.h diff
    Fix Win32 debug-heap assertion freeing cached filter lists (GitLab #1099) filterFile::listed() (and ~filterFile/reset()) freed its cached str_list with strListFree(), which links locally into each server module (xpdev is statically linked), but built it with findstr_list(), which is dllimport'd from sbbs.dll. So the list was allocated in sbbs.dll's CRT heap and freed in the server module's own /MTd CRT heap. Each statically-linked CRT keeps a per-module debug block list, so the freeing module's _free_dbg_nolock can't find the block and asserts at debug_heap.cpp:996. This is why the issue was debug/Windows-only and undetectable by App Verifier: in release the UCRT heap is GetProcessHeap() (shared across modules) so the cross-module free actually succeeds, and non-Windows shares one libc heap -- only the Win32 debug CRT's per-module accounting notices. It was also reproducible single-threaded (deterministic heap mismatch, not a race) and only via findstr_list() (the lone allocation crossing the DLL boundary). Affects every filter object (ip_can, ip_silent_can, host_can, host_exempt) in every server, since filterFile is a header-only class compiled into each module. Present since the class was introduced in cd30ec58a (press-5-filled). Pair findstr_list() with a findstr_list_free() exported from the same translation unit (sbbs.dll), and call it from all three filterFile free sites so allocation and deallocation share one heap. This also lets the strListFree() that #1099 had to comment out of reset() be restored (now under the object mutex, since reset() runs in the shutdown path where a late client thread may still be in listed()). Verified: rebuilt services.dll imports both findstr_list and findstr_list_free from sbbs.dll. Built clean on Win32/Release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  80. Rob Swindell (on Windows 11)
    Wed Jun 24 2026 22:22:54 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_global.cpp diff
    src/sbbs3/js_internal.cpp diff
    Fix crash/regression in require() when a required symbol is undefined js_IsTerminated() was reading its object argument's JS private slot as a js_callback_t*, but the only caller (js_require) passes the execution scope -- for a top-level require() that's the global object, whose private is a global_private_t, not a js_callback_t. Reinterpreting it walked garbage: js_callback_t::terminated (offset 16) lands in the middle of global_private_t and ::bg lands past its end, so the result is undefined. In jsexec those bytes happened to dereference truthy; in sbbsctrl they were the unmapped pointer 0x000007fc, faulting with an access violation in JS_TriggerAllOperationCallbacks' call chain and taking down the Control Panel (WER minidump 2026-06-24). The condition was also inverted: generating-12-calm (c185d884eb) uncommented urge-22-door's (78b2682ec8, gitlab #681) stubbed `if (TRUE) { //!js_IsTerminated(...)` as `if (js_IsTerminated(...))`, dropping the `!`. The two defects cancelled in the common case (garbage returned true, so the inverted test still printed the error), which is why it went unnoticed -- but it meant #681's actual purpose, suppressing the "symbol not defined" error during termination, never worked, and on Windows it crashed instead. Fix js_IsTerminated() to walk the scope chain to the internal "js" object (mirroring js_execfile's lookup) before reading the js_callback_t, and restore the `!` so the error is reported unless the script is genuinely terminating. Verified: require() of a defined symbol succeeds, require() of an undefined symbol throws "symbol 'x' not defined by script 'y'", and no crash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  81. Rob Swindell (on Windows 11)
    Wed Jun 24 2026 01:09:28 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/xtrn.cpp diff
    src/xpdev/multisock.c diff
    Don't leak server listen sockets to spawned child processes (Windows) On Windows, sockets are inheritable by default and the timed-event / native external CreateProcess() path passed bInheritHandles=TRUE unconditionally, so every spawned child (a jsexec timed event, a native door, a Web Server CGI) inherited open handles to all server listen sockets. A long-lived child (e.g. a chat_llm_irc.js timed event) would then keep those sockets bound in the kernel after the parent (sbbsctrl/sbbscon) exited, leaving a ghost process that owned every Synchronet port and silently dropped incoming connections of all protocols. Two complementary fixes: - multisock.c: mark every listen socket (and accepted client socket) non-inheritable via SetHandleInformation(HANDLE_FLAG_INHERIT, 0) right after creation. This is the shared listen-socket path for all servers (Terminal/Mail/FTP/Web/Services), so children can no longer inherit a listen socket regardless of any CreateProcess inheritance flag. - xtrn.cpp external(): only pass bInheritHandles=TRUE when the child actually needs to inherit a handle we're sharing - the redirected stdio pipes (use_pipes) or the duplicated passthru/client socket that a native socket-door talks over. A timed event running jsexec shares neither and now gets FALSE. DOS doors communicate via named mailslots/events and need no inheritance. Fixes #1151. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  82. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 23:33:57 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/lobby.msg diff
    xtrn/syncdoom/syncdoom.example.ini diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom lobby: live who's-online/activity panel + activity log Add an opt-in live lobby ([lobby] live): a bottom-anchored panel that shows who's online (Synchronet's canonical presence_lib node_status format -- alias bright/green for SyncDOOM players, activity grey, clipped so a long status never wraps; SyncDOOM players first) plus recent game events, refreshed ~1/s and growing upward over the art. The poll loop services node messages/telegrams (nodesync), passes control keys through so Ctrl-T/Ctrl-U no longer draw over the panel while leaving Ctrl-P for node paging, and treats Enter/'?' as a menu redraw. Add the 'L' activity-log view (json_lines over events.jsonl) with prune-on-entry retention. Move the menu-key hints into the themeable lobby.msg art and use console.pause() for the standard prompt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  83. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 23:33:57 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/g_game.c diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: in-game multi-line who's-online + game-event log Replace the single clipped HU message line for the in-game who's-online list (Ctrl-U) with the shared multi-line top banner (white-on-red, the same non-blocking overlay incoming pages use): one node per row with the full standard activity phrase (sbbs_action_str, matching the waiting room / terminal-server display), auto-clearing after a few seconds. Grow the banner to 10 rows, size its output buffer for that, and wipe vacated rows when a shorter banner replaces a taller one. Add game-event logging to <data>/syncdoom/events.jsonl (-eventlog): start, level-clear, frag, death and end records (with debug fields: terminal, user, node, tier, build), plus frag/death/total-frag accessors in g_game.c. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  84. Rob Swindell (on Windows 11)
    Tue Jun 23 2026 22:58:57 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: log authenticated user logon/logoff at LOG_INFO http_logon()/http_logoff() logged every web logon and logoff at LOG_DEBUG, so webv4 (and HTTP-auth) user logins were invisible in the server log unless debug-level web logging was enabled - unlike the FTP (ftpsrvr.cpp:2695), mail (mailsrvr.cpp:1422/4380/4489) and terminal (answer.cpp:452) servers, which all record a successful user login at LOG_INFO. Log a logon at LOG_INFO when a real user authenticated (user.number > 0) and keep anonymous/Guest logons (number == 0) at LOG_DEBUG, so the constant per-request anonymous churn (bots, crawlers) stays quiet. http_logoff() already early-returns unless a user was logged in, so it moves to LOG_INFO unconditionally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  85. Rob Swindell (on Windows 11)
    Tue Jun 23 2026 22:44:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: consolidate js_CreateUserObjects() branches in http_checkuser() The user>0 and guest (NULL user) branches differed only in the user argument and an error-log string; collapse them into a single call with a ternary for the user pointer. No functional change (the anonymous failure path now logs the same "creating user objects" message as the authenticated path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  86. Rob Swindell (on Windows 11)
    Tue Jun 23 2026 22:33:50 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: remove #1169 timing probes (issue resolved) Reverts the debug-level timing probes added in f6d382c13 to localize the webv4 login stall; #1169 is now root-caused and fixed in b0f02c4e6 (recvbufsocket reads buffered TLS data directly instead of waiting on the raw socket for MaxInactivity). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  87. Rob Swindell (on Windows 11)
    Tue Jun 23 2026 22:21:10 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: read buffered TLS request body directly (fix #1169 login stall) A webv4 login/logout is an HTTPS POST whose body (credentials) often arrives in the same TLS record as the headers, so it sits decrypted-but-unread in the TLS layer with nothing left on the raw socket. read_post_data() -> recvbufsocket() gated each read on session_check(), which since 50258e70b ("detect TLS client disconnect", #1155) only treats a TLS session as readable when a byte has been peeked (peeked_valid) - it no longer short-circuits on tls_pending. With the body buffered but no peeked byte, session_check() fell through to socket_check() on the raw socket and blocked for the full MaxInactivity timeout (60-90s) before the buffered body was finally read. That is the #1169 "login stalls ~90s at Initializing User Objects" symptom: POST-only (login/logout), duration == MaxInactivity, no wire traffic. Guard the recvbufsocket() wait with tls_pending the same way sockreadline() already does for header reads: when TLS data is already buffered, read it directly instead of waiting on the raw socket. Header reads were unaffected because sockreadline() kept its own tls_pending guard; only the body read regressed. Manifests whenever the body is TLS-buffered at read time (reliably on Windows, intermittently on Linux v3.22a); absent in v3.21f, which predates 50258e70b. Verified on vert: the auth POST's "Authorization check complete" -> "Responding to request" gap went from 60s to 0s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  88. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 14:51:29 GMT-0700 (PDT)
    Modified Files:
    

    src/vdmodem/readme.txt diff
    src/vdmodem/vdmodem.c diff
    vdmodem: Add configurable ConnectMsg result for incoming connections (#1165) SVDM hardcoded "CONNECT 9600" for extended result codes, which makes DOS BBS software that paces its output to the reported modem speed (e.g. SPITFIRE) run slowly even though the TCP connection is unrestricted. Add a "ConnectMsg" key to the root section of svdm.ini specifying the text reported after "CONNECT" (e.g. "115200/ARQ"), so the sysop can report a faster, optionally error-corrected connection. It applies only with extended (ATX1+) verbose (ATV1) result codes; empty (the default) preserves the legacy "CONNECT 9600"/"CONNECT" behavior. The reported speed is cosmetic and does not limit TCP throughput. Bump SVDM version to 0.6 and document ConnectMsg in readme.txt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  89. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 13:39:59 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: add debug-level timing probes to localize webv4 login stall (#1169) Issue #1169 reports an exactly-90-second stall on every webv4 portal login/logout, logged between "Initializing User Objects" and the first "Adding query value" line. It was initially suspected to be related to #1153 (Windows exclusive user.tab locking), but the reporter confirmed the stall persists on a current nightly that already carries the #1153 fix, so it is unrelated. Tracing the path shows js_CreateUserObjects() and its area-object creators only build lazy JS skeletons and take no user.tab lock, and the stalling request is anonymous (no user-record write at all), so the native "Initializing User Objects" step is an unlikely culprit. To localize the delay empirically, add LOG_DEBUG probes that bisect the gap between that log line and query-string parsing: - http_checkuser(): "User Objects initialized" (bounds js_CreateUserObjects) - check_request(): "Authorization check complete" (bounds check_ars tail) - respond(): "Responding to request (dynamic=%d)" - exec_ssjs(): "beginning JS request" / "initializing request properties" (brackets JS_BEGINREQUEST to catch a blocking begin-request) The adjacent pair of lines that straddles the 90s gap in a debug log localizes the offending region. Probes are tagged "#1169 timing probe" for easy removal once root-caused. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  90. Rob Swindell (on Windows 11)
    Tue Jun 23 2026 01:33:27 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/node.c diff
    Extended node status was displayed without user number Broken since commit d116f36227
  91. Rob Swindell
    Tue Jun 23 2026 01:33:12 GMT-0700 (PDT)
    Added Files:
    

    exec/sdos.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    Removed Files:

    exec/sdos.src diff
    Merge branch 'sdos-shell' into 'master' Sdos Shell from Baja to JS See merge request main/sbbs!699
  92. Thomas McCaffery
    Tue Jun 23 2026 01:33:12 GMT-0700 (PDT)
    Added Files:
    

    exec/sdos.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    Removed Files:

    exec/sdos.src diff
    Sdos Shell from Baja to JS
  93. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 01:30:24 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/sbbs_node.c diff
    src/doors/syncdoom/sbbs_node.h diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom: show lobby/waiting/game in the node who's-online status The door now sets a free-text node status (node.exb + the NODE_EXT misc bit) so the BBS-wide who's-online AND the door's own Ctrl-U list show the SyncDOOM sub-state instead of the generic "running external program": - JS lobby/menu -> the default "running SyncDOOM" (unchanged) - waiting room -> "in the SyncDOOM waiting room" - in a level -> "playing SyncDOOM: <wad> (E1M1)" / "(MAP07)" The WAD label is the friendly [wadset:*] name the lobby passes via a new -wadname arg, or the -iwad basename (e.g. "freedoom1") when absent. The map only appears while actually in a level (usergame); it's cleared on door exit (and the BBS rewrites it from the action anyway). sbbs_node.c gains sbbs_node_set_ext() (write node.exb + set/clear NODE_EXT via a locked read-modify-write, as in sbbs) and sbbs_node_ext() (read); sbbs_list_nodes() flags NODE_EXT nodes so Ctrl-U shows their free text. Also makes the terse action abbreviations more readable (main menu, reading msgs, downloading, ...). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  94. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 00:45:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/m_menu.c diff
    syncdoom: shorten the F1 help line to "CTRL-S STATS" Match the in-game STATS ON/OFF toggle label (was "STATISTICS"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  95. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 00:45:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: fix Windows Terminal sixel regression (per-frame palette) The define-once palette optimization assumed the terminal retains sixel color registers across separate DCS images. SyncTERM does, but Windows Terminal and xterm do NOT -- so frames that omitted the palette block referenced undefined registers and rendered with wrong/default colors. Gate define-once on g_is_syncterm; everywhere else re-send the palette every frame. It's now the stable 1:1 register<->index set, so a per-frame palette is identical each time (no churn) -- matches Deuce's guidance and restores correct sixel on WT/xterm while keeping SyncTERM's define-once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  96. Rob Swindell (on Debian Linux)
    Tue Jun 23 2026 00:28:11 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: add sixel to the F4 tier cycle on JXL terminals When JXL is the startup tier, the door now also probes sixel (the JXL ladder short-circuits before probe_sixel, and a JXL-capable SyncTERM answers the sixel CTDA query immediately), and adds it as a second graphics state so F4 cycles jxl -> sixel -> text tiers. Lets a player A/B the two graphics tiers live. Skipped when sixel is force-disabled. Graphics-tier cycle labels now name the tier (jxl/sixel/ppm) instead of the generic "graphics", so the F4 flash shows which one you're on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  97. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 23:12:55 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/README.md diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: document WAD compatibility for sysops Add a "WAD compatibility" note where sysops actually pick WADs -- the top of the [wads] section in syncdoom.example.ini and the wads/ bullet in the xtrn README. Covers: vanilla / limit-removing only (Chocolate Doom engine, so Boom/MBF21/(G)ZDoom-only maps and patches won't run -- a GZDoom mod like MyHouse plays only its vanilla decoy map), the known-good IWAD list, and the multiplayer rule that every player in a lockstep match runs the same WAD set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  98. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 23:12:55 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/i_video.c diff
    src/doors/syncdoom/render_sixel.c diff
    src/doors/syncdoom/render_sixel.h diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: make the sixel tier work on SyncTERM Three fixes that together make SyncTERM render the sixel tier correctly (it previously garbled and/or scrolled), and let older SyncTERM reach it. - Stable, define-once palette. The encoder rebuilt a per-frame palette from the RGB framebuffer and re-emitted all 256 color-register definitions every frame -- with register numbers assigned in scan order, so the same Doom color was a different register each frame. That churn corrupted SyncTERM's decoder (garbled colors every few frames). Now we encode from Doom's native 8-bit indices (i_video.c exposes the live palette + a change counter) against a fixed 1:1 register<->index palette, and (re)define the registers ONLY when Doom actually changes the palette (damage/pickup/radsuit/menu). A steady scene sends band data alone -- identical palette every frame, ~4KB smaller. - DECSDM (private mode 80) reset while the sixel tier is active. It defaults to set, which scrolls the page + appends a newline whenever a full-screen sixel reaches the bottom -- i.e. every frame. Resetting it pins the sixel at the page origin and stops the per-frame scroll. Restored on exit/tier-change. - CTDA sixel auto-detect. SyncTERM advertises sixel only via its private CTerm Device Attributes (ESC[<c -> capability 4 = pixel ops), not the xterm DA1 flag, so older (no-JXL) SyncTERM used to fall back to text. Probe ESC[<c and accept cap 4, so those versions auto-select sixel. 1.4+ still prefers JXL (the tier ladder probes JXL first). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  99. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 23:12:55 GMT-0700 (PDT)
    Modified Files:
    

    exec/lbshell.js diff
    Don't need to pass a filespec to bbs.list_file_info(..., FI_USERXFER) Fixes lbshell.js line 1388: ReferenceError: spec is not defined
  100. Deucе
    Mon Jun 22 2026 23:01:28 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm_dec.c diff
    Ensure pixelsb is initialized for setpixels() This could cause weird blinking issues with sixel background areas
  101. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 18:22:49 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: anchor mouse steering to the image + cap sixel size Two fixes for terminal mouse steering in a window much larger than the rendered game (notably xterm sixel): - Steer relative to the IMAGE, not the window. The steer center and full-deflection span are now taken from the rendered image's cell rectangle (g_img_col + width) instead of the terminal's center. With a letterboxed or top-left-anchored image the old window-center put the "idle" point out in the black margin, so the player spun until the pointer was dragged off into empty space; now idle sits on the picture's center and the image edge is full turn. Exact once the terminal's real cell size is known (SyncTERM always; xterm with allowWindowOps). - Never upscale the sixel past native 640x400, even when real pixel geometry was measured. Sixel is near-raw RLE, so a window-filling sixel on a large console is both too big for the terminal to render and a huge per-frame payload that stalls the DSR pacing -- the door froze on a blank screen once allowWindowOps let xterm report a big window. The 640 cap was previously skipped when geometry was known; sixel is impractical to upscale regardless. JXL/PPM (real compression) keep the full scale_max. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  102. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 18:19:22 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/ini_file.c diff
    xpdev: fix iniSetString/iniGetSection misfiling keys in empty sections When an ini section's header exists but the section is empty (immediately followed by another section header, or EOF), section_start() returned the end of the list, so iniSetString()/iniSetValue() inserted new keys at the end of the file -- under the last section -- instead of into the intended (empty) section. The misfiled keys then couldn't be read back via iniGetString(section, key), and repeated rewrites accumulated duplicates (GitLab #1168). Root-cause the special case away: find_section() now returns the index of the next section header (or the list terminator) for an empty section, which is both the correct stop-point for read loops and the correct insertion point for new keys. section_start() is removed (it collapsed to an identity). That change required guarding iniGetSection(): it unconditionally pushed list[i] and only worked before because find_section() returned the NULL terminator for an empty section. It now skips the push when list[i] is a section header, so an empty section yields no keys instead of bleeding the following section's header and keys into the result. This also fixes a pre-existing latent bug: iniGetSection(ROOT) on a file with no root-level keys returned the first named section's contents -- which corrupted iniSortSections(list, NULL, ...) output (reachable from filedat.c's batch_list_sort(), duplicating the first entry of a no-root batch list). Adds an in-memory regression suite under #ifdef INI_FILE_TEST (run when the test binary is invoked with no file arguments): 6 checks covering the #1168 misfile, the empty-section read guard, and the empty-root case. The suite reports 3 failures against the unfixed code and 0 against the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  103. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 01:56:12 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/sbbs_node.c diff
    syncdoom: define a PATH_MAX fallback in sbbs_node.c (MSVC) sbbs_node.c used PATH_MAX but only included dirwrap.h, which defines the xpdev-portable MAX_PATH -- not PATH_MAX, which is POSIX-only and absent under MSVC (error C2065). Add the same #ifndef PATH_MAX fallback syncdoom.c already uses, so the door's Windows/MSVC build compiles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  104. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 01:56:12 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: write per-user prefs fresh to fix lost settings The per-user prefs file (data/user/####/doom/syncdoom.ini) silently stopped saving and loading its [input] settings (mouse, instant_turn, kpturn, ...) and piled up duplicate keys. Cause: read-modify-write plus "remove a key when it equals the default" eventually emptied the [input] section, and an xpdev bug then misfiled every later [input] key at the end of the file under [video], where it was unreadable (gitlab #1168). Write the file fresh each time instead -- store only the current non-default prefs, sections built in order -- so the empty-section path is never hit and an already-corrupted file self-heals on the next save. These are generated prefs in data/, so there's nothing hand-edited to preserve by reading the old file first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  105. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 01:07:47 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: Ctrl-O toggles mouse steering in-game + F1 help line Ctrl-O now flips terminal mouse steering on/off live, mirroring the Ctrl-S/Ctrl-T door hotkeys: it flashes a "MOUSE ON/OFF" label, switches the xterm tracking modes to match (?1003h/?1006h on, ?1003l/?1006l off), and saves the setting per-user. Turning it off also drops any held button and the last pointer offset so the player stops cleanly. This doubles as the per-user off switch for anyone who doesn't want mouse control. Also adds a "CTRL-O MOUSE / CTRL-S STATISTICS" line to the F1 controls help screen, shifting the control list up a few pixels to keep it clear of the skull. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  106. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 00:56:59 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/doomgeneric.h diff
    src/doors/syncdoom/i_input.c diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/README.md diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: terminal mouse steering (xterm SGR mouse) On SyncTERM and other xterm-mouse-capable clients the door now turns with the mouse, which bypasses Doom's turn-acceleration ramp and the door's key-up-synthesis grace machinery entirely -- so turning is markedly smoother and more precise than the arrow keys (no key-repeat lag). Terminals report the pointer's ABSOLUTE, screen-clamped cell position (no relative deltas, and the host can't recenter the pointer), so the model is a virtual joystick: the pointer's horizontal offset from screen-center sets a turn RATE -- hold it left of center to keep turning left, return to center to stop. A relative-delta "native feel" model was prototyped and dropped (it stalls uselessly at the window edge). An idle timeout relaxes steering to neutral when the pointer stops reporting, so abandoning it off-center (e.g. alt-tabbing away) no longer spins forever -- terminals send no focus-out event to signal it. Buttons map to Doom's defaults: left = fire, right = strafe-modifier, middle = forward. Vertical mouse and the wheel are unused. Button state comes only from real press/release events, never from motion reports (a motion report can carry stale/phantom button bits, which otherwise stuck the fire bit on while steering). Enable/disable with [input] mouse = on|off (default on) or -mouse on|off; saved per-user, suppressed in menus and while typing chat, and simply inert on terminals without mouse reporting. Implementation: parse SGR mouse reports (ESC[<b;col;row M/m) in the CSI parser (enlarged the param buffer); DG_GetMouse() projects state to a per-tic ev_mouse posted from I_GetEvent(); enable/disable the tracking modes (?1003h/?1006h) in DG_Init/terminal_restore. Also fixes the in-game Ctrl-P key, which a stale "return 't'" alias had been shadowing so it opened talk instead of reaching the page handler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  107. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 00:56:59 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/sbbs_node.c diff
    src/doors/syncdoom/sbbs_node.h diff
    Modified Files:

    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/hu_stuff.c diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: in-game who's-online (Ctrl-U) and node paging (Ctrl-P) Let a player see who else is on the BBS and page a node from the door. Door- native: no SCFG/scfg_t load -- new sbbs_node.{c,h} reads node.dab from $SBBSCTRL and user/name.dat from $SBBSDATA, and pages a node the way putnmsg does (append to msgs/n###.msg + set the target's NODE_NMSG flag), all with plain file I/O and the current Synchronet headers (nodedefs.h). Self node from $SBBSNNUM. The public interface uses a projected sbbs_node_info_t so the BBS node_t never collides with Doom's own node_t (BSP nodes). UI: - Waiting room: Ctrl-U lists the online nodes (node, alias, activity), Ctrl-P pages one (list -> pick node -> type message); incoming pages are shown there too. Blocking screens are fine here -- no game is running. A hint row advertises the keys. - In-game: non-blocking, so the lockstep netgame and the frame pacing aren't disturbed. Ctrl-U posts a compact one-line HUD message ("Online: <alias> <activity>, ..."); an incoming page is word-wrapped into a transient top-of- screen banner (Doom's HU line caps at 80 chars and would truncate it); Ctrl-P in-game just points to the waiting room. Polled between ticks. Activity wording matches Synchronet's nodestr() defaults; a terse set of abbreviations is used for the compact in-game line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  108. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 00:56:59 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: accept a short or SAUCE-padded waiting.bin splash load_splash() required the file to be exactly 4000 bytes (80x25), so an editor that saved a shorter canvas (e.g. PabloDraw's 80x23 = 3680 bytes) or appended a SAUCE record was rejected and the door fell back to the baked-in art. Now it zero-fills the cell buffer and loads up to 4000 bytes from any non-empty file: a short image fills the top rows (rest black), a longer one has the extra bytes (SAUCE) ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  109. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 00:56:59 GMT-0700 (PDT)
    Added Files:
    

    xtrn/syncdoom/waiting.bin diff
    Modified Files:

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/d_net.c diff
    src/doors/syncdoom/g_game.c diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/syncdoom.c diff
    src/doors/syncdoom/tools/gen_splash.py diff
    xtrn/syncdoom/README.md diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: FAST TURN default, change-only prefs, quit fog, editable splash Door/lobby polish (all built; FAST TURN and the teleport-fog quit live-confirmed in MP): - FAST TURN: a new Options-menu checkbox / [input] instant_turn that defeats Doom's slow-start turn-acceleration ramp -- the ramp keeps resetting on terminal key-repeat gaps and makes turning feel laggy. Now DEFAULT ON, with the TURN grace lowered (150 -> 75 ms) so full-speed taps don't over-swing. Per-user, saved. (The inline input rows shift up a touch to fit the new option.) - Per-user prefs now save ONLY settings the player actually changed from the sysop/built-in default; matching keys are removed. So a sysop's house defaults keep reaching returning players for any key they never touched in-game. - [game] quit_effect = keep | vanish | fog (default fog): a departing player's marine teleports out (fog puff + sound) instead of standing frozen. The body is removed deterministically across the lockstep netgame, so it's a house setting, not a per-user toggle. - [game] splash: the waiting-room backdrop is now an external, editable waiting.bin (80x25 raw char+attr "binary text", PabloDraw/Moebius-editable), loaded at startup with the baked-in art as fallback. tools/gen_splash.py emits it alongside the C header. - READMEs + syncdoom.example.ini updated for all of the above. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  110. Rob Swindell (on Debian Linux)
    Mon Jun 22 2026 00:56:59 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom: case-insensitive WAD resolution (DOOM2.WAD vs doom2.wad) On a case-sensitive (Linux) filesystem a configured "doom2.wad" did not find a real DOOM2.WAD -- DOS/Windows WADs are commonly upper-case -- so the lobby hid the wadset and the door reported it "not found". Resolve case-insensitively with the stock helpers: the lobby uses file_getcase() (presence check + launch args), and the door's wadcopy() uses fexistcase() (xpdev dirwrap), which rewrites the path to the real on-disk case. A no-op on case-insensitive (Windows) filesystems. Reported by nelgin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  111. Rob Swindell (on Windows 11)
    Sun Jun 21 2026 20:54:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/MainFormUnit.cpp diff
    sbbsctrl: count total_users() on a background thread, not the GUI thread TMainForm::StatsTimerTick() called total_users() directly on the VCL message-pump thread. total_users() walks the entire user base, reading every record (a lock/read/unlock per user); on a large base - especially a network-mounted data_dir - that is slow, and while the servers are busy (e.g. a web scrape saturating the file share) it froze the GUI for seconds at a time. 1eb9328327 (limited-19-blackjack) reduced how often the scan runs, but throttling frequency can't stop an individual O(N) blocking scan from freezing the UI when it does run. Run the scan on a short-lived background thread (_beginthread, matching the server-launch threads in this file). The worker never touches the VCL: it publishes the count, and StatsTimerTick() picks it up on a later tick. The existing frequency guard and the !StatsForm->Visible early-return are kept, so a hidden Stats window still scans nothing and at most one scan runs at a time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  112. Rob Swindell (on Windows 11)
    Sun Jun 21 2026 20:54:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: bound drain_outbuf() so a dead client can't wedge the server drain_outbuf() spun in a SLEEP(1) loop as long as the outbuf ring buffer held data and the socket was still valid, with no timeout and no check of the terminate_server flag (the "/* ToDo: This should probably timeout eventually... */" note acknowledged this). When a client stops reading, the output thread blocks in its send and the buffer never drains, so the session thread spins forever. Under a distributed web scrape (many abandoned Alibaba/Aliyun keep-alive connections) this hung web-server shutdown: the "Waiting for N child threads to terminate" loop never completed because several http_session_thread()s were stuck in drain_outbuf() <- send_error(). Bound the wait: return (not break) when terminate_server is set, or once the buffer has stalled for max_inactivity seconds. Returning rather than falling through matters - the output thread can hold outbuf_write while blocked in a send, so the trailing pthread_mutex_lock() would just re-hang; returning lets the caller close the socket, which unblocks the output thread. Unbounded since the original SLEEP-based drain in 00f254912d (maker-8-money). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  113. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 20:37:40 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/zmachine/getgames.js diff
    xtrn/zmachine: getgames.js -- clear message when http.js lacks Download() Older Synchronet installs ship an exec/load/http.js without HTTPRequest.Download(), so getgames' fetches died with an opaque "Download is not a function" (X-Bit hit this on a Windows install). Detect it up front: print a clear "update http.js" notice, still install the bundled games, and skip the download entries gracefully instead of throwing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  114. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 20:33:24 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/zmachine/tools/blorb2gfx.js diff
    xtrn/zmachine/zmachine.js diff
    xtrn/zmachine: Windows/ImageMagick fixes for v6 graphics baking Surfaced by a Windows install report (X-Bit / X-Bit BBS) of Arthur's v6 scenes rendering washed-out in SyncTERM. - blorb2gfx.js + v6scaledFile: prefer ImageMagick 7's unified "magick" command, fall back to "convert". On Windows, bare "convert" is the OS disk-format tool that shadows ImageMagick on the PATH, silently breaking the bake. - blorb2gfx.js: place "-depth 8" AFTER the input so PPM output is forced to maxval 255. On ImageMagick 7, -depth before the input is only a read setting, so a 4-bit (16-colour) Blorb source wrote maxval 15 -- which SyncTERM renders washed-out (and which looks "fine" in GIMP, since GIMP normalises maxval). Latent on every IM7 host, not just Windows. The -gamma 0.55 pre-darken (to cancel SyncTERM's pnm_gamma) is unchanged and still applied. - zmachine.js: require("userdefs.js") instead of load() -- sbbsdefs.js already require()s it, so a 2nd load() re-executes it (idempotent require avoids the redundant re-declaration); best practice regardless. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  115. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 19:54:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/prntfile.cpp diff
    src/sbbs3/sbbs.h diff
    Fix custom-shell menus showing the stock menu instead of the override-dir file When a command shell sets a menu sub-directory override (menu_dir), menu file lookups are supposed to search that subdir before the default text/menu dir. Commit d3123dd1ae (vampire-14-reset) added that default-dir fallback, but implemented it *inside* menu_exists() on a per-extension basis. Because the terminal-type extension priority loop lives one level up in menu() (rip, mon, ans, seq, msg, asc), a higher-priority extension that exists only in the default dir would preempt a customized lower-priority extension in the override subdir: e.g. with menu_dir="errol_", a custom errol_/main.asc was shadowed by the stock text/menu/main.msg, because menu() asked menu_exists(code,"msg") (which fell back to the default dir and matched) before ever trying .asc in the subdir. Result: the stock "classic" menu was displayed while the custom shell's keystroke handling still worked -- as reported by Errol Casey (Amessyroom) on sync_sysops. Move the subdir->default fallback out to wrap the *entire* extension search, in both menu() and the NULL-ext path of menu_exists(), so the override subdir is exhausted across all extensions before the default dir is consulted at all. Factor the single-dir, single-extension lookup into a new no-fallback helper menu_exists_in(). Direct callers of menu_exists() (including the JS bbs.menu_exists() binding, str.cpp info-file checks, and random_menu()) keep the two-pass fallback and their existing boolean contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  116. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 15:06:30 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/mp_server.c diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom: quit empty matches fast + reap dead servers' registry files Two fixes so abandoned/dead games stop lingering in the Browse list: - mp_server.c: once a client has connected, an empty match (creator cancelled, everyone left, or the game finished) now quits after ~8s instead of the full 60s idle timeout. A never-joined spawn keeps the full grace (its creator's connect is still in flight). An empty match is already hidden from Browse, so nobody can join it during the wait anyway. - sd_list_games: a clean shutdown removes its own .ini, but an unclean death (kill/crash) leaves it frozen at its last heartbeat. Reap (delete) such a file once it's well past stale (x3, leaving margin for SMB attribute-cache lag on a shared cross-host games dir); a still-live server re-creates its file on the next heartbeat. The merely-stale window still just hides it. Validated: reap test deletes only the ancient orphan, keeps stale/empty ones hidden-but-present, lists the joinable game. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  117. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 15:06:30 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.js diff
    syncdoom lobby: auto-join when only one game is running Pressing J with a single game listed made the player read a one-row table and then type "1". When exactly one game is running, skip the list and the "join which?" prompt and join it directly (still validated: lobby status, WAD set installed). A one-line "Joining <host>'s game..." confirms the pick. Two or more games still show the picker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  118. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 15:00:35 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: list waiting-room players one per row (fits 3-4 long names) The player list was a single row of "N.name" entries separated by spaces -- fine for two short aliases, but three or four longer ones (Synchronet allows 25-char aliases) overran 80 columns and truncated, like the prompt did. Put one player per row and size the status panel to the player count: it grows upward over the splash from 4 rows (1-2 players) to 6 (a full 4), so every name shows in full regardless of length. Validated via pyte at 1-4 players incl. a 25-char alias -- nothing reaches column 80. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  119. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 14:49:51 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/sd_splash.h diff
    src/doors/syncdoom/tools/gen_splash.py diff
    syncdoom: redo the splash as a shaded-block DOOM wordmark The first splash used solid full-blocks in flat horizontal color bands -- the crude look real ANSI art avoids. Real ANSI grades color with shade glyphs (0xB0/0xB1/0xB2) that mix a bright foreground over a dark background to make intermediate tones (yellow over red = orange, etc.). Regenerate sd_splash.h as a crisp rasterized "DOOM" wordmark rendered that way: a white-hot top melting through a cyan-chrome band into a dithered yellow->orange->red fire, with a dark-red 3D bevel on the right/bottom letter edges, a left highlight, and molten drips. Still bespoke (not derived from any commercial Doom asset). tools/gen_splash.py rewritten to match (Pillow + numpy). Validated by rendering sd_splash.h through the door's exact emission path (pyte) behind the waiting-room panel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  120. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 14:15:05 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/syncdoom.example.ini diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom lobby: optional full-screen DOOM ANSI attract on entry Show a full-screen DOOM ANSI splash once when a player enters the lobby, before the menu. A random *.ans/*.asc from the [lobby] art_dir (default an "art" sub-dir of the door dir) is paged; any key drops into the menu. Silent and skipped when no art is installed or [lobby] attract = false, so it costs nothing out of the box. The art is sysop-provided -- nothing ships in the repo (the classic ~48-row DOOM scene portraits are fan art of id's monsters; a sysop drops their own into the art dir). The waiting-room bespoke logo splash is unchanged. - syncdoom_lib.js: cfg.lobby, sd_attract_dir(), sd_attract_files() (filters by extension case-insensitively -- classic art is often upper-case *.ANS and directory() is case-sensitive on *nix). - lobby.js: sd_attract() called once at the top of sd_main(). - syncdoom.example.ini: documented [lobby] attract / art_dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  121. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 14:14:54 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: keep the waiting-room "waiting alone" prompt within 80 cols The controller-waiting-alone prompt ("Waiting for another player... auto- starts when full. Q cancel (then pick single-player to play solo).") was a single ~103-char line that ran off the right edge of an 80-column terminal, truncating the solo hint to "...pick si." (reported with a screenshot). Split it across the panel's two free rows: the "waiting / auto-starts" line on top+2 and the "Q to cancel / play solo" hint on top+3, each well within 80 cols. The other two prompt cases already fit and are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  122. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 13:39:35 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/sd_splash.h diff
    src/doors/syncdoom/tools/gen_splash.py diff
    Modified Files:

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: bake in a bespoke fiery-DOOM-logo waiting-room splash The multiplayer waiting room had no backdrop. Render a baked-in 80x25 splash (the same "ENDOOM" char+CGA-attr cell format) behind the player panel: a fiery DOOM logo (chrome -> white -> yellow -> orange -> red gradient with molten drips), "S Y N C" overhead, and a tagline. The art is bespoke -- NOT derived from any commercial Doom asset -- so it ships safely in the public tree (a Freedoom ENDOOM looked poor, and doom.wad's is copyrighted). - sd_render_screen(cells): generalizes the former WAD-ENDOOM renderer to draw any 80x25 char+attr buffer (CGA attribute -> SGR, raw CP437 or UTF-8 per the terminal's charset), capped to the terminal's row count. - sd_splash.h: the embedded splash array, generated by tools/gen_splash.py (Pillow). Edit the generator, not the header. - Drops the w_wad.h/z_zone.h includes (no longer reading a WAD lump). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  123. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 13:31:50 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: resolve a bare -iwad/-file against the [wads] dir, not CWD The door exports DOOMWADDIR from the [wads] dir so a bare "-iwad freedoom1.wad" -- the direct-exec single-player install entry -- can locate the WAD in the configured directory. But the arg-rewrite that makes WAD paths absolute before the sandbox chdir ran abscopy(), which resolves a relative name against the door's CWD: "freedoom1.wad" became "<doordir>/freedoom1.wad" before Doom's own IWAD search ran, defeating DOOMWADDIR and failing with "IWAD file '<doordir>/freedoom1.wad' not found!" whenever the WAD lives in the wads/ subdir (the default layout). Add wadcopy(): a relative -iwad/-file/-merge value resolves against the configured (absolute) [wads] dir when the file is found there -- the same place the JS lobby loads from -- falling back to CWD-relative (abscopy) otherwise. The lobby, which passes absolute WAD paths, is unaffected. The installer's single-player launch string needs no change. Reported by Accession. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  124. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 04:00:51 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/render_text.c diff
    src/doors/syncdoom/render_text.h diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/controls.msg diff
    xtrn/syncdoom/syncdoom.example.ini diff
    Removed Files:

    src/doors/syncdoom/cli_data.c diff
    src/doors/syncdoom/cli_data.h diff
    syncdoom: remove the text-tier dither (and the Ctrl-N toggle) The blue-noise dither only ever applied in 256-color text mode, and the cell-diff forced it static (an animated dither re-tints every cell every frame, defeating the diff) -- at which point it was barely visible and not worth its keep. Rip it out: drop the noise machinery from render_text.c (init_noise, NOISE_SAMPLE, the per-cell noise application, rt_set_dither/rt_dither_applicable) and delete cli_data.c/.h (the 172 KB blue-noise texture pack, dither-only) + its build entry. Remove the door's Ctrl-N hotkey, [video] dither config, per-user dither pref, and the docs/controls/example.ini references. Colors are now emitted exactly as quantized (no change at 4/24-bit, which never dithered; 256-color loses only the subtle static texture). Builds smaller and one fewer thing to explain. Text tiers only; JXL/sixel unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  125. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 03:48:43 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/hu_stuff.c diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: legible text-tier HUD -- message line + chat as real characters In the text tier Doom's HUD text (pickup messages, chat) was rasterized into the 320x200 framebuffer and then downsampled with the rest of the image to half/ sextant blocks -> illegible mush. Render it as actual terminal characters instead. - hu_stuff.c: HU_message_text()/HU_chat_text() expose the current message-line and chat strings; sd_text_hud (set by the door in text mode) suppresses Doom's own framebuffer draw of them so only the crisp terminal-character version shows. - syncdoom.c (text path only): fetch the strings, register their cell rectangles as cell-diff exclusions (so the game never repaints under them -- no flicker, same as the stats overlay), then draw them on top -- the message top-left (white-on-black, capped to stay clear of the right-side stats overlay), chat one row below (yellow-on-blue). sd_text_hud is 0 in the graphics tiers, where Doom draws the HUD into the image as before. Validated: the attract demo's pickups now render as readable text ("Got the pump-action shotgun!", "Picked up some bullets.", ...) rather than blocks. Chat is wired the same way; needs a live multiplayer game to confirm. Text tiers only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  126. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 03:23:21 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/render_text.c diff
    src/doors/syncdoom/render_text.h diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: cell-diff text renderer -> flicker-free, lower bandwidth The text tier re-painted the entire screen every frame; the big frame segments over TCP, so the terminal showed a half-updated row before the stats overlay (at the end of the frame) arrived -> the overlay strobed, and every frame was a full redraw. render_text.c now diffs: a shared put_cell() sink (replacing the per-cell output_colors/glyph) keeps a SHADOW of each terminal cell's (fg,bg,glyph) signature and re-emits a cell only when it changed (absolute cursor positioning instead of per-row newlines; the SGR cache carries across the frame). rt_config sizes the shadow + forces a full repaint on any tier/charset/geometry change; rt_render_frame clears + repaints on force, else emits only the deltas. The dither is held STATIC (the noise texture no longer cycles) so a cell's signature is stable frame-to-frame -- otherwise every cell would read as changed. Ctrl-N still toggles dithering on/off; it's just spatial now, not animated. HUD exclusion (rt_exclude_add/clear + rt_invalidate): the door registers the stats overlay's cell rectangle before each render, so the game diff never emits those cells -> the overlay is never repainted under, killing the flicker without sacrificing a row. rt_invalidate() resyncs the shadow after the door clears a label row behind the renderer's back. Validated (harness + pyte): renders identically to the old full redraw; only the forced repaints clear-screen (2 of 316 frames), near-static frames drop to 0-500 B (the diff skipping unchanged cells), and the overlay stays intact over the game with exclusion on. Text tiers only; JXL/sixel untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  127. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 02:56:50 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: anchor text-mode overlay + labels to the rendered grid width In text mode the grid is capped (text_max_cols, default 200), so on a terminal wider than the cap the stats overlay (right-justified) and the centered labels (DEPTH/DITHER/STATS/video) were positioned against the full g_cols and landed off to the right of the actual game view -- the overlay flew past the edge, the labels drifted right. Add overlay_cols() = the rendered width (g_text_cols in text mode, g_cols for the graphics tiers, where the overlay sits in the centered image's margin) and route the overlay's right-justify + all four label centerings through it. g_text_cols is set in setup_text_mode to the capped tcols. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  128. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 02:00:31 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: stats-overlay display fixes (letterbox clear, shrink, MB/s) Three fixes to the Ctrl-S overlay (and the Ctrl-T/N/F4 labels), reported on a large Windows Terminal / SSH window where the centered game frame never repaints the top row: - Clear on dismiss: when a label's dwell ends (or the overlay is toggled off) the top row is wiped (ESC[1;1H ESC[2K) and a full repaint forced. In a letterboxed window that row is in the margin, so it was stranding the text otherwise. - Shrink-gap blanking: the overlay is right-justified, so a narrower redraw (fewer fps/lag digits, or KB/s -> MB/s) left the previous wider text's left end behind. Track the prior width and blank exactly the now-uncovered cells. - Throughput reads fractional MB/s above 999 KB/s (KiB/MiB of wire bytes) so the field stays narrow on a fast link (LAN sixel runs 3-6 MB/s). Confirmed fixed live on Windows Terminal (LAN). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  129. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 00:59:43 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: AIMD auto-depth controller, stable depth, lag relabel, cap 8 Follow-up to dfd77154e. Live VPN testing showed the base-minus-penalty auto depth oscillating 4/5<->1 on a jittery link, and the depth-1 dips were a slideshow. Rework the controller and the supporting RTT measurement. - auto depth is now a delay-based AIMD controller with a dead-band and a rate limit (max_inflight is a pure getter; auto_depth_update runs per DSR report): probe up one when the round-trip is clean (<1.25x baseline), ease down when it queues, HOLD in between -> it SETTLES at the link's sustainable depth instead of hunting. Heavy queuing eases down (no longer slams to 1). Validated: holds high on a fat VPN, stable on LAN, no oscillation. - RTT baseline integrity: a reclaimed frame's late DSR report could be mis-matched to a freshly-sent one, reading absurdly low and collapsing the ceiling to depth 1 (the "manual cycle -> dips to 1 -> slow crawl back" bug). Two guards: skip exactly the reports owed by reclaimed frames (g_dsr_stale), and ignore any sample far below the smoothed RTT. - g_rt_high (renamed from the misleading "g_remote"): the frame round-trip is network latency PLUS the client's decode/render time, so a LAN with non-instant JXL decode legitimately has a ~50ms round-trip. Once it's non-trivial, floor depth at 2 (depth 1 there only caps the frame rate) -- latched, so a corrupted sample can never strand a remote player at depth 1. - Relabel the displayed/logged "RTT" -> "lag": it isn't pure ping (includes decode), so "lag" is honest. Overlay, Ctrl-T popup, exit telemetry. - Raise the depth cap 5 -> 8 (DEPTH_MAX; DSR ring widened to 16). On a high- latency link frame rate ~= depth/round-trip, so 5 left fps on the table (e.g. ~19fps at depth 5 / 230ms); depth 8 reaches the 35fps sim cap there. auto climbs to ~6 on such a link on its own; 7-8 are manual. Docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  130. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 00:59:43 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/controls.msg diff
    syncdoom: frame de-dupe + overlay throughput + auto-depth retune Follow-ups to the adaptive frame pacing (3f94d6c31), driven by live VPN/LAN testing and the exit telemetry now in the BBS log. Frame de-dupe: - emit_frame keeps a copy of the last framebuffer sent and skips a byte-identical re-render -- the duplicates Doom emits between its 35fps sim tics, plus any still scene. Caps the wire rate at the real visual rate and saves the redundant bytes (logs show 50-75% of renders skipped). Cache is invalidated on any label flash or geometry change so the screen never goes stale. - The Ctrl-S overlay no longer suspends de-dupe (which defeated the point while you watched the meter): the frame body is de-duped independently, and the overlay refreshes only when its text changes, so a static screen costs ~nothing. This also cures the overlay flicker (it was repainting row 1 every frame). Stats overlay + telemetry: - Overlay shows transmit throughput (KB/s to the player) and reads "depth N/auto"; dropped the "SyncDOOM" label for room. Exit telemetry logs the de-duped count. - Ctrl-T's centered popup is suppressed while the overlay is up (it already shows the live depth/RTT) so it can't obscure the game. auto-depth retune (max_inflight): - Floor depth at 2 once RTT > ~30ms: depth 1 is one frame per round-trip -- a slideshow on a remote link -- so never auto-park a far player there. - The BDP cap now only applies while actively streaming (recent fps >= 10). An idle or de-duped lull drops the frame rate for lack of MOTION, not bandwidth; capping on that was clamping depth to 1 right as the next move began (the logged "depth=1 on a 211ms link" bug). The RTT-inflation backoff stays the real bloat guard, and heavy queuing now drains all the way to 1 to flush a backlog. controls.msg: add the Ctrl-S row + color/style refresh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  131. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 00:59:43 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/controls.msg diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: adaptive frame pacing to lift remote frame rate (Ctrl-T / Ctrl-S) Over a high-latency link the DSR per-frame pacing capped the frame rate at ~1/RTT (a slideshow). Make the number of frames "in flight" configurable and, by default, adaptive. - [video] frames_in_flight = 1..5 | auto (now the default). A pipeline lets several frames cross the link at once, lifting a far-away player's frame rate toward Doom's 35fps sim; on a fast LAN it goes well past that (duplicate re-renders between tics -- a frame-dedupe pass, next, will trim those). - auto = delay-based + bandwidth-aware congestion control: base depth from the BASELINE (windowed-min) RTT, back off when the current RTT inflates above it (a queue we're causing), and cap at the bandwidth-delay product (recent_fps x min_RTT) so a high-latency *low-bandwidth* link (a far VPN) can't over-pipeline and bloat the input lag. Validated live across LAN and VPN, and in simulation (stable / queuing / bandwidth-limited links). - Ctrl-T cycles the depth (1..5 -> auto) live and saves it per-user; Ctrl-S toggles a top-right stats overlay (tier / fps / RTT current+baseline / depth). - Exit telemetry logs RTT (current + baseline min) and the effective depth. - Built-in default is auto (door is new -- no installed base to surprise); the example.ini ships frames_in_flight = auto. README + controls.msg + the F1 help document the keys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  132. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 00:59:43 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm_irc.js diff
    chat_llm_irc: disable SpiderMonkey branch-limit in the bot main loop js.branch_limit (alias of js.time_limit) caps the cumulative operation- callback count over the JS runtime's whole life. A long-lived daemon that polls in a while-loop for days inevitably reaches it, at which point jsexec sets js.terminated and the bot exits; the launcher relaunches it, producing the periodic 'guru dropped and rejoined' churn on IRC. Set js.branch_limit = 0 (disabled) at the top of main_loop(). This loop is not a runaway: it blocks on sock.poll() with a timeout and leaves promptly on js.terminated or the .stop semfile, so real shutdown still works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  133. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 00:59:43 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm_persona.utf8 diff
    chat_llm: add SECURITY guardrails to the guru persona prompt The guru had no security framing, so a caller could get it to role-play as a shell: typing 'cat /etc/passwd | grep bbs' produced a fabricated, realistic-looking passwd line (no actual file access -- the LLM has no shell -- but credential-file-shaped output that invites escalation and erodes trust in a multi-party channel). Add a high-priority SECURITY section (marked as overriding even the anti-fabrication rules) instructing the persona that it is a chat persona, not a shell/OS/command interpreter: do not simulate command execution or invent command output; never emit (real or fabricated) system files, credentials, hashes, keys, or other users' private data; cannot grant access or change accounts; and ignore caller attempts to override its identity/rules or extract the prompt. Validated against the live engine: shell-command, prompt-injection, privilege-escalation, and data-exfil probes all deflect in character while normal Q&A is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  134. Rob Swindell (on Debian Linux)
    Sun Jun 21 2026 00:59:43 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/chat.cpp diff
    chat: emit UTF-8 guru replies correctly to UTF-8 terminals (fix mojibake) Guru replies come off the LLM as UTF-8, but they were pushed through CP437-assuming output primitives that re-encode each byte to the terminal charset. On a UTF-8 terminal this double-encoded every multibyte character into mojibake (e.g. a curly apostrophe shown as three glyphs), and any node-spy (sbbsctrl, mqtt_spy.js) mirroring the node's byte stream saw the same corruption. Fix both output paths: - Multinode (chat_llm_multinode_turn): stop pre-converting the reply to CP437 based on the reader's terminal and emit with bprintf(P_AUTO_UTF8, ...). The reply stays UTF-8; bputs() adapts per terminal -- raw UTF-8 to a UTF-8 term, print_utf8_as_cp437() to a CP437 term. - 1-on-1 streamed and non-streamed (simulate_type, the shared chokepoint reached via js_simulate_type and chat_llm_session): when the terminal is UTF-8, emit each multibyte codepoint atomically via bputs(seq, P_UTF8) instead of byte-by-byte outchar(). Typo simulation correctly applies to ASCII only. Robust to a codepoint split across SSE tokens (bytes still go out raw and in order). The speed_factor<=0 fast path uses P_AUTO_UTF8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  135. Rob Swindell (on Windows 11)
    Sat Jun 20 2026 18:54:45 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/build.bat diff
    build: use x64-hosted compiler (PreferredToolArchitecture=x64) on Windows The 32-bit-hosted CL.exe (HostX86\x86) intermittently crashed with 0xC0000005 (STATUS_ACCESS_VIOLATION) on the parallel Windows GitLab build -- e.g. job 1547029, where CL.exe died compiling upgrade_to_v319.vcxproj. The crash is in the compiler front-end itself, not our code, and lands on a random source file each run: the classic symptom of the 32-bit host exhausting its ~2GB user address space under a loaded parallel build. Passing PreferredToolArchitecture=x64 as a solution-wide global property switches CL.exe to the x64-hosted, x86-targeting cross-compiler (HostX64\x86). Output is identical (still /MACHINE:X86, /arch:IA32) but the compiler runs as a 64-bit process with no 2GB ceiling. Set on the msbuild command line (not a .vcxproj/Directory.Build) so it is a global property guaranteed visible before Cpp.Default.props selects the tool architecture, and so it also covers the sibling xpdev/smblib libs. Follow-on to the mspdbsrv (266083317) and LTCG mitigations in Directory.Build.targets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  136. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 18:39:04 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/zmachine/.gitignore diff
    xtrn/zmachine: gitignore runtime game content (v6 .gfx/.blb/.z6, titles, splash, logs) The door dir doubles as the install dir, so sysop/runtime game data lives alongside the package. Ignore it (v6 .z6 games + .gfx graphics caches, Blorb sources, per-genre titles.ini IFDB caches, the optional intro splash, and logs) so a populated install never shows it as untracked or risks committing game data to the shared repo. Bundled Zork I/II/III remain tracked.
  137. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 18:17:23 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/userdat.c diff
    src/sbbs3/userdat.h diff
    Create user_can_access_xtrn() - unused as of yet I created this function in anticipation of using it to solve issue #1166, but it's more complicated than first thought (the JS system object where the node status is often retrieved does not know the identity of the user viewing it). I plan to use this function to help solve that issue later.
  138. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 18:03:46 GMT-0700 (PDT)
    Added Files:
    

    xtrn/zmachine/.gitignore diff
    xtrn/zmachine/README.md diff
    xtrn/zmachine/SYSOP.md diff
    xtrn/zmachine/fantasy/arthur-r74-s890714.msg diff
    xtrn/zmachine/fantasy/zork1.z3 diff
    xtrn/zmachine/fantasy/zork2.z3 diff
    xtrn/zmachine/fantasy/zork3.z3 diff
    xtrn/zmachine/games.ini diff
    xtrn/zmachine/getgames.js diff
    xtrn/zmachine/install-xtrn.ini diff
    xtrn/zmachine/jszm.js diff
    xtrn/zmachine/quetzal.js diff
    xtrn/zmachine/test/.gitignore diff
    xtrn/zmachine/test/blorb2gfx.js diff
    xtrn/zmachine/test/colour.js diff
    xtrn/zmachine/test/conformance.sh diff
    xtrn/zmachine/test/czech.sh diff
    xtrn/zmachine/test/fixtures/games_test.ini diff
    xtrn/zmachine/test/getgames_test.js diff
    xtrn/zmachine/test/gfx_probe.js diff
    xtrn/zmachine/test/journey_smoke.js diff
    xtrn/zmachine/test/quetzal.js diff
    xtrn/zmachine/test/resume.js diff
    xtrn/zmachine/test/screen.sh diff
    xtrn/zmachine/test/screenlist.js diff
    xtrn/zmachine/test/syntaxcheck.js diff
    xtrn/zmachine/test/test.ppm diff
    xtrn/zmachine/test/timed.js diff
    xtrn/zmachine/test/unicode.sh diff
    xtrn/zmachine/test/unit.js diff
    xtrn/zmachine/test/v45.js diff
    xtrn/zmachine/test/v6.js diff
    xtrn/zmachine/test/v6clearscreen.js diff
    xtrn/zmachine/test/v6gfx.sh diff
    xtrn/zmachine/test/v6mode.js diff
    xtrn/zmachine/test/v6parity.sh diff
    xtrn/zmachine/test/v6pic.js diff
    xtrn/zmachine/test/v6sixel.js diff
    xtrn/zmachine/test/v6surface.js diff
    xtrn/zmachine/test/viewport.js diff
    xtrn/zmachine/test/zz_smoke.js diff
    xtrn/zmachine/tools/blorb2gfx.js diff
    xtrn/zmachine/v6pics.js diff
    xtrn/zmachine/v6sixel.js diff
    xtrn/zmachine/viewport.js diff
    xtrn/zmachine/zmachine.js diff
    xtrn/zmachine: add the JSZM Z-machine interactive-fiction door A Synchronet external program (door) that plays Z-machine interactive fiction (Infocom classics and modern IF) in the terminal server, using the JSZM interpreter ported to Synchronet JavaScript (SpiderMonkey 1.8.5). - jszm.js / quetzal.js -- interpreter engine + Quetzal save codec - zmachine.js -- the door front-end (v3/4/5/8 text + v6 graphics) - v6pics.js / v6sixel.js / viewport.js / tools/blorb2gfx.js -- v6 graphics (SyncTERM APC + Sixel tiers; Blorb->.gfx baker) - games.ini / getgames.js -- curated game catalog + installer provisioner (bundles MIT-licensed Zork I/II/III; fetches more on request, incl. Arthur v6) - install-xtrn.ini -- SCFG auto-install - test/ -- conformance + unit/smoke suite Squashed from the standalone jszm development history (process/design docs not included). Original JSZM by zzo38 (public domain); jszm by David Lehenbauer; ES5 port, Synchronet door, v6 graphics, and installer by Rob Swindell (Digital Man). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  139. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:57:34 GMT-0700 (PDT)
    Added Files:
    

    xtrn/syncdoom/getwads.js diff
    Modified Files:

    xtrn/syncdoom/README.md diff
    xtrn/syncdoom/install-xtrn.ini diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: installer fetches the free Freedoom WAD set SyncDOOM ships no game data, so a fresh install wasn't playable until the sysop supplied WADs by hand. Add getwads.js: it downloads the freely-redistributable Freedoom set -- Phase 1 + 2 and FreeDM, pinned to release 0.13.0 -- into the [wads] dir, extracting each WAD from its GitHub release zip via the new HTTPRequest.Download() (streamed to disk) + Archive.extract(). Idempotent (skips WADs already present), non-fatal on any download/extract failure, and resolves its directory via js.exec_dir (the script's own dir, beside syncdoom.ini). install-xtrn.ini runs it as a prompted, optional [exec:] step after seeding the config. [wads] default flips from the commercial doom2 to freedoom1 (what the installer provides) so the door plays out of the box; the commercial wadsets stay hidden until their WADs are supplied. README documents the auto-download. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  140. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:54:25 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/http.js diff
    http.js: add Download() and output_file for streaming a response body to disk HTTPRequest buffered the entire response body in a (SpiderMonkey 1.8.5) string, which is costly and risky for large downloads. Add an output_file property and a Download(url, filename) convenience method that stream the body straight to a file in bounded 16 KB chunks instead. Backward compatible: output_file defaults to undefined, so existing Get()/Post() callers are unchanged. Only a final 2xx response streams to the file -- a redirect (3xx) body is still read into this.body so Get()'s redirect-follow re-requests, and a 4xx/5xx error body stays in this.body for the caller to inspect. Also exposes body_length (bytes received) on every request. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  141. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/build.bat diff
    src/doors/syncdoom/build.sh diff
    xtrn/syncdoom/.gitignore diff
    Modified Files:

    docs/v322_new.md diff
    src/doors/syncdoom/COMPILING.md diff
    src/doors/syncdoom/MULTIPLAYER.md diff
    xtrn/syncdoom/README.md diff
    syncdoom: docs + build-helper polish for release Documentation review before publishing the door: - xtrn README: Windows/MSVC is documented as SUPPORTED (build.bat or CMake + vcpkg, see COMPILING.md), not "planned" -- the Winsock port has shipped. - MULTIPLAYER.md: add the `[net] skill` and `[wadset:*] skill` keys to the config tables and correct the stale "skill is not a set key" note (it is one). - COMPILING.md: document the one-command build helpers in each platform section. - docs/v322_new.md: add SyncDOOM to the Stock Modules "what's new" list. Build helpers: - build.sh: the *nix counterpart of build.bat -- CMake configure + build, then copy the binary next to the lobby in this tree's xtrn/syncdoom/ (the same place build.bat installs the .exe). For an in-place install that's the live door dir; JPEG-XL tier auto-enabled when libjxl is found. - build.bat: track the existing Windows/MSVC build+install helper (was untracked). - xtrn/syncdoom/.gitignore: ignore the locally-built syncdoom / syncdoom.exe door binaries so they can't be committed by accident. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  142. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/MULTIPLAYER.md diff
    src/doors/syncdoom/mp_server.c diff
    src/doors/syncdoom/render_text.c diff
    src/doors/syncdoom/render_text.h diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: don't let a multiplayer match start with one player; drop in-progress games from Browse First real-player test: creators sat alone in the waiting room, pressed Start not realizing they should wait, and landed in a one-player "co-op". Mid-game join isn't possible (vanilla lockstep fixes player slots at GAMESTART, no state transfer), so make starting alone hard instead: - Waiting room refuses a manual Start while the controller is alone in a multi-player match (num_players < 2 && max_players > 1) -- beep, ignore -- and shows "Waiting for another player to join... auto-starts when full... (to play by yourself pick Play single-player)". Auto-start-when-full is unchanged; an explicit 1-player match (test/solo) still starts immediately. - Dedicated server drops its registry .ini the moment the match goes in-progress (and stops heartbeating it), so Browse only ever lists joinable games. Safe: max_games isn't registry-counted and ports use a live bind-test. - MULTIPLAYER.md documents the no-mid-game-join rationale + these mitigations. Also in this batch: - Default input graces moved to their slider midpoints: TAP 500->300, TURN 180->150 (HOLD stays 150); example.ini updated. A per-user syncdoom.ini [input] still overrides. - Ctrl-N dither toggle is now a no-op (no label, no save) where it does nothing: the graphics tiers, and the text color depths that never dither (16-color, truecolor). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  143. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/render_text.c diff
    src/doors/syncdoom/render_text.h diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/controls.msg diff
    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/syncdoom.example.ini diff
    syncdoom: Ctrl-N dither toggle + per-user syncdoom.ini; fix dither washout on F4 cycle - Dither washout fix: init_noise() rescaled noise_textures in place, and rt_config calls it on every F4 tier cycle, so the repeated rescaling flattened the dither until it vanished and never came back. Guard it to scale once (color depth is fixed for the life of the process). User-confirmed on Windows Terminal (8-bit). - Per-user prefs: the per-user file is now a sectioned syncdoom.ini in -home ([input] kp* graces + [video] dither), mirroring the house syncdoom.ini beside the exe (was a flat input.ini). Saved read-modify-write so unmanaged keys survive. Precedence: built-in -> house ini -> per-user -> CLI. - [video] dither = auto|on|off (auto = by color depth: on at 256-color, off at 16-color and truecolor) plus a live Ctrl-N toggle -- no function key was free, so 0x0E is intercepted at the door level (never reaches Doom). rt_set_dither() re-derives the dither state without rescaling. Dither is text-tier only; off also trims text bandwidth (fewer SGR color escapes from broken-up flat runs). - Docs: Ctrl-N row in controls.msg (byte-correct codes + the per-user note) and the README controls table; example.ini [video] dither + per-user overlay note. - Lobby: drop the redundant console.pause() in sd_controls -- printfile's auto-pause is the single dismissal; trimmed controls.msg to one screen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  144. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/MULTIPLAYER.md diff
    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/dstrings.c diff
    src/doors/syncdoom/i_system.c diff
    src/doors/syncdoom/m_config.c diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/net_udp.c diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/README.md diff
    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/syncdoom.example.ini diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom: persist options, in-game input-feel sliders, skill select, loopback bind, key/quit fixes Door (src/doors/syncdoom): - Config persistence: doomgeneric had Save/LoadDefaultCollection #if-ORIGCODE'd out, so screen size / messages / detail never saved or loaded. Re-enabled both (deps were all live). Skip DEFAULT_KEY entries -- the door's fixed terminal-> Doom keycodes (>=128) don't survive the config scancode round-trip and were corrupting strafe/fire/use to 0. Also call M_SaveDefaults on hangup/time-limit exits, not just menu quit. - I_Quit now actually exits (its exit() was #if-ORIGCODE'd, so menu quit, hangup, and the waiting-room cancel fell through into the game). - Waiting-room Q cancel: drain stale lobby type-ahead + scan the whole read so a buffered Enter can't auto-launch before Q is seen. - In-game Options > Input "feel" sliders (KEY TAP/HOLD/TURN) for the key-up- synthesis graces, inline on the Options menu, snap-to-grid stepping, bars aligned with SCREEN SIZE. Saved per-user (input.ini in -home, root section); [input] ini is the house default; -kp* CLI still wins. - -skill plumbed through to doomgeneric (was being eaten by the -s prefix matcher, which clobbered the client socket and bounced the player to the lobby). - net_udp.c: server binds 127.0.0.1 by default (-bindaddr); off-box play is opt- in. Client keeps INADDR_ANY. (File reformatted to house style.) - Help screen: git hash lower-left, build date lower-right. - Quit prompts: "dos" -> "the BBS". Lobby (xtrn/syncdoom): - [net] bind (defaults to advertise); skill picker with [net]/[wadset] skill default; WAD-set picker fits the real terminal width (name-only fallback); dropped em-dash "--" separators from user-facing strings. Docs: MULTIPLAYER.md, READMEs, and syncdoom.example.ini updated for the bind default, skill, input-feel, and persistence behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  145. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/MULTIPLAYER.md diff
    syncdoom: reconcile MULTIPLAYER.md with as-built; add inter-BBS federation design Bring the multiplayer design doc in line with what shipped: the real [net] keys (advertise, port_low/high, max_games, max_players=4, idle_timeout, stale, allow_external), colon section separators ([wadset:*], [net:<hostname>]), the per-host override, the actual mp_write_registry fields, and the [wads]/ [wadset:*] keys -- marking autoscan/default_iwad/sort/bind/discovery deferred. Replace the open "scope=public" sketch with a concrete Inter-BBS federation design: finger-based registry discovery, a trusted-peer IP allowlist, and an optional out-of-band shared secret (passed on the command line) for dynamic-IP peers. Co-op first, given lockstep latency across the internet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  146. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Added Files:
    

    xtrn/syncdoom/syncdoom.example.ini diff
    Modified Files:

    src/doors/syncdoom/README.md diff
    xtrn/CLAUDE.md diff
    xtrn/syncdoom/install-xtrn.ini diff
    Removed Files:

    src/doors/syncdoom/syncdoom.ini diff
    syncdoom: ship syncdoom.example.ini + installer copy; drop duplicate src config Version-control the documented config template as xtrn/syncdoom/syncdoom.example.ini and have install-xtrn.ini copy it to the live syncdoom.ini on install (the [copy:] action won't clobber an existing one without confirmation). The door and lobby both read the one combined file (video/input for the door; net/wads/wadset for the lobby). Remove the duplicate door-only sample src/doors/syncdoom/syncdoom.ini, which had already drifted from the deployed config, and repoint the door README at the combined example. Document the example.ini -> live-ini [copy:] pattern in xtrn/CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  147. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom lobby: per-host advertise, WAD-set default/note, per-WAD .msg Multi-host: overlay a hostname-keyed [net:<hostname>] section onto [net] (keyed by system.local_host_name) and pass [net] advertise to the dedicated server as -advertise, so one shared install can give each host its own joinable address. Blank advertise still means loopback / same-host only. WAD picker: pre-select the [wads] default set, show a [wadset:*] note (with a pause) before launch, and display any "<wadname>.msg" readme sitting beside a WAD in the selected set (paged, before launch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  148. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/render_text.c diff
    syncdoom: skip redundant per-cell SGR in the text/block renderer The text tier emitted a full color escape for every cell. Track the last foreground/background actually sent and emit only the component(s) that changed -- a flat run now emits just the glyph -- cutting output bytes sharply over a BBS link with identical on-screen results. The cache is reset at each \033[0m (row end) and at frame start, where the terminal is back to its default colors. Also drops the now-unused buffer_append_format. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  149. Rob Swindell (on Windows 11)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/COMPILING.md diff
    src/doors/syncdoom/vcpkg.json diff
    Modified Files:

    src/doors/syncdoom/.gitignore diff
    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/README.md diff
    src/doors/syncdoom/mp_server.c diff
    src/doors/syncdoom/render_text.c diff
    src/doors/syncdoom/syncdoom.c diff
    syncdoom: add Windows/MSVC build support (incl. JPEG-XL via vcpkg) Port the door to build with Visual Studio 2022 / MSVC via CMake. The engine (Chocolate Doom) was already _WIN32-aware and all socket I/O already went through xpdev's sockwrap; this fills the remaining gaps: - syncdoom.c: guard <unistd.h>, add the Windows include block; now_ms() via timeGetTime(), DG_SleepMs() via Sleep(), abscopy() via _fullpath; WSAStartup() at startup (sockwrap doesn't self-init Winsock); guard SIGPIPE out; PATH_MAX fallback. - mp_server.c: implement mp_spawn_server() with CreateProcess (DETACHED_PROCESS) -- the Windows analogue of the fork/setsid detach. - render_text.c: mempcpy/stpcpy fallbacks for MSVC (glibc-only). - CMakeLists.txt: MSVC branch (CRT deprecation defs, winmm, C11); Winsock/winmm come transitively from xpdev. JPEG-XL: locate an MSVC-built static libjxl + deps (highway/brotli/lcms2), with a release/debug split so multi-config (VS) builds link the right CRT. - vcpkg.json: manifest pinning libjxl for the MSVC JPEG-XL tier. - COMPILING.md: build instructions for *nix and Windows; README trimmed to a quickstart that points to it. Verified on Win32: Release + Debug both link and run (mode=jxl), with a self-contained exe (libjxl statically linked, no JXL DLLs). The *nix build is unchanged -- all Windows code paths are behind _WIN32 guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  150. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.msg diff
    syncdoom lobby: hint the global hotkeys on the lobby screen Add a footer line to the lobby art pointing out Synchronet's global hotkeys that stay live inside the door -- Ctrl-U (who's online) and Ctrl-P (private message) -- handy for a user waiting in the lobby for others to join the game. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  151. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/syncdoom.ini diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom lobby: page nodes to join, fix join-race & stale games, cap at 4 players When creating a multi-player game, offer to page other active nodes whose users can run the door (User.compare_ars against the door's execution_ars), delivered via the sysop-configurable NodeMsgFmt so it matches normal inter-node messages. The page prompt now runs before the server is spawned, so it no longer sits in the spawn->connect window where a browse-joiner could connect first and steal the host/controller slot. Hide games with zero connected players from the Join list, so a match everyone has left doesn't linger until the server's idle-timeout. Cap players at Doom's MAXPLAYERS (4) rather than NET_MAXPLAYERS (8) -- the game has only four player slots/colors/starts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  152. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    syncdoom: document WASD controls and DeHackEd/WAD-merge support Rewrite the in-game controls section of the door README for the WASD scheme and the type-cheats/save-names-in-UPPERCASE rule, and document the -deh DeHackEd/BEX patch loading and -merge WAD-merge support now built into the door. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  153. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/d_englsh.h diff
    src/doors/syncdoom/doomkeys.h diff
    src/doors/syncdoom/g_game.c diff
    src/doors/syncdoom/m_menu.c diff
    src/doors/syncdoom/syncdoom.c diff
    xtrn/syncdoom/controls.msg diff
    syncdoom: WASD controls, in-game chat text-entry, Ctrl-P talk alias Remap the terminal input to a WASD scheme: W/S move forward/back (the up/down- arrow movement keys), A/D strafe, turning stays on the left/right arrows, Space fires, E uses/opens, R toggles always-run. The run toggle moves off the literal '\' to a synthetic KEY_RUNTOGGLE, so '\' is freed and an uppercase R still reaches the cheat parser. Suppress the gameplay remaps while typing text (chat_on or menuactive) so space/letters enter literally, and give chat its own discrete-tap path in key_seen (a repeated char must register every time). Add Ctrl-P as a talk alias. Refresh the F1 help screen (WASD/E/R/T rows, horizontally-centered skull computed from the live patch width, explicit QUICKSAVE/QUICKLOAD), drop the parens that read poorly in the bitmap font, and reword the quit prompt to "quit to bbs". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  154. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/m_menu.c diff
    syncdoom: move the build-id to the lower-right of the F1 controls screen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  155. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/lobby.js diff
    xtrn/syncdoom/lobby.msg diff
    syncdoom: lobby menu/UX -- Join relabel, create-when-empty prompt, picker width - lobby.msg + lobby.js: relabel the menu -- J "Join a multi-player game" (was B Browse) and C "Create a multi-player game" (was "co-op"); the optional external-join moves to E so it doesn't collide with J. - sd_browse: when no games are running, prompt console.yesno("Create a game now"). - sd_pick_wadset: cap each WAD-set label to one line (long descriptions wrapped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  156. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/syncdoom/syncdoom.ini diff
    xtrn/syncdoom/syncdoom_lib.js diff
    syncdoom: lobby -- deh= wadset key + FreeDM wadset - syncdoom_lib.js: a wadset may now specify deh = <patch[,patch...]>; it's built into -deh args and required-present like the WADs, so a mod shipping a DeHackEd patch can be curated. - syncdoom.ini: document the deh key; add the FreeDM wadset (free deathmatch IWAD). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  157. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Added Files:
    

    src/doors/syncdoom/deh_ammo.c diff
    src/doors/syncdoom/deh_bexstr.c diff
    src/doors/syncdoom/deh_cheat.c diff
    src/doors/syncdoom/deh_defs.h diff
    src/doors/syncdoom/deh_doom.c diff
    src/doors/syncdoom/deh_frame.c diff
    src/doors/syncdoom/deh_io.c diff
    src/doors/syncdoom/deh_io.h diff
    src/doors/syncdoom/deh_main.c diff
    src/doors/syncdoom/deh_mapping.c diff
    src/doors/syncdoom/deh_mapping.h diff
    src/doors/syncdoom/deh_misc.c diff
    src/doors/syncdoom/deh_ptr.c diff
    src/doors/syncdoom/deh_sound.c diff
    src/doors/syncdoom/deh_str.c diff
    src/doors/syncdoom/deh_text.c diff
    src/doors/syncdoom/deh_thing.c diff
    src/doors/syncdoom/deh_weapon.c diff
    src/doors/syncdoom/w_merge.c diff
    Modified Files:

    src/doors/syncdoom/CMakeLists.txt diff
    src/doors/syncdoom/d_main.c diff
    src/doors/syncdoom/deh_main.h diff
    src/doors/syncdoom/deh_misc.h diff
    src/doors/syncdoom/deh_str.h diff
    src/doors/syncdoom/doomfeatures.h diff
    src/doors/syncdoom/doomtype.h diff
    syncdoom: vendor and enable DeHackEd (-deh) and WAD merge (-merge) Both were #undef'd in doomfeatures.h with their sources absent. Vendor the DeHackEd subsystem (deh_*.c/.h) and w_merge from Chocolate Doom and enable FEATURE_DEHACKED + FEATURE_WAD_MERGE. The deh sources came from a newer Chocolate Doom than our doomgeneric base, so a few adaptations were needed: - doomtype.h: add the PRINTF_ATTR / PRINTF_ARG_ATTR macros the newer headers use. - deh_io.c: this base has no M_fopen, and lumpinfo is an array of structs. - deh_main.c: drop DEH_AutoLoadPatches (needs the newer i_glob); and free the -deh filename only when it is the malloc'd copy -- this base's D_TryFindWADByName returns the original argv string when the file isn't found, and freeing that stack pointer aborted the door (free(): invalid pointer). - w_merge.c taken from the pre-lumpinfo-pointer-refactor revision to match. Also un-gate LoadIwadDeh (ORIGCODE -> FEATURE_DEHACKED) so the Freedoom/FreeDM IWAD dehacked lumps (level names, etc.) load. Command-line -deh is applied after, so it still overrides. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  158. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/README.md diff
    xtrn/syncdoom/controls.msg diff
    xtrn/syncdoom/lobby.js diff
    syncdoom: lobby help -- terminals & video-modes section; presentation fixes - controls.msg: add a TERMINALS & VIDEO MODES section (JXL needs SyncTERM 1.4+, sixel terminals, CP437/UTF-8 ANSI text), shorten "Run (toggle)", and replace the esoteric Ctrl-A cursor-right indent codes (and a stray trailing Ctrl-Z) with plain spaces so the file edits cleanly in any editor. - lobby.js: drop the redundant "Command:" prompt (the art is the menu) and stop passing P_NOPAUSE so the now-longer help auto-paginates. - README: JXL needs SyncTERM 1.4+, not 1.2+ (per src/syncterm/CHANGES). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  159. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: cap the text-tier render grid on oversized terminals A maximized terminal can measure huge (e.g. 561x105); the text tier then renders the full grid -- ~330 KB per frame, 1.5+ MB/s -- which floods the link and trips the dead-client watchdog, so the door looks like it auto-terminates. Beyond Doom's own detail the extra cells add only bytes, so cap the text grid to [video] text_max_cols x text_max_rows (default 200x80; 0 = uncapped). Invisible for normal terminals; it only bounds the maximized case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  160. Rob Swindell (on Debian Linux)
    Sat Jun 20 2026 14:04:13 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/syncdoom/syncdoom.c diff
    syncdoom: anchor the sixel image at top-left when terminal pixels are unknown Sixel is positioned by text cell, but the image is sized in pixels; on a terminal that doesn't report its real cell-pixel size (e.g. xterm with allowWindowOps off) the door assumed 16px-tall cells, so the "centered" row landed too low and the frame looked bottom-anchored. When real geometry is unknown, anchor the sixel at row 1, col 1 -- predictable, and what a user expects. JXL/PPM (SyncTERM, real geometry) still center via the APC DX/DY offsets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  161. Deucе
    Fri Jun 19 2026 18:41:26 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Fix big in parsing binary PBM/PPMs Only a single whitespace is allowed between the header and the raster data. While we're here, remove some unused variables.
  162. Rob Swindell
    Wed Jun 17 2026 17:56:02 GMT-0700 (PDT)
    Modified Files:
    

    exec/pcboard.js diff
    Merge branch 'pcboard-shell' into 'master' Fix file scan prompt See merge request main/sbbs!700
  163. Thomas McCaffery
    Wed Jun 17 2026 17:56:02 GMT-0700 (PDT)
    Modified Files:
    

    exec/pcboard.js diff
    Fix file scan prompt
  164. Rob Swindell (on Windows 11)
    Sat Jun 13 2026 17:53:25 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/smbutils/SKILL.md diff
    smbutils skill: correct count-limit option and document r/pagination gotcha Surfaced while reading sub-board messages with smbutil: the skill described the count-limit option as -#<N> in one place (smbutil actually rejects that with "Unknown opt '#'") and as unsupported in another. The real option is a bare -<N> (e.g. -1). Also documented that `r` with no count reads to the end of the base and paginates - so piped/non-interactive it can stall or auto-background waiting for a keypress - and added the clean scripted form (-o -1 r#<num> <base> </dev/null) plus an options-table row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  165. Rob Swindell (on Windows 11)
    Sat Jun 13 2026 17:37:14 GMT-0700 (PDT)
    Modified Files:
    

    docs/v322_new.md diff
    exec/binkit.js diff
    exec/irc.js diff
    exec/load/dorkit.js diff
    exec/load/lockfile.js diff
    src/sbbs3/exec.cpp diff
    src/sbbs3/js_internal.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    services/web/terminal: decouple JS client-disconnect termination from auto_terminate ead5ccf16 (song-11-earn) "Detect disconnection in JavaScript callback" overloaded the js_callback_t.auto_terminate flag - historically just "abort on server shutdown/recycle" - to also abort a script ~10 operation-callbacks after its client socket disconnects. cfa6fe9e1 (bolt-11-banner) exempted static services (IRC daemon, MRC connector), but per-connection service scripts were left exposed. BinkIT, run as the inbound binkp service, intentionally keeps working after the peer disconnects: it finishes the batch, updates binkstats.ini, and as its last act touches the FIDOIN/BINKOUT event semaphores. The disconnect check aborted it mid-cleanup, so those semaphores were never touched and outbound FTN mail to downlinks silently piled up. Decouple the two concerns with a new js_callback_t.terminate_on_disconnect control (exposed as the js.terminate_on_disconnect property), distinct from auto_terminate (which again governs only shutdown/recycle, via js_CommonOperationCallback). All three client-connected servers now gate their disconnect abort on terminate_on_disconnect and share a single JS_DISCONNECT_TERMINATE_COUNT (10) constant, replacing three literal 10s (including the long-standing terminal check in exec.cpp). Default true in the Terminal, Web, and Services servers; binkit.js opts out with js.terminate_on_disconnect=false. js_EvalOnExit suspends it alongside auto_terminate so on-exit cleanup can't be cut short. Stock scripts that previously set js.auto_terminate=false to ride out a client disconnect now set terminate_on_disconnect in lockstep: dorkit.js (self-manages carrier loss), irc.js, and lockfile.js (UnlockAll - avoids stale locks if killed mid-unlock, which also closes the same exposure for per-connection services). Re #1156. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  166. Rob Swindell (on Debian Linux)
    Thu Jun 11 2026 15:38:01 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    exec/chat_llm_irc.js diff
    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>
  167. Rob Swindell (on Debian Linux)
    Thu Jun 11 2026 15:38:01 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    javascript skill: document terminal-control abstraction + K_CTRLKEYS vs K_EXTKEYS Two lessons from the Z-Machine v6 door work: - Terminal control sequences (cursor show/hide, etc.) are abstracted in ansiterm_lib.js (CSI ?25h/l via ansiterm.send) -- reach for that before hand-writing escape codes. - Function/cursor key input: inkey/getkey pre-translate the arrow/Home/End escapes into control codes (TERM_KEY_*), conflating them with Ctrl-letters. K_EXTKEYS still yields those conflated codes and has no F-key codes; K_CTRLKEYS passes control keys through AND leaves the escape sequences RAW so you can parse the cterm forms yourself (ESC[A/B/C/D, ESC[11~..24~) and keep arrows/F-keys distinct. getbyte() is the raw alternative but drops idle-disconnect + UTF-8. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  168. Rob Swindell
    Wed Jun 10 2026 15:19:14 GMT-0700 (PDT)
    Added Files:
    

    exec/wildcat.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    Removed Files:

    exec/wildcat.src diff
    text/menu/wildcat/sysop.asc diff
    Merge branch 'wildcat-shell' into 'master' Wildcat Clone Shell from Baja to JS See merge request main/sbbs!696
  169. Thomas McCaffery
    Wed Jun 10 2026 15:19:14 GMT-0700 (PDT)
    Added Files:
    

    exec/wildcat.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    Removed Files:

    exec/wildcat.src diff
    text/menu/wildcat/sysop.asc diff
    Wildcat Clone Shell from Baja to JS
  170. Rob Swindell
    Tue Jun 09 2026 19:00:21 GMT-0700 (PDT)
    Added Files:
    

    exec/wwiv.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    Removed Files:

    exec/wwiv.src diff
    Merge branch 'wwiv-shell' into 'master' WWIV Clone shell Baja to JS See merge request main/sbbs!692
  171. Thomas McCaffery
    Tue Jun 09 2026 19:00:20 GMT-0700 (PDT)
    Added Files:
    

    exec/wwiv.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    Removed Files:

    exec/wwiv.src diff
    WWIV Clone shell Baja to JS
  172. Deucе
    Sun Jun 07 2026 22:31:00 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Fix reading of PBM P4 files If the width is not a multiple of eight, ignore the last padding bits at the end of each line. But really, there's no reason to create a PBM where the width isn't a multiple of eight. Fixes issue reported by DigitalMan on Discord
  173. Deucе
    Sun Jun 07 2026 22:30:59 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.adoc diff
    src/syncterm/term.c diff
    Some minor PBM fixes. 1. Fix the text PBM format parsing (which nobody should ever use for anything ever) 2. Fix the embedded MASK= parsing (which should only rarely be used for oddball things)
  174. Rob Swindell (on Debian Linux)
    Sun Jun 07 2026 18:41:14 GMT-0700 (PDT)
    Modified Files:
    

    docs/v322_new.md diff
    exec/load/binkp.js diff
    BinkIT: don't record successful binkp/1.1 callouts as failures The JSBinkP session loop only breaks out on *receiving* a final M_EOB, but in binkp/1.1's two-M_EOB handshake the completed state is usually reached by *sending* the last EOB. After sending it, the peer closes the connection, and the next loop iteration attempts one more M_EOB on the now-closed socket; that send fails and (since b795cf6a3d) marked the whole session as failed -- even though all files had already been sent and acknowledged. binkit.js then wrote "[callout failure]" to data/binkstats.ini with the sent file(s) listed. binkp/1.0 peers (Mystic, mbcico) exchange a single M_EOB each and break immediately on receipt, so they were recorded correctly; the bug hit binkp/1.1 peers (Synchronet/BinkIT, binkd) -- the vast majority of sessions -- producing huge "failed_callouts" counts despite mail flowing fine. Reported in DOVE-Net's sync_sysops by Khronos and Gamgee. Fix: a failed closing M_EOB send is benign once our sent files have all been acknowledged (pending_ack empty); break the loop without failing the session in that case. Only fail when files remain unacknowledged. The on-wire behavior is unchanged. Bumped JSBinkP revision to 6 so the fix is identifiable via the vers= field in binkstats.ini. Validated with a two-process TCP loopback harness: binkp/1.1 callout flips from false to true (file transferred either way); binkp/1.0 and no-files polls unchanged; a peer that drops mid-transfer still fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  175. Rob Swindell (on Debian Linux)
    Sun Jun 07 2026 16:41:14 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/cterm_lib.js diff
    cterm_lib: add cterm_screen_geometry() resolver Wraps query_fontdims()/query_graphicsdim()/charheight() into one call returning { cols, rows, cellW, cellH, pxW, pxH } with fallbacks, for pixel-addressed protocols (Z-machine v6 door). Reusable by any door. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  176. Rob Swindell (on Debian Linux)
    Sun Jun 07 2026 16:41:14 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    skills/javascript: load() caller-scope trap + uselect/printfile/directory gotchas Lessons captured from Synchronet door work: - load('file') runs in the CALLER's scope, so its top-level vars become locals of whatever function called load() -- a top-level / on_exit handler can't see a value load()ed inside main(); capture it into a reachable scope (the silent "quetzal is not defined" ReferenceError). - console.uselect: the title is auto-prefixed with "Select " (pass "a Game", not "Select a Game"); the display call's number argument is the DEFAULT item index. console.line_counter = 0 discards a pending auto-pager prompt before a clear. - console.printfile renders Ctrl-A codes + ANSI/CP437 by default; P_PCBOARD is only for PCBoard @X codes (it would misread a literal @). - directory() defaults to GLOB_MARK, so directory entries come back with a trailing '/' (self-identifying). The CWD is process-global (always ctrl/, since sbbs is multi-threaded) -- which is why no chdir is exposed to BBS JS; build absolute paths from js.exec_dir / system.*_dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  177. Deucе
    Sun Jun 07 2026 15:25:36 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Fix X phase in setpixels() While the first text row in a setpixels() call was handled correctly, later rows always started at an X phase of 0. This means cells on the right edge would not be marked as containing pixels if the right edge is fewer pixels into a cell than the left edge. Reported by DigitalMan on Discord via direct message.
  178. Rob Swindell
    Sun Jun 07 2026 14:28:12 GMT-0700 (PDT)
    Modified Files:
    

    exec/major.js diff
    Merge branch 'major-shell' into 'master' Fix prompt color bleed See merge request main/sbbs!694
  179. Thomas McCaffery
    Sun Jun 07 2026 14:28:12 GMT-0700 (PDT)
    Modified Files:
    

    exec/major.js diff
    Fix prompt color bleed
  180. Rob Swindell (on Debian Linux)
    Sat Jun 06 2026 15:23:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/tdfonts_lib.js diff
    tdfiglet: emit conditional new-line (Ctrl-A '/') on right-most-column rows When a rendered row fills to the right-most column of the detected or configured width, output Synchronet's conditional new-line (Ctrl-A '/') instead of a hard CRLF. The terminal auto-wraps at the right margin, so a hard CRLF there produced an extra blank line; the conditional new-line only emits a new-line when the cursor isn't already at column 0, so it's correct on both auto-wrapping and non-auto-wrapping terminals. This generalizes the previous right-justify-with-zero-padding special case (which simply omitted the CRLF) to any justification, keyed solely on whether the row reaches the right-most column. In -a (ANSI) mode nothing extra is emitted, preserving prior behavior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  181. Rob Swindell (on Debian Linux)
    Sat Jun 06 2026 15:23:17 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    skills/javascript: document inkey timed input + inactivity model Add a "Timed / non-blocking key input, and the inactivity model" subsection to the input/output coverage: console.inkey(mode, timeout) semantics (timeout in ms; K_NONE returns "" vs K_NUL returns null on timeout; 1-char string on a key), the getkey-enforces-idle / inkey-doesn't trap, and the live console inactivity properties (max_getkey_inactivity, getkey_inactivity_warning, last_getkey_activity). Verified against inkey.cpp, getkey.cpp, js_console.cpp, scfglib1.c (NOINP=0x0100, default max_getkey_inactivity=300). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  182. Rob Swindell
    Sat Jun 06 2026 13:35:39 GMT-0700 (PDT)
    Added Files:
    

    exec/obv-2.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    text/menu/obv-2/main.msg diff
    Removed Files:

    exec/obv-2.src diff
    Merge branch 'obv2-shell' into 'master' OBV-2 Clone Shell from Baja to JS See merge request main/sbbs!689
  183. Thomas McCaffery
    Sat Jun 06 2026 13:35:39 GMT-0700 (PDT)
    Added Files:
    

    exec/obv-2.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    text/menu/obv-2/main.msg diff
    Removed Files:

    exec/obv-2.src diff
    OBV-2 Clone Shell from Baja to JS
  184. Rob Swindell
    Sat Jun 06 2026 13:34:01 GMT-0700 (PDT)
    Added Files:
    

    exec/spitfire.js diff
    text/menu/spitfire/chat.msg diff
    text/menu/spitfire/e-mail.msg diff
    text/menu/spitfire/file.msg diff
    text/menu/spitfire/main.msg diff
    text/menu/spitfire/msg.msg diff
    text/menu/spitfire/msglist.asc diff
    text/menu/spitfire/msgview.asc diff
    text/menu/spitfire/multchat.msg diff
    text/menu/spitfire/qwk.msg diff
    Merge branch 'xbit-spitfire-shell' into 'master' Add Spitfire BBS-inspired command shell See merge request main/sbbs!690
  185. xbit ops
    Sat Jun 06 2026 13:34:01 GMT-0700 (PDT)
    Added Files:
    

    exec/spitfire.js diff
    text/menu/spitfire/chat.msg diff
    text/menu/spitfire/e-mail.msg diff
    text/menu/spitfire/file.msg diff
    text/menu/spitfire/main.msg diff
    text/menu/spitfire/msg.msg diff
    text/menu/spitfire/msglist.asc diff
    text/menu/spitfire/msgview.asc diff
    text/menu/spitfire/multchat.msg diff
    text/menu/spitfire/qwk.msg diff
    Add Spitfire BBS-inspired command shell
  186. Rob Swindell (on Windows 11)
    Sat Jun 06 2026 02:27:41 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/text_defaults.c diff
    text_defaults.c: regenerate to sync with text.dat (SearchStringPrompt) textgen output was stale relative to ctrl/text.dat: string 076 (SearchStringPrompt) had been updated in text.dat — the "(?=help)" hint recolored — but the generated default in text_defaults.c was never regenerated to match. No-op for behavior (text.dat is the source of truth loaded at runtime); keeps the compiled-in default in sync. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  187. Rob Swindell (on Windows 11)
    Sat Jun 06 2026 02:23:35 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/str.cpp diff
    Terminal server: fully transmit pre-disconnect message before close (#1157) Messages shown to a client immediately before disconnecting (nonodes.txt when all nodes are full or node init fails, and badip.msg/badhost.msg for blocked clients) were intermittently not seen by the caller. Root cause: these paths printed the file then called flush_output(), which waits on outbuf.empty_event. That event fires when output_thread moves the ring-buffer contents into its *linear* buffer (RingBufRead), not when those bytes are actually sent over the socket. flush_output() therefore returned in the gap between ring->linear hand-off and the sendsocket(), the caller closed the socket, and output_thread's send then failed on the closed FD ("!ERROR ... sending on socket"). The result was a thread-scheduling race, matching the reporter's intermittent, protocol-independent symptom. Add sbbs_t::WaitForOutbufDrained(timeout): wait for the ring buffer to empty AND for output_thread's linear buffer to be transmitted, tracked via a new output_thread_busy atomic (set before the ring->linear read so the empty_event hand-off can't be mistaken for "everything sent", cleared once the linear buffer is fully sent). Use it in place of flush_output() at the two nonodes.txt disconnect sites (main.cpp) and in trashcan_msg() (str.cpp). flush_output() is retained for outcom()'s transmit-backpressure retry loop, where "wait for ring-buffer space" (with its online short-circuit) is the correct semantics. The nonodes drop-before-close pattern is from ff1aae498 (same-3-jazz); trashcan_msg()'s flush_output(500) is from 424dfe107 (cold-36-task); the underlying empty_event-means-ring-drained-not-sent gap is inherent to the two-stage output_thread design. Reported by xbit with detailed cross-client testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  188. Rob Swindell
    Fri Jun 05 2026 11:39:52 GMT-0700 (PDT)
    Added Files:
    

    exec/major.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    text/menu/major/email.asc diff
    text/menu/major/file.asc diff
    text/menu/major/main.asc diff
    text/menu/major/msg.asc diff
    text/menu/major/quickscn.asc diff
    text/menu/major/userdefs.asc diff
    Removed Files:

    exec/major.src diff
    Merge branch 'major-shell' into 'master' Major Shell from Baja to JS See merge request main/sbbs!686
  189. Thomas McCaffery
    Fri Jun 05 2026 11:39:52 GMT-0700 (PDT)
    Added Files:
    

    exec/major.js diff
    Modified Files:

    exec/GNUmakefile diff
    exec/Makefile diff
    text/menu/major/email.asc diff
    text/menu/major/file.asc diff
    text/menu/major/main.asc diff
    text/menu/major/msg.asc diff
    text/menu/major/quickscn.asc diff
    text/menu/major/userdefs.asc diff
    Removed Files:

    exec/major.src diff
    Major Shell from Baja to JS
  190. Rob Swindell (on Debian Linux)
    Thu Jun 04 2026 19:13:31 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/services.cpp diff
    services: init static-service client socket to INVALID_SOCKET, not 0 js_static_service_thread() memset()s its service_client to zero and never assigns .socket, leaving it 0 - a valid file descriptor (stdin), not the codebase's "no socket" sentinel. The thread already passes INVALID_SOCKET to js_initcx(), but js_initcx() doesn't store its sock arg into the struct, so the field silently stayed 0. Set it to INVALID_SOCKET explicitly so no code path mistakes fd 0 for a live client socket. Hygiene follow-up to cfa6fe9e1 (bolt-11-banner) / #1156; not a behavior change (socket_check() already returns false for both 0 and INVALID_SOCKET, so the SERVICE_OPT_STATIC guard remains the actual fix). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  191. Rob Swindell (on Debian Linux)
    Thu Jun 04 2026 19:01:05 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/services.cpp diff
    services: don't apply JS disconnection check to static services ead5ccf16 (song-11-earn) added a disconnection check to the Services server's js_OperationCallback() that aborts a script once its client socket has been gone for 10 consecutive callbacks (socket_check() of client->socket fails). This is correct for per-connection services (accepted client socket), but static services (SERVICE_OPT_STATIC - e.g. the IRC daemon, MRC connector) run via js_static_service_thread() with a zero-initialized service_client whose .socket is never set (0). socket_check(0, ...) returns false, so auto_terminate static services were wrongly warned "Disconnected" and aborted after 10 callbacks, then (being STATIC_LOOP) immediately restarted - cycling endlessly. Gate the disconnection check on !(client->service->options & SERVICE_OPT_STATIC) so it only runs for real per-client connections. Fixes #1156 (reported by Accession). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  192. Rob Swindell (on Debian Linux)
    Thu Jun 04 2026 18:58:50 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    javascript skill: correct js.on_exit scope (door child-scope vs jsexec) The earlier note said on_exit evaluates "in the GLOBAL scope" -- true only for jsexec/login/timed-event modules (host evaluates against js_glob and recurses child scopes). Doors and most bbs.exec/;exec/js.exec invocations run in a fresh child js_scope (exec.cpp:595) and the host evaluates on_exit against THAT scope (exec.cpp:701) with no recursion. Registration captures the scope where js.on_exit is called -- top level = js_scope, inside a function = that function's call object -- so a handler registered inside main() is filed where a door's EvalOnExit(js_scope) never looks and silently never runs, even though the same code passes under jsexec (which recurses). This shipped as a live bug. Rewrite: register on_exit at top level (not inside a function); prefer try/finally for clean-unwind cleanup (a door disconnect is a clean unwind) and use on_exit only as the forced-terminate backstop; note that a jsexec wrapper does not reproduce a door's on_exit scope, so finally is the dependable path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  193. Rob Swindell (on Debian Linux)
    Thu Jun 04 2026 18:58:50 GMT-0700 (PDT)
    Added Files:
    

    xtrn/CLAUDE.md diff
    xtrn: add CLAUDE.md with install-xtrn.ini guidance Guidance for working in the xtrn/ tree: how install-xtrn.ini installers work (root keys, section types, flow keys), the 16-char LEN_CODE limit (the install-xtrn.js '8 chars' comment is stale), and '?<module>' cmd resolution against startup_dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  194. Rob Swindell (on Debian Linux)
    Thu Jun 04 2026 18:58:50 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    javascript skill: document js.on_exit() scope + forced-termination semantics js.on_exit() evaluates its handler string in the script's GLOBAL scope (it compiles the string against the global object), so a handler function nested in another function is unreachable and throws silently at exit. It also fires on forced termination (operator-terminate / SIGTERM), which -- unlike a thrown exception -- does NOT run catch/finally (verified empirically via SIGTERM + file markers). New 'Script lifecycle: exit handlers' section covers the global-scope requirement, the finally-vs-on_exit distinction, and the out-of-process testing caveat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  195. Rob Swindell (on Debian Linux)
    Thu Jun 04 2026 18:58:50 GMT-0700 (PDT)
    Modified Files:
    

    exec/txt_handler.js diff
    txt_handler.js: serve robots.txt as text/plain, not the HTML viewer The .txt web handler wrapped every *.txt request (including /robots.txt) in the HTML "LIST"-style viewer, returning Content-Type: text/html. A robots.txt served as HTML is unparseable by crawlers, which then treat the site as having no robots.txt and crawl everything -- defeating the file entirely. Serve robots.txt as raw text/plain (same path as the existing ?raw output), while still rendering all other *.txt in the HTML viewer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  196. Rob Swindell (on Windows 11)
    Thu Jun 04 2026 09:42:24 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: detect TLS client disconnect in session_check() (#1155) session_check()'s is_tls branch treated a readable socket as "connected" and latched session->tls_pending; once set, it returned "connected" on every later call without re-probing the socket. But a peer's TLS close_notify (and a FIN) arrive as readable bytes, so after an HTTPS client hung up, session_check() reported it connected forever. The JavaScript disconnect check in js_OperationCallback (ead5ccf16) relies on session_check(), so its abort never armed (offline_counter stayed 0): a badly-formed SSJS/XJS page that loops on mswait() without checking for disconnection (e.g. the webv4 user/system stats) ran forever, pinning its http_session thread, a MaxClients slot, and a CLOSE_WAIT socket -- a pile of zombie HTTPS clients in sbbsctrl/MQTT and eventual MaxClients exhaustion. Why this only bit Windows: socket_check() (xpdev) has two paths. On non-Windows builds it uses poll() (CFLAGS += -DPREFER_POLL, set only in build/Common.gmake, i.e. the GNU-make/Unix builds). poll() reports POLLHUP when the peer closes its end -- even while there is still buffered data to read -- and socket_check() returns false on POLLHUP before it ever runs the readable/MSG_PEEK logic. So on Unix the close was detected, session_check() returned false, and tls_pending never latched. Windows (MSBuild) does not define PREFER_POLL and uses select(), which has no POLLHUP equivalent: a closing TLS socket simply looks "readable" (MSG_PEEK returns the encrypted close_notify bytes), so the latch was set and the disconnect masked. The session_check() bug is platform- independent; poll()/POLLHUP merely hid it everywhere except Windows. Fix: drop the tls_pending liveness latch. Use peeked_valid (a decrypted byte already buffered) as the readable fast-path, and when the raw socket is readable, probe via cryptPopData(1 byte) -- which a raw MSG_PEEK cannot do -- to tell apart application data (connected; the byte is cached in session->peeked so the next sess_recv() returns it), CRYPT_ERROR_TIMEOUT (connected, no app data yet) and CRYPT_ERROR_COMPLETE (peer closed -> disconnected). The probe is non-blocking (CRYPT_OPTION_NET_READTIMEOUT == 0, set at session setup) and runs in the session's own thread, so there is no concurrent reader. Also close the socket in place in recvbufsocket() when session_check() reports a disconnect (it previously relied on the latch returning true and the following sess_recv() failing). Latch introduced in d93478b918 (famous-15-sons); the readable-as- connected + tls_pending set predates it (dbbfabf1b1, funky-27-foam). Validated on a production Windows server: CLOSE_WAIT count ~22 -> 0, sbbsctrl thread count 221 -> 25, and ran overnight with no zombie HTTPS clients. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  197. Rob Swindell (on Windows 11)
    Wed Jun 03 2026 09:37:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    Detect disconnection in JavaScript callback First noticed (many times) in the Web Server, sbbsctrl-win32 would should very long-lived (seemingly infinitely lived) clients. When I lost Internet access last night for a few hours, this gave me the opportunity to debug these zombie clients and it turns out that a badly formed ssjs or xjs (in this case, webv4 user and system stats), that doesn't check for disconnection, and just loops, can loop almost forever (eventually the infinite loop detection, if enabled in the configuration, should abort the script). Checking the connection state in the JS callback was the missing piece. Through code review, I saw the same check was missing in the services server. I reproduced this theoretical issue by modifying an existing service script to not check the connection itself and indeed it ran-on after disconnection without this fix. I did the 10-checks before termination thing (giving the script a chance to self-terminate more gracefully) like is done in the Terminal Server.
  198. Rob Swindell (on Windows 11)
    Tue Jun 02 2026 23:55:09 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/filewrap.c diff
    xpdev/filewrap: flock path of xp_lockfile() honors fd open mode too (#1153) The non-Linux POSIX (flock) path of xp_lockfile() always took LOCK_EX, even for a read-only descriptor, unlike the Linux/fcntl path which derives the lock type from the fd's open mode. So on FreeBSD/macOS/etc. (USE_FCNTL_LOCKS is Linux-only) a read-only lock() serialized concurrent readers - the same exclusive-lock-on-reads issue fixed for Windows in the previous commit. flock() (unlike fcntl()) isn't forced to a lock type by the access mode, so the LOCK_EX was a choice; mirror the fcntl path and take LOCK_SH for an O_RDONLY fd. The user.tab read path is already covered on these platforms (rdlock()'s flock branch uses LOCK_SH); this brings the lock() primitive itself to parity, so any read-only-fd caller benefits. flock remains whole-file (pos/len ignored) - a pre-existing coarseness, unchanged here. Not compiled in this environment (the flock branch builds only on non-Linux POSIX); mirrors the existing fcntl-branch idiom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  199. Rob Swindell (on Windows 11)
    Tue Jun 02 2026 23:39:50 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/userdat.c diff
    src/sbbs3/userdat.h diff
    src/xpdev/filewrap.c diff
    src/xpdev/filewrap.h diff
    xpdev/filewrap: shared read locks (rdlock) so user.tab reads don't serialize on Windows/SMB xp_lockfile() on Windows used _locking(), which offers exclusive locks only, so every byte-range lock - even a read-only record read - was exclusive. The POSIX implementation already takes a *shared* lock (F_RDLCK) for descriptors opened O_RDONLY, so this was a silent cross-platform divergence dating to the original xpdev (f3f66b77d, lake-3-indigenous). readuserdat() locks every user.tab record it reads (for consistency against concurrent writers). On Windows that lock was exclusive, so two read-only getuserdat() calls on the same record serialized - and with the data directory on an SMB share, each lock/unlock is a network round-trip and the 200-attempt retry loop becomes a lock convoy. Under a web-scrape / login-probe flood, read-only lookups of hot records (e.g. user #1) serialize across every server sharing the mount, starving legitimate user.tab access. (GitLab #1153.) Add a shared-lock primitive: - xpdev: rdlock() - POSIX F_RDLCK; Windows LockFileEx without LOCKFILE_EXCLUSIVE_LOCK (the only Windows API offering shared byte-range locks). On Windows, lock()/xp_lockfile() and unlock() move to the LockFileEx/UnlockFileEx family so exclusive and shared locks pair with a single UnlockFileEx (a LockFileEx lock can't be released with _locking(LK_UNLCK)). Borland keeps the legacy _locking path. - sbbs3: rdlockuserdat(); readuserdat() takes a shared lock for read-only access (leave_locked == false) and an exclusive lock when the lock is held for a subsequent write. Concurrent readers of a user record now proceed in parallel; writers and the read-modify-write path stay exclusive. smblib, node.dab, the access log, and the JS/Baja file.lock() API are unchanged (all correctly remain exclusive). Build-verified under MSVC (Win32 Release: filewrap.c, userdat.c compile; sbbs.dll links). POSIX (GCC/Clang) and the Borland xpdev build not compiled here; not yet runtime-tested against the live SMB install. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  200. Rob Swindell (on Windows 11)
    Mon Jun 01 2026 23:52:48 GMT-0700 (PDT)
    Modified Files:
    

    CLAUDE.md diff
    CLAUDE.md: do not write off libmozjs 1.8.5 crashes as unresolvable mozjs185 (win32) access violations are usually fixable bugs in our use of the engine, not inherent old-engine/OOM limitations to defer to SM128. Documents the js_rtpool NULL-runtime case (#1152) and the Windows WER-minidump + cdb root-cause workflow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  201. Rob Swindell (on Windows 11)
    Mon Jun 01 2026 23:25:22 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_rtpool.cpp diff
    js_rtpool: don't crash when JS_NewRuntime() fails (NULL runtime) JS_NewRuntime() returns NULL on failure (e.g. under memory pressure), but jsrt_GetNew() pushed that NULL onto the runtime-pool list unchecked. The trigger_thread 100ms loop then walked the list and called JS_TriggerAllOperationCallbacks(NULL), dereferencing [NULL+0x164] -> access violation that takes down the whole in-process server. Observed crashing sbbsctrl.exe (3.21.4.0, Win32, mozjs185 1.8.5) twice, identical WER bucket; minidump faulting frame: mozjs185_1_0!JS_TriggerAllOperationCallbacks+0x5 (esi/JSRuntime* = NULL) sbbs!thread_start<...> (== js_rtpool.cpp trigger_thread) each preceded by web-log "out of memory" / "Failed to create new context" entries (memory pressure -> JS_NewRuntime returns NULL). Fix: don't list a NULL runtime; skip a NULL node in trigger_thread defensively; and no-op jsrt_Release(NULL) (JS_DestroyRuntime(NULL) would crash the same way -- not currently reachable, but the same bug class). Platform-agnostic (shared pool logic); surfaced on the 32-bit Windows build, which runs the JS heap dry first. Latent since 4173ce48d0 (2014). GitLab #1152. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  202. Rob Swindell (on Windows 11)
    Mon Jun 01 2026 22:25:25 GMT-0700 (PDT)
    Modified Files:
    

    exec/webfileindex.ssjs diff
    webfileindex: deflect crawlers from ?view= to prevent OOM crash The per-file ?view= path renders archive listings/images and explodes the crawlable URL space, while the index materializes the entire file_area on each request -- a large allocation in the 32-bit mozjs185 JS heap. Aggressive bots ignore robots.txt (webv4 already sends Disallow: /) and walk thousands of these, exhausting the heap; mozjs185 then turns the allocation failure into an access violation (0xc0000005 in mozjs185-1.0.dll) that crashes the whole in-process server (sbbsctrl) instead of a catchable JS OOM exception. Crashed sbbsctrl.exe on vert twice (5/29 + 6/1), each correlated with a "webfileindex.ssjs line 299: out of memory" web-log entry from a crawler hitting /files/...?view=... Deflect known crawler User-Agents with a cheap 429 *only* on ?view= requests, before any file_area access. Bots can still index directory listings; real browsers are unaffected. Stopgap until the SM128/64-bit migration, where OOM is catchable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  203. Rob Swindell (on Debian Linux)
    Mon Jun 01 2026 22:09:26 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/mqtt/SKILL.md diff
    skills/mqtt: document the native MQTT JS object (not just mosquitto) The skill only taught the external mosquitto_sub/pub path; add a 'Native access from Synchronet JavaScript' section for the in-process MQTT object (system.mqtt_enabled gate, no-arg connect() auto-configured from main.ini, subscribe/read(ms,true)->{topic,data}, publish, member table, stock mqtt_sub/pub/spy/stats.js), the local-objects-vs-MQTT decision rule, and a read-only-for-untrusted-callers warning. Also fills in observed server subtopics (state/<STATE>, served, client, client/list, client/action/<kind>) and the cross-host 'one BBSID spans every host' note + server-status field layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  204. Rob Swindell (on Debian Linux)
    Mon Jun 01 2026 22:09:26 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    exec/chat_llm_irc.js diff
    chat_llm: IRC bot acknowledges brief replies to its own lines When a user makes a clear, brief, *unaddressed* reply to the bot's OWN last line, the bot now reacts once with a short in-character line (bare, no nick prefix). Three cases: - closer -- a social acknowledgement ("ah ok", "thanks", "makes sense") - correction-- pushback ("no, that's wrong") -> a gracious concession that never re-asserts or argues - answer -- the bot's last line was a question and the user answered it -> a brief reaction that doesn't ask a new question or start a new topic (so it acknowledges without restarting the volley) Deterministic gate (chat_llm_irc.js handle_privmsg): the bot's prior line must be substantive (a reply/intervention, never another ack) + recent + directed at this speaker with nobody in between, the speaker not muted, a per-channel cooldown elapsed, and the text matches a closer/correction shape (ack_kind) -- or, for 'answer', our prior line ended in '?'. Closers are probabilistic (irc_ack_closer_chance, default 0.9); corrections and answers always react. The bot never reacts twice in a row, so the user always gets the last word. ack_kind + chat_llm_ack live in chat_llm.js (classifier is unit-testable; the line is a constrained model call with a canned fallback on ramble/question/error). New config (chat_llm.ini, default ON): irc_ack_enabled, irc_ack_window, irc_ack_cooldown, irc_ack_closer_chance -- documented in the module header. Also carries the irc_intervention_min_tokens gate (default 2): an unanswered question must have at least N content tokens to qualify for a high-confidence unprompted intervention, so a single content word can't carry a "confident enough to chime in" score. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  205. Rob Swindell (on Windows 11)
    Sun May 31 2026 23:47:28 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/xjs.js diff
    xjs: fix cold-cache recompile race in xjs_compile() Concurrent web requests on a cold .xjs.ssjs cache could observe the file mid-regeneration: it was file_remove()'d and recompiled in place, leaving it absent for the whole compile and racing concurrent load()/js.exec() into "Script file ... does not exist" and "creating ... No such file or directory" (errno 2) errors (seen on vert/cvs webv4: index.xjs, components, sidebar, pages/000-home). Compile to a unique temp file and atomically rename into place (atomic-replace on POSIX; on Win32, where rename won't overwrite, keep an up-to-date target and discard the redundant temp). Also use strict '<' for the staleness check so a same-second cache isn't recompiled. Race latent since ca76b74ba9 (2012). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  206. Rob Swindell (on Debian Linux)
    Sun May 31 2026 19:02:01 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    exec/chat_llm_irc.js diff
    exec/chat_llm_routes.js diff
    exec/llm_tools/relay_message.js diff
    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>
  207. Rob Swindell (on Debian Linux)
    Sun May 31 2026 19:02:01 GMT-0700 (PDT)
    Added Files:
    

    .claude/skills/javascript/scripts/jsobjs.py diff
    Modified Files:

    .claude/skills/javascript/SKILL.md diff
    skills/javascript: add jsobjs.py native-API lookup + guidance Parses the in-tree docs/jsobjs.html (generated by jsexec jsdocs.js) and greps the object model, so finding a native method (e.g. strftime) is one command away instead of a hand-rolled reinvention. Reads the file live each run -- auto-discovered by walking up from the script/CWD -- so it never goes stale against the checkout; falls back to a regenerate hint if missing. SKILL.md gains a "Before hand-rolling a utility, look for a native one" section pointing at the script and the js_global.cpp grep fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  208. Rob Swindell (on Debian Linux)
    Sun May 31 2026 19:02:01 GMT-0700 (PDT)
    Added Files:
    

    exec/chat_llm_routes.js diff
    Modified Files:

    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    chat_llm: extract all intent routing into pluggable router modules classify_intent now ships NO routing of its own -- it is a priority-ordered dispatcher over routers registered by modules named in chat_llm.ini's `intent_routers` key (loaded once by ensure_intent_routers()). register_intent_router(fn, priority) orders them, so a site module can slot between stock ones. The stock routing (bbs_directory, this_bbs, external_archives, relay_message + ARCHIVE_TOPICS) moves verbatim into the new stock module exec/chat_llm_routes.js (two routers: BBS-directory at priority 10, this_bbs/relay/archives at 30). The generic text helpers (_strip_bbs_suffix, _looks_like_proper_noun / _network / _os) stay in the engine as shared utilities both stock and site routers call. Site-specific routing (Synchronet's GitLab git_* tools) lives in a separate local module loaded alongside (priority 20, preserving its original mid-order position). ctrl/chat_llm.ini: intent_routers = chat_llm_routes (stock now gets its routing via the module; a clean install with the key empty has none). Behavior-preserving: the classify_intent regression suite stays green -- engine-alone routes nothing, stock+site routes identically to before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  209. Rob Swindell (on Debian Linux)
    Sun May 31 2026 19:02:01 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: route issue priority/recommendation queries to git_issues classify_intent had no rule for issue queries phrased by importance, so the model free-formed and fabricated issue numbers + titles (observed in #synchronet: #12345 "Enhance Security Flags", then #98765 after being told it was hallucinating). Add routes so these reach the tool and the model picks from REAL issues: - "highest priority / most critical / most important (etc.) issue" and "which issue would you recommend" -> git_issues kind=recent (opened) - "most recent / latest / last (open|closed) issue" (singular / embedded, e.g. "poem about the most recently closed gitlab issue") -> git_issues recent, state from the open/closed word (fixes the model inventing a lookup iid:12345 for "most recently closed issue") Guarded against false positives -- "I have a critical issue with X" stays unrouted. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  210. Rob Swindell (on Debian Linux)
    Sun May 31 2026 19:02:01 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    exec/chat_llm_irc.js diff
    exec/llm_index.js diff
    exec/llm_tools/relay_message.js diff
    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>
  211. Rob Swindell (on Debian Linux)
    Sun May 31 2026 19:02:01 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    exec/chat_llm_irc.js diff
    exec/llm_index.js diff
    exec/llm_tools/relay_message.js diff
    chat_llm: relay opt-out + caps, deterministic relay reply, ini root-section defaults Relay (IRC): - Recipients can opt out of relays ("don't relay messages to me" and variants; "relay me" opts back in). State in data/chat/<bot>_norelay.json; the relay_message tool refuses to queue for an opted-out nick and deliver_pending() drops anything already queued if the recipient opts out later. - Cap pending relays per recipient AND per sender (anti-flood), configurable via relay_max_pending (default 5), passed to the tool via env. - Deterministic relay reply: the pre-classifier now speaks the relay_message tool's own result text verbatim (.error / .note) and skips the model turn. qwen2.5:7b was observed discarding a refusal and fabricating a delivery promise ("I'll make sure X knows") for a relay it actually refused (e.g. to an opted-out recipient); the short-circuit removes any chance of misreporting whether the relay happened. Scoped to relay_message only. - De-hardcode the tool's queue/opt-out paths: chat_llm_irc.js passes relay_path/norelay_path through ctx -> env so the tool and bot share the same persona-derived files. - Move bot state (relay/mute/seen/norelay) under data/chat/. Config / persona: - chat_llm.ini defaults now live in the root (unnamed) section; a named [<persona>] section overrides. "default" is the reserved fallback persona (the root defaults) and must not be a section or a guru code. - ctx_from_user() canonicalizes the persona code to lowercase and maps blank/missing -> "default"; PERSONA in chat_llm_irc.js is lowercased at the source. - Sanitize code-derived filenames: load_index key and BOT_FILE_BASE parts run through safe_id() so an unfortunate code can't escape data/chat/ or collide via path characters. Docs: - javascript SKILL.md: document the root (unnamed) section as global defaults (iniGetObject(null)) and the case-fold caveat. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  212. Deucе
    Sat May 30 2026 21:29:58 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/ui_picker.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/capture_menu.wren diff
    src/syncterm/scripts/auto/connected/disconnect_flow.wren diff
    src/syncterm/scripts/auto/connected/font_pick.wren diff
    src/syncterm/scripts/auto/connected/music_menu.wren diff
    src/syncterm/scripts/auto/connected/online_menu.wren diff
    src/syncterm/scripts/auto/connected/transfer_pick.wren diff
    src/syncterm/scripts/sftp_queue.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_button_test.wren diff
    src/syncterm/scripts/ui_checkbox_test.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_form_test.wren diff
    src/syncterm/scripts/ui_input_test.wren diff
    src/syncterm/scripts/ui_list_test.wren diff
    src/syncterm/scripts/ui_logview_test.wren diff
    src/syncterm/scripts/ui_markdown.wren diff
    src/syncterm/scripts/ui_menubar_test.wren diff
    src/syncterm/scripts/ui_popup.wren diff
    src/syncterm/scripts/ui_radio_test.wren diff
    src/syncterm/scripts/ui_spinbox_test.wren diff
    src/syncterm/scripts/ui_widget_test.wren diff
    src/syncterm/scripts/wrentest.wren diff
    Clean up Wren UI scripts
  213. Rob Swindell (on Debian Linux)
    Sat May 30 2026 03:08:09 GMT-0700 (PDT)
    Modified Files:
    

    exec/llm_index/msgs.js diff
    llm_index/msgs: surface a post's first URL as a citable source line Message-base provenance carries no canonical URL (unlike wiki chunks, which get "Cite this URL verbatim:" from format_retrieved_context), so an answer grounded in an announcement could state the facts but had no link to cite -- e.g. a SyncTERM release answer trailing off at "see the release notes: " with nothing after. Extract the first http(s) URL from the post body and prepend it as a "Cite this URL verbatim:" line into the chunk text (which the runtime emits verbatim, so no engine change is needed -- a re-index suffices). De-dupes a doubled scheme ("https://https://", seen in a real syncanno post) and trims trailing punctuation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  214. Rob Swindell (on Debian Linux)
    Sat May 30 2026 02:41:24 GMT-0700 (PDT)
    Added Files:
    

    exec/llm_index/files.js diff
    exec/llm_index/msgs.js diff
    Modified Files:

    ctrl/chat_llm.ini diff
    exec/llm_index.js diff
    Removed Files:

    exec/llm_index/acronyms.js diff
    exec/llm_index/syncfact.js diff
    llm_index: rename msgbase/filebase sources to msgs/files; drop vert crawlers The index_sources source name "msgbase" wrongly implied its argument was itself a message base -- "msgbase:Main" reads as if Main (a message *group*) is a base. Rename the two area crawlers so the source names the content, not a false type: exec/llm_index/msgbase.js -> msgs.js (source "msgs") exec/llm_index/filebase.js -> files.js (source "files") Syntax is unchanged from master's condensed form: bare name = group / library, +<code> = one sub / dir, name/-<token> = exclude a sub / dir. One behavior fix carried along: +<code> now indexes that sub standalone instead of also pulling in its parent group's other subs, so "msgs:+syncanno" indexes just the Synchronet Announcements sub. Also drop the vert-specific acronyms.js and syncfact.js crawlers from the stock tree -- they index a local glossary / fact list particular to this install and belong in the local-scripts repo, not the public sources. Re-index validated end-to-end: files=2000, msgs=123 (syncanno only), guru.idx rebuilt, syncanno retrievable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  215. Rob Swindell (on Debian Linux)
    Sat May 30 2026 00:57:18 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    exec/llm_tools/bbs_directory.js diff
    chat_llm: fix guru BBS-directory routing, OS filtering, and IRC venue Four guru-quality fixes surfaced by live IRC testing: - classify_intent: route "which/what BBS does <sysop> run?" to bbs_directory(list, sysop:X). Previously unrouted, so the 7B fabricated BBS names ("DoveNetNode", "BinkleyTerm"). - classify_intent + bbs_directory: recognize OS-filter queries ("how many BBSes run FreeBSD?") via _looks_like_os and filter on the per-BBS OS parsed from the autoverify finger result (the same source load/sbbslist_html.js uses for its OS chart). Was mis-binding the OS as a software filter and fabricating counts; now FreeBSD=3, Win32=80, Linux=115, macOS=0. - bbs_directory: when a filtered list (sysop/os/software/network) yields zero matches, return an explicit no-match note telling the model not to invent a BBS. The 7B fabricated names from an empty bbses[] otherwise ("which BBS does mro run?" -> invented BBSes). - build_messages: IRC grounding now names the channel and states it is a shared Synchronet IRC channel, NOT the host BBS -- stops the guru greeting channel users with "what brings you to <BBS>?" and treating them as BBS callers. Regression: 29/32 (the 3 fails are pre-existing flaky FAQ citation checks); all bbs_directory/routing tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  216. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:18 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: fix classify_intent under-routing for 3 query phrasings classify_intent pre-executes a tool to bypass qwen2.5:7b's unreliable tool-choice, but three natural phrasings fell through to the model, which then answered tool-less: - "how many BBSes are in the list?" -- no rule for an UNFILTERED total count (only filtered "how many <X> bbses" existed). Add a total-count rule AFTER the filtered ones so a network/software filter still wins; unfiltered list mode returns total = full directory size. - "what has been committed to Synchronet lately?" -- the recent-commits pattern's end-anchor rejected a "to <repo>" phrase before "lately". - "how many files were downloaded today?" -- the stats pattern didn't allow a passive-voice auxiliary ("were/was/has been") between noun and verb. Regression: count-sbbslist, git-commits-recent, host-bbs-stats-today now PASS; previously-matching forms still route (no regressions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  217. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    skills/javascript: document INI trailing-comment type-dependency Synchronet .ini value parsing strips a trailing "; comment" only for single-token value types (boolean via isTrue, enum via parseEnum, and integer/float/datetime via the numeric parse). STRING values keep the whole post-'=' text (truncsp only), so an inline comment after a string-valued key becomes part of the value -- the trap that corrupts URL/path/name settings. File.iniGetObject() reads every value as a raw string and thus never strips inline comments. Add a "Reading INI files (and the trailing-comment trap)" section with the per-type table, the rationale (single-token values have no delimiter ambiguity; strings do), and the history behind the single-token support (cceb1fbb8 isTrue after FozzTexx's sexpots.ini report; 7346893d6 enum-after-comment). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  218. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    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>
  219. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/llm_tools/bbs_directory.js diff
    bbs_directory: add "popular" kind (sort by user count) list_bbses couldn't answer "which BBS has the most users?" / "what's the most popular BBS?" -- the description claimed it, but the kind switch only had recent/oldest/active/reliable/name, so the model fell back to "recent" and listed recently-online BBSes instead. sbbslist entries carry self-reported lifetime totals (entry.total.users), so add a "popular"/"users"/"busiest" kind sorting by user count descending, expose `users` in the compact record so the model can cite the number, and document the kind + trigger in the tool def. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  220. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: gate filebase boost on how-to, not all docstyle The download boost excluded any docstyle query, which wrongly caught "where can I download X" ("where" is a docstyle pattern) and denied it the filebase boost. Gate on howto_intent instead: "how do I download X" -> wiki, but "where can I download X" / "is X available" -> filebase. Fixes the regression case that slipped into eaf5c41ba. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  221. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: boost filebase for download / file-availability queries "is X available for download" / "where do I download X" pulled in wiki file-config pages (config:file_options, user:files) because "download" matches them with high frequency. Add _is_download_query() and, for those queries (excluding docstyle "how do I download X" so wiki how-tos stay), skip the wiki boost and boost filebase/ so the real files win (e.g. "where can I download syncterm" -> the SyncTERM files). A query term that matches no filename (e.g. "hyperterminal" vs htpe63.zip) still can't be surfaced by boosting -- that's a data limitation, noted inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  222. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: boost acronyms/glossary/facts for definitional queries acronym/ and syncfact/ docs don't get WIKI_BOOST (only dokuwiki/ does), so a short authoritative acronym definition lost to a wiki-boosted config page that merely mentions the term ("what does FOSSIL stand for" -> acronym/FOSSIL only #3). Add _is_definitional_query() ("what is X", "what does X stand for / mean", "define X", "what's a/an X") and, for those queries, boost acronym/, syncfact/, and dokuwiki/ref:* in bm25_search so the definition wins. Pairs with the new acronyms + syncfact index sources. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  223. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Added Files:
    

    exec/llm_index/acronyms.js diff
    exec/llm_index/syncfact.js diff
    llm_index: add acronyms + syncfact source crawlers Two flat-file crawlers for definitional / trivia grounding: - acronyms.js: reads DokuWiki acronyms conf files (conf/acronyms.local.conf + conf/acronyms.conf), emits one chunk per acronym phrased as a definition ("FTN stands for FidoNet Technology Network") so BM25 matches "what is X" / "what does X stand for". Comma-separated path list; first file wins on duplicate acronyms (list local before generic). - syncfact.js: reads a flat one-fact-per-line file (data/syncfact.lst), one chunk per fact; relative path resolves under data_dir. Add to index_sources to include them on the next index rebuild. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  224. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: mechanical-phrasing rewrite tolerates an adjective "No relevant hits found for X" slipped past the empty-result rewrite because an adjective ("relevant"/"matching"/"specific"/...) sat between "no" and the noun. Allow an optional adjective in both the "no <noun>" and "couldn't find any <noun>" branches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  225. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: extend dangling-ref + deeper entity-decode + ".," cleanup Three postprocess hygiene fixes from live watching: - Dangling non-link strip now matches "this resource/page/article/ guide/entry/write-up", not just "this link" (e.g. "...check out this resource: FidoNet Basics" with no URL). - Entity-decode loop cap raised 3 -> 6 passes: the 7B was seen QUADRUPLE-escaping a wiki URL ("&amp;amp;amp;lt;..."), which the 3-pass cap left as "&lt;". - Collapse the "item., item" period-then-comma artifact (from the numbered-list -> inline conversion) into a single comma. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  226. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/llm_tools/external_archives.js diff
    external_archives: redirect Synchronet-feature queries to the wiki When the query pairs Synchronet/sbbs with a technical or config term ("which TLS algorithms does Synchronet support", "Synchronet MQTT config"), return count:0 + a note steering to the wiki instead of the documentary Synchronet entry. The model was calling external_archives for Synchronet feature questions and answering from a history blurb; those belong on wiki.synchro.net (already in the RAG context). Plain history queries ("tell me about Synchronet", "who created Synchronet") still get the curated entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  227. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: rewrite mechanical empty-tool-result phrasing When a tool returns no hits, the model sometimes echoes a database-style "No results found for X" / "I couldn't find any info on X" -- which reads like an error, not the guru. final_reply_postprocess() now rewrites a whole-reply no-results admission into the guru's natural voice ("haven't come across anything on X"), preserving the topic. Anchored to the full trimmed reply so it only fires when the WHOLE reply is that admission, never on a sentence that merely says "found". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  228. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: strip dangling "check out this link: <name>" (no URL) The 7B model often appends a link reference that names a resource but never produces a URL ("...for more info, check out this link: BBS Documentaries"). strip_fake_urls() can't catch it -- there is no URL to strip. final_reply_postprocess() now removes the trailing clause, from a lead-in verb through "(this|the) link[:] <name>" to the end, but only when the clause carries no http(s) URL (callback check), so real citations survive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  229. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: iterate HTML-entity decode for double-escaped replies The 7B model sometimes DOUBLE-escapes ("&amp;lt;...&amp;gt;"); the single-pass decode peeled only one layer and left "&lt;...&gt;" literal in the reply (seen in a live "who is Dr Seuss?" turn). Decode in a bounded loop (max 3 passes) until the string stops changing, so double-/triple-escaped entities fully resolve. Safe because a chat reply never legitimately contains a literal "&lt;". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  230. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: strip bare tool-name leaks from replies The model sometimes names an internal tool in user-facing prose without parens -- e.g. "...checking out the documents linked from external_archives: <url>" or "the this_bbs tool shows...". The leak-strip only handled the with-parens form (and didn't list external_archives at all). Add external_archives to that list and add a bare-name strip (optional lead-in preposition/article + tool name + optional "tool" + trailing colon). Leftover whitespace is collapsed by the existing cleanup pass; URLs in the sentence survive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  231. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: decode HTML entities + don't archive-route Synchronet how-tos Two fixes surfaced while testing "pkzip" queries: 1. final_reply_postprocess() now HTML-entity-decodes replies. The 7B model occasionally emits "&lt;http://...&gt;" (an autolinked URL in angle brackets) or "&amp;" inside a URL; plain-text IRC/terminal then showed the literal entities. Decode &lt;/&gt;/&quot;/&#39;/ &apos; then &amp; (last, to avoid double-decoding) before the markdown-strip and URL-validation passes. 2. classify_intent() no longer pre-routes to external_archives when the query is a Synchronet how-to/config question that merely NAMES a BBS-era topic. "how do I configure pkzip in sbbs?" was forced to external_archives and answered with SEA v. PKWARE lawsuit trivia; docstyle-shaped queries now skip the archive route and fall through to the model + wiki RAG. Pure-history phrasing ("how did the SJG raid happen", "tell me about pkzip") is not docstyle, so it still routes to the archive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  232. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    javascript skill: how to syntax-check/test a side-effecting module Document the guard-and-load() idiom: gate a module's load-time entry point (server loop, socket connect, greeting) behind a NO_MAIN-style sentinel so it can be load()ed under jsexec for syntax-checking and unit-testing its helpers without running the side effect. Cites the stock CHAT_LLM_NO_STANDALONE / CHAT_LLM_IRC_NO_MAIN guards. Notes that syncjslint.js is a style linter (jslint) with false positives on valid SpiderMonkey regex, not a reliable syntax gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  233. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm_irc.js diff
    chat_llm_irc: add per-user mute/unmute A user can address the bot with "mute me" (or "shut up", "be quiet", "stfu", "leave me alone", ...) to stop it replying to them and stop it auto-intervening on their questions; "unmute me" (or "talk to me", "come back", ...) resumes. Per-user, keyed by lowercased nick, persisted to data/<bot_file_base>_mute.json so a restart doesn't silently un-mute everyone. Mute/unmute are honored even while muted, so a user can always bring the bot back. IRC-only by design: IRC is the only context where the guru speaks unprompted (direct address + high-confidence intervention). The Terminal Server guru-paging and private/jsexec chat are pull-based, so there is nothing to mute there. Also adds a CHAT_LLM_IRC_NO_MAIN guard so the adapter can be loaded for unit testing without connecting a duplicate bot (mirrors chat_llm.js's CHAT_LLM_NO_STANDALONE). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  234. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/chat_llm.js diff
    chat_llm: skip RAG for identity/social queries inject_retrieval() fired on any query whose top BM25 hit cleared the score floor -- including self-referential questions like "what's your name?" (the token "name" scores well), injecting board/wiki chunks that the model then conflated with its own identity ("I'm Vertrauen BBS"). Add _is_conversational_query() and gate inject_retrieval() on it: identity ("who/what are you", "your name", "are you a bot/sysop"), capability, and social-pleasantry queries now retrieve nothing and are answered from the system prompt's identity rules alone. Kept narrow so genuine knowledge questions (incl. "who is the sysop?") still retrieve. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  235. Rob Swindell (on Debian Linux)
    Fri May 29 2026 23:59:17 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm.ini diff
    exec/chat_llm.js diff
    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>
  236. Rob Swindell (on Windows 11)
    Fri May 29 2026 16:29:43 GMT-0700 (PDT)
    Modified Files:
    

    CLAUDE.md diff
    docs: document the release-notes (v3*_new*) update convention in CLAUDE.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  237. Rob Swindell (on Windows 11)
    Fri May 29 2026 16:17:14 GMT-0700 (PDT)
    Modified Files:
    

    docs/v322_new.md diff
    docs/v322_new: note the LLM-backed Guru chat features Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  238. Rob Swindell (on Debian Linux)
    Fri May 29 2026 15:58:44 GMT-0700 (PDT)
    Modified Files:
    

    CLAUDE.md diff
    CLAUDE.md: document the Synchronet directory hierarchy Add a "Directory hierarchy" section mapping each install sub-directory (ctrl/, data/, exec/, exec/load/, mods/, text/) to its purpose and its system.*_dir accessor, with the practical rule that drives file placement: configuration and hand-curated data go in ctrl/, generated run-time data in data/, and exec/ is for code only. Notes that the locations are SCFG-configurable so code must use system.*_dir, never hardcoded paths. Links to wiki.synchro.net/dir: as the canonical ref. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  239. Rob Swindell (on Debian Linux)
    Fri May 29 2026 15:58:44 GMT-0700 (PDT)
    Added Files:
    

    ctrl/llm_external_archives.json diff
    Modified Files:

    exec/chat_llm.js diff
    exec/llm_tools/external_archives.js diff
    chat_llm: move curated archive index to ctrl/, fix stale loader path llm_external_archives.json is hand-curated configuration for a shipped component (the chat_llm external_archives tool), so it belongs in ctrl/ alongside chat_llm.ini -- not exec/, which is for code. Move it there and point both readers (chat_llm.js, llm_tools/external_archives.js) at system.ctrl_dir. This also fixes a latent bug: chat_llm.js's strip_fake_urls() loaded its archive URL whitelist from 'chat_external_archives.json' -- a stale name left over from the chat_->llm_ rename that never existed -- so the valid set was empty and every legitimate textfiles.com/bbsdocumentary.com URL the external_archives tool produced was stripped out of the bot's reply. Update the data file's stale _comment (chat_tools.js -> llm_tools/external_archives.js). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
  240. Rob Swindell (on Windows 11)
    Fri May 29 2026 15:30:15 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    skills/javascript: note the chat_llm LLM-Guru module family Add the chat_llm engine, chat_llm_irc adapter, llm_tools, and llm_index to the stock exec/*.js inventory, plus wiki links to the new module/config/ howto pages now published on wiki.synchro.net. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
  241. Rob Swindell (on Windows 11)
    Fri May 29 2026 15:30:09 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/chat_llm.ini diff
    exec/chat_llm_irc.js diff
    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>
  242. Rob Swindell (on Windows 11)
    Fri May 29 2026 11:52:41 GMT-0700 (PDT)
    Added Files:
    

    install/systemd/chat_llm_irc.service diff
    install/systemd/llm_index.service diff
    install/systemd/llm_index.timer diff
    install/systemd: add chat_llm_irc + llm_index units Three example systemd units for the LLM-backed chat infrastructure, modeled on broker.service: chat_llm_irc.service long-running IRC bot (exec/chat_llm_irc.js). Only needed if the deploy serves chat via IRC; terminal-side guru paging and multinode channel guru run in-process inside sbbs.service. llm_index.service oneshot RAG index rebuild via exec/llm_index.js. Common to every chat_llm consumer; refreshes the BM25 index the retrieval side reads. llm_index.timer nightly trigger for llm_index.service, with a 15-min random delay and Persistent=true so missed runs catch up on next boot. For sysops migrating the chat_llm service to a dedicated Linux host (e.g. cvs.synchro.net): copy to /lib/systemd/system, tweak SBBSCTRL / User / Group / paths via `systemctl edit`, then enable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  243. Rob Swindell (on Windows 11)
    Fri May 29 2026 02:06:37 GMT-0700 (PDT)
    Modified Files:
    

    CLAUDE.md diff
    CLAUDE.md: note portability between MSVC, GCC, and Clang The project's C/C++ has to compile under MSVC, GCC, and Clang (plus Borland for the legacy GUIs). MSVC tends to be the most permissive; a clean MSBuild is not a full verification. Documents the goto-crosses-initialization pitfall that broke chat.cpp under GCC (introduced in 683147f9c, fixed in 2a0ee8dd1), plus other MSVC-vs-GCC/Clang divergences worth watching when only one toolchain is available locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  244. Rob Swindell (on Windows 11)
    Fri May 29 2026 02:00:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/chat.cpp diff
    chat.cpp: hoist declarations to satisfy GCC's "goto crosses init" rule Build broke on GCC (linux-x64 CI) in chat_llm_session() and chat_llm_multinode_turn() -- introduced in 683147f9c -- because the gotos to js_done / mt_done / cleanup labels skipped past initializations of local variables that were still in scope at the label. MSVC accepts this with at most a warning; GCC errors. Fixes: - chat_llm_session(): hoisted `bool supports_utf8` above any `goto cleanup`. Hoisted `double speed_factor` / `bool sim_typos` to the top of the JS_BEGINREQUEST block, above any `goto js_done` from within (they were below the gotos but in the same scope as the label). - chat_llm_multinode_turn(): hoisted `bool supports_utf8` above any `goto mt_done`. Wrapped the `JSString* input_str` / `jsval chat_args[]` declarations in an inner brace block so they fall out of scope before `mt_done:` instead of straddling it. Verified locally by single-file MSVC build of chat.cpp (0 errors, 0 warnings). Structural fix targets exactly the cross-init errors GCC reported. CI failure: https://gitlab.synchro.net/main/sbbs/-/jobs/1509516 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  245. Rob Swindell (on Windows 11)
    Fri May 29 2026 00:54:31 GMT-0700 (PDT)
    Added Files:
    

    ctrl/chat_llm.ini diff
    ctrl/chat_llm_greeting.utf8 diff
    ctrl/chat_llm_opening_persona.utf8 diff
    ctrl/chat_llm_persona.utf8 diff
    exec/chat_llm.js diff
    exec/chat_llm_irc.js diff
    exec/llm_external_archives.json diff
    exec/llm_index.js diff
    exec/llm_index/dokuwiki.js diff
    exec/llm_index/filebase.js diff
    exec/llm_index/msgbase.js diff
    exec/llm_tools.js diff
    exec/llm_tools/_common.js diff
    exec/llm_tools/bbs_directory.js diff
    exec/llm_tools/external_archives.js diff
    exec/llm_tools/relay_message.js diff
    exec/llm_tools/this_bbs.js diff
    chat_llm: introduce LLM-backed chat engine with tool calling and RAG Adds an Ollama-backed chat infrastructure for the BBS guru: exec/chat_llm.js -- chat engine: dispatch, classifier, tool loop, RAG injection, postprocess exec/chat_llm_irc.js -- IRC bot adapter (joins channels, runs chat_session on direct address or high-confidence intervention, queues and delivers relay messages) exec/llm_tools.js -- tool registry: loads exec/llm_tools/*.js and registers each via llm_tool_register ({name, execute, def}) exec/llm_tools/_common.js shared helpers exec/llm_tools/bbs_directory.js sbbslist lookup + finger probe exec/llm_tools/this_bbs.js local subs/libs/dirs/doors/stats exec/llm_tools/external_archives.js curated BBS-era archive index exec/llm_tools/relay_message.js deferred-delivery message queue exec/llm_index.js -- RAG index builder (BM25) exec/llm_index/dokuwiki.js DokuWiki page crawler exec/llm_index/filebase.js file-base description crawler exec/llm_index/msgbase.js message-base post crawler exec/llm_external_archives.json archive data ctrl/chat_llm.ini engine + chat config ctrl/chat_llm_persona.utf8 system prompt, normal turns ctrl/chat_llm_opening_persona.utf8 system prompt, opening turn ctrl/chat_llm_greeting.utf8 user-role greeting trigger Adding a tool = drop a file in exec/llm_tools/; the loader picks it up automatically. Runtime files derive from <persona>_<protocol> base (default "guru_irc") so multiple bots can coexist in data/. The engine speaks Ollama's OpenAI-compat /api/chat endpoint; tested against qwen2.5:7b with the Synchronet wiki + filebases as RAG sources. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  246. Rob Swindell (on Windows 11)
    Fri May 29 2026 00:48:20 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/chat.cpp diff
    src/sbbs3/js_console.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/scfg/scfgchat.c diff
    src/sbbs3/scfg/scfgindex.h diff
    src/sbbs3/scfgdefs.h diff
    src/sbbs3/scfglib2.c diff
    src/sbbs3/scfgsave.c diff
    chat.cpp/scfg: dispatch guru paging to a JS module Adds infrastructure for a JS-driven guru chat engine, gated on a new per-guru "module" config field: src/sbbs3/scfgdefs.h new guru_t.module field (LEN_CODE+1) src/sbbs3/scfglib2.c read module= from chat.ini's [guru:CODE] src/sbbs3/scfgsave.c write module= back to chat.ini src/sbbs3/scfg/scfgchat.c "Module" line in SCFG's guru menu + helpbuf src/sbbs3/scfg/scfgindex.h register the option in SCFG's option index src/sbbs3/sbbs.h declarations for the dispatch methods src/sbbs3/chat.cpp dispatch + simulate_type src/sbbs3/js_console.cpp console.simulate_type() JS binding When cfg.guru[gurunum]->module is empty (the default for every existing install), the legacy guruchat() loop runs unchanged. When module is set, sbbs_t::chat_llm_session(gurunum) takes over 1:1 paging by load()ing exec/<module>.js and invoking its chat_session(input, ctx) function. Channel-guru turns route to chat_llm_multinode_turn() (one- shot per user line, no session state). Both engines log to data/logs/guru.log in the legacy format, so sysops see all guru conversations together regardless of which engine handled them. Also adds sbbs_t::simulate_type() with optional fat-finger / transposition typo simulation, plus a console.simulate_type() JS binding (with UTF-8 -> CP437 inplace conversion for non-UTF8 terminals) so a JS module can stream tokens out as they arrive in the streaming response without typing them out of order. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  247. Rob Swindell (on Windows 11)
    Fri May 29 2026 00:35:25 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/CLAUDE.md diff
    scfg/CLAUDE.md: document helpbuf two-space sentence convention Helpbuf text uses ". " between sentences (period + double-space + capital), matching the in-tree convention. Mixed single/double spacing within one help block reads as inconsistent at runtime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  248. Rob Swindell (on Windows 11)
    Fri May 29 2026 00:13:50 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/jsexec/SKILL.md diff
    .claude/skills/jsexec: per-user ARS check pattern + stdout buffering gotcha Two findings from chat-index work and regression-suite debugging: 1. Checking access against a specific user under jsexec. The natural-looking accessors (sub.can_read, dir.can_download) evaluate against the implicit session user -- which under jsexec doesn't exist, so they return true for everything. Document the reliable pattern: instantiate User(N) and walk the ownership chain calling u.compare_ars() on each ARS string conjunctively. Also note that the global "user" lowercase = useron in C++ -- it is undefined under jsexec, distinct from the User constructor. 2. stdout / stderr / stdin are global File instances. They have a .flush() method that fflushes the underlying FILE*. The gotcha: print() goes through C stdio, which block-buffers (~4KB) when stdout is redirected to a file or pipe. A long-running script looks stalled under tail -f even though it's progressing. Route output through stdout.writeln() + stdout.flush() to make it line-buffered against the redirected FD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  249. Rob Swindell (on Windows 11)
    Thu May 28 2026 01:25:51 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/http.js diff
    http.js: add PostStreaming() for HTTP/1.1 chunked-transfer responses Adds three methods to HTTPRequest: - SetupPostStreaming() -- like Post()'s setup but HTTP/1.1 and no automatic body collection - ReadChunkedBody(on_chunk) -- reads chunked-transfer-encoded body, invokes on_chunk(text) per chunk - PostStreaming(url, data, on_chunk, ...) -- POST + streamed read; returns the full accumulated body after the stream completes. Falls back gracefully if the server returns non-chunked Content-Length (single on_chunk call with the whole body). Needed by exec/chat_llm.js for streaming Ollama responses (SSE / chunked transfer) in the LLM-chat module. Purely additive -- existing Post() / Get() behavior unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  250. Rob Swindell (on Debian Linux)
    Wed May 27 2026 22:35:56 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/upgrade_to_v319.c diff
    upgrade_to_v319: refuse to clobber already-upgraded file bases After smb_open(), check smb.status.total_msgs and skip any dir with existing files; require -f on the command line to force overwrite. Previously the tool unconditionally re-ran smb_create() on every dir, destroying populated SMB file bases when re-run on an upgraded system. Also remove the unused overwrite() helper and add -h/-? usage. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  251. Rob Swindell (on Windows 11)
    Wed May 27 2026 22:24:01 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/ini_file.c diff
    xpdev: initialize p in iniParseSections() to silence /sdl C4703 CI job 1507910 (windows-x86 [sbbs], commit 311ed074) failed with a hard C4703 ("potentially uninitialized local pointer variable 'p' used") at ini_file.c:1709 from sbbsexec.vcxproj, which uses the VS2017 toolset (MSVC 14.16) and /sdl (which elevates C4703 to error). The use is actually safe — the preceding `for (i = 0; list[i] != NULL; ++i)` loop runs at least once (list[0] != NULL was just checked) and sets p on every iteration — but the older toolset's per-TU data-flow can't see that proof. Whole-program optimization could; 311ed074 disabled WPO for Release|Win32, removing the cover. The faulty data-flow chain (early return + extended use of p outside the discovery loop) was introduced in 48edf261f0; initializing p at its declaration silences the warning and makes the invariant explicit at zero runtime cost.
  252. Rob Swindell (on Windows 11)
    Wed May 27 2026 22:14:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/Directory.Build.targets diff
    sbbs3: also disable WPO/LTCG for Release|Win32 builds Extends the Release|Win32 toolchain pruning in 266083317. That commit suppressed PDBs to take mspdbsrv.exe out of the picture; CI job 1507619 then surfaced the next layer — a `link.exe` LTCG-pass ICE at `ars.c(35)` (compiler file `p2/main.cpp` line 258), still random across runs. Same shape of failure (shared toolchain subprocess, random TU), different subsystem: the LTCG backend (c2.dll UTC) re-codegens objects compiled with `/GL` at link time. Adds `WholeProgramOptimization=false` (drops `/GL`) and `LinkTimeCodeGeneration=Default` (drops `/LTCG:incremental`) for Release|Win32 across every sbbs3 vcxproj, eliminating LTCG as a shared CI dependency. WPO inlining is not load-bearing for these I/O-bound BBS binaries.
  253. Deucе
    Wed May 27 2026 21:33:37 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    Use fast millisecond timer for Wren scheduling Use xp_fast_timer64_ms() for the Wren transfer pump throttle and for the internal Wren timer queues. These call sites only need millisecond granularity, but they can be reached very frequently from the terminal receive path when Wren is active. Keep the existing semantic gates in inline_transfer_pump_wren_() before the timer read, so inactive Wren sessions and transfer worker paths still avoid the scheduling work entirely. Store Timer.trigger() and Hook.every() deadlines as millisecond ticks instead of long double seconds. This avoids repeated xp_timer() calls in the dispatch/sweep path while preserving the existing catch-up behavior for stalled recurring timers.
  254. Deucе
    Wed May 27 2026 21:32:36 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/genwrap.c diff
    src/xpdev/genwrap.h diff
    Add fast millisecond timer wrapper Add xp_fast_timer64_ms() beside the existing xp_fast_timer64() wrapper. The new helper keeps the coarse/fast clock preference used by the seconds timer, but returns millisecond ticks for callers that need sub-second scheduling without the full precision cost of xp_timer(). On Unix this prefers CLOCK_MONOTONIC_COARSE or CLOCK_MONOTONIC_FAST when available, then falls back through the existing monotonic choices. On Windows it mirrors the fast timer path through GetTickCount(), which already maps to GetTickCount64() on newer WINVER builds.
  255. Deucе
    Wed May 27 2026 17:29:12 GMT-0700 (PDT)
    Added Files:
    

    src/conio/scale_spinner.c diff
    Modified Files:

    src/conio/CMakeLists.txt diff
    src/conio/GNUmakefile diff
    src/conio/scale.c diff
    Optimize scaled row interpolation The scaler commonly applies integer scaling before the final aspect-correction interpolation. That produces repeated adjacent rows, but interpolate_height() still blended those rows pixel by pixel. Track the vertical repeat count from multiply_scale() and let interpolate_height() copy rows that are known duplicates. Also replace its static temporary row buffers with direct source row pointers and use the actual row byte count. This removes thread-hostile static state and avoids unnecessary row copies. On the scale_spinner X11 profiling run with a dense 0xB2 screen, sampled totals dropped from 4,132 to 2,630 (-36.4%). do_scale samples dropped from 3,407 to 1,844 (-45.9%), and interpolate_height dropped from 3,186 to 1,580 (-50.4%). Add a small scale_spinner profiling binary target for exercising one-cell updates under internal/external scaling.
  256. Rob Swindell (on Windows 11)
    Wed May 27 2026 13:47:45 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/Directory.Build.targets diff
    sbbs3: disable PDB generation for Release|Win32 builds Adds src/sbbs3/Directory.Build.targets, auto-imported by MSBuild after every sbbs3 .vcxproj's body, which overrides DebugInformationFormat to None in ClCompile and GenerateDebugInformation to false in Link for the Release|Win32 configuration across all 33 projects (including scfg/scfg.vcxproj). The motivation is sporadic, non-deterministic "internal compiler error" failures observed on the Windows GitLab runner -- random source file and line each run, persisting after AV exemption and single-instance runner limiting. The root cause is mspdbsrv.exe (the shared PDB writer spawned by cl.exe under /Zi) wedging or thrashing across the seven sequential builds the windows-x86 [sbbs] job performs in one shot; mspdbsrv outlives a single cl invocation and a single MSBuild process, so per-runner throttling does not help. Eliminating the compiler /Zi and linker /DEBUG settings removes mspdbsrv from the Release pipeline entirely. Verified by building ans2asc.vcxproj Release: produces a working .exe (~42% smaller, no debug sections), no .pdb in either the output dir or the intermediate dir, and MSBuild evaluates _DebugSymbolsProduced as false. Debug builds are unaffected. The CI artifact list in .gitlab-ci.yml never shipped .pdb files anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  257. Rob Swindell (on Debian Linux)
    Wed May 27 2026 13:07:35 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/prntfile.cpp diff
    sbbs_t::menu_exists: fall back to default menu dir if not found in menu_dir When menu_dir is set (the override sub-directory of text/menu/), menu file lookups now retry in the default menu directory if no matching file (any extension or width variant, mods overlay included) exists in the subdir. Since this lives in menu_exists(), all callers — sbbs_t::menu(), random_menu(), and others — get the fallback.
  258. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgnet.c diff
    scfg: rich helpbuf for Network Configuration (Networks) root menu Replace the one-line stub with a per-sub-menu summary of all four entries (Internet E-mail, QWK Packet Networks, FidoNet EchoMail and NetMail, MQTT), each pointing the sysop at the right sub-menu for the network they care about and cross-referencing the related server-side configuration where applicable. This wraps up the Tier-1 SCFG helpbuf fill-out: every user-facing section intro now has per-entry orientation instead of a "this menu contains options" stub.
  259. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgmsg.c diff
    scfg: rich helpbuf for Message Areas > Message Options menu Replace the three-line stub with per-option help for all 18 entries (BBS ID, retry time, QWK packet limits, e-mail purging and dedup, anonymous / quoting / upload / forward / kill-read toggles, real-name delivery, signatures, deleted-message visibility, MailBase storage method, guest scan depth).
  260. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsub.c diff
    scfg: rich helpbuf for Sub-board Toggle Options menu Replace the three-line stub with per-toggle descriptions for all 22 toggles + the Extra Attribute Codes sub-menu (privacy / anonymity / real-name posting, edit/delete permissions, scan defaults, voting / quoting / tagging, signature suppression, operator-msg permanence, LZH compression, markup-code handling, word-wrap, 80-column forcing, UTF-8 auto-detect, @-code expansion in sysop msgs, new-sub template).
  261. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgxtrn.c diff
    scfg: rich helpbuf for External Programs menu Replace the two-line stub with per-sub-menu descriptions for all 6 entries (Fixed Events, Timed Events, Native Program List, Message Editors, Global Hot Key Events, Online Programs/Doors). Intro clarifies these all run under the Terminal Server (events on its event thread, the rest in online-user node context). Notable corrections vs. the old draft: - Global Hot Key Events aren't "sysop hot-keys" -- they're gated by per-event ARS and any user meeting them can trigger. - Message-editor example uses SlyEdit (the actual popular editor) rather than the invented "SyncEdit".
  262. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsys.c diff
    src/sbbs3/scfg/scfgxfr1.c diff
    scfg: rich helpbuf for File Areas > File Options menu Replace the three-line stub with per-option/sub-menu descriptions for all 16 entries (min disk space, batch queue caps, user-transfer cap, upload/download credit percentages, leech detection, filename rules, archive formats, and the six per-handler sub-menus: viewers, testers, download events, extractors, compressors, transfer protocols). Tightens these in passing: - Names ZMODEM / YMODEM / XMODEM in all-caps (per Forsberg's original casing); also catches one stray "Zmodem" in the New User Values helpbuf (scfgsys.c). - File-options take-effect note is "after Terminal Server is recycled" — the file-area cfg struct isn't reloaded mid-session, contrary to an earlier draft of this helpbuf.
  263. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsys.c diff
    scfg: rich helpbuf for System > Security Options menu Replace the three-line stub with per-option help for all 17 entries (System Password, password-prompt timing, sysop access, login methods, password policy, deletion grace, inactivity auto-delete, registration gate, TLS self-signing, and pointers to the three security-values sub-menus).
  264. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsys.c diff
    scfg: rich helpbuf for System > New User Values menu Replace the four-line stub with per-option/sub-menu descriptions for all 17 entries (Level, Flag Sets, Exemptions, Restrictions, Expiration Days, Credits, Minutes, Editor, Command Shell, Download Protocol, Days of New Messages, Gender Options, Default Toggles, QWK Packet Settings), including: - "Minutes" framed as the minute-bank starting balance, distinct from session time-per-call. - "Command Shell" notes shells can be Baja or JavaScript. - Restriction example uses 'C' (no chat) rather than the confusingly-overloaded 'O' (use alias only). Also fixes a "thes" -> "these" typo carried over from the original stub.
  265. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsys.c diff
    scfg: rich helpbuf for System Advanced Options menu Replace the one-line "care should be taken" stub with a per-option breakdown for all 23 options (Magic Word, directory paths, SIF files, credit/minute conversion rates, backups, log rotation, inactivity limits, control-key pass-through, statistics interval, filter-file cache). Press-F1-for-detail callout points sysops at the existing rich per-option helpbufs for full content. Notable corrections vs. the legacy stubs: - "Maximum Number of Minutes" reframed as the minute-bank cap (these are banked minutes a user accumulates, not session time). - "Credits Per Dollar" drops the misleading "use powers of 2" shorthand: credits are a byte-denominated download budget and the 100 KiB conversion block isn't itself a power of 2, so any value works -- binary-aligned values (1M / 2M / 4M) are just convention.
  266. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsys.c diff
    scfg: rich helpbuf for System Configuration (System) root menu Replace the two-line stub with per-option/sub-menu descriptions for all 13 entries (BBS Name, Location, Local Time Zone, Short Date Format, Operator, Notifications, Toggle Options, New User Values, New User Prompts, Security Options, Advanced Options, Loadable Modules, Extra Attribute Codes), framing each sub-menu so a sysop can pick the right one without trial-and-error.
  267. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgnet.c diff
    scfg: rich helpbuf for Networks > FidoNet EchoMail and NetMail menu Replace the three-line stub with per-option help for all 13-14 options (System Addresses, Default Origin Line, NetMail/EchoMail semaphores, NetMail Directory, Allow Sending, File Attachments, Send Using Alias, Crash/Direct/Hold defaults, Kill After Sent, Cost, Choose Source Address). Names BinkIT (mailer) and SBBSecho (tosser) as the bundled Synchronet pairing on the message-base/tosser distinction.
  268. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/CLAUDE.md diff
    src/sbbs3/scfg/scfgnet.c diff
    scfg: rich helpbuf for Networks > Internet E-mail menu Replace the two-line stub menu intro with a per-option breakdown (System Address, Inbound/Outbound semaphores, Allow Sending, Allow File Attachments, Send Using Alias, Kill After Sent, Cost) that explains this menu sets *user-facing* policy for outbound SMTP — not server-side delivery, which lives under Servers > Mail Server. Also tighten src/sbbs3/scfg/CLAUDE.md: pack lines close to the 72 visible-col ceiling instead of leaving 10+ chars of slack, which produces a column of short lines that reads as choppy.
  269. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsrvr.c diff
    scfg: rich helpbuf for SendMail Support menu (Mail Server) Replace the two-line stub with a per-option breakdown (Enabled, Rescan Interval, Connect Timeout, Auto-exempt Recipients, Max Delivery Attempts, Delivery Method, Relay Server Address/Port/Authentication/ Username/Password) explaining direct-vs-relay delivery and when each mode is appropriate.
  270. Rob Swindell (on Debian Linux)
    Mon May 25 2026 01:31:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/CLAUDE.md diff
    src/sbbs3/scfg/scfgsrvr.c diff
    scfg: rich helpbuf for Services Server Settings menu Replace the one-line "for full documentation, see ..." stub with a per-option breakdown (Enabled, Log Level, Network Interfaces, Lookup Client Hostname, Configuration File, Login Requirements, Login Info Save, Limit Rate of Connections, JavaScript Settings, Failed Login Attempts) following the format used by rate_limit_cfg / js_startup_cfg. Also extend src/sbbs3/scfg/CLAUDE.md with two clarifications uncovered while writing this: - 72 columns is the renderer's hard wrap (help window width 76 minus 2 borders minus 2x1 pad in showbuf()), not a stylistic target; longer lines auto-wrap mid-word. - The wrap budget is *visible* width: ` and ~ are highlight/inverse toggles consumed by WIN_HLP rendering, plus control bytes 1/2. - Don't cross-reference sbbsctrl in helpbufs; SCFG is cross-platform but sbbsctrl is Windows-only.
  271. Rob Swindell (on Windows 11)
    Sun May 24 2026 21:37:18 GMT-0700 (PDT)
    Added Files:
    

    docs/v322_new.md diff
    docs: add v322_new.md draft for in-progress v3.22 release Initial draft of the v3.22 "What's New" release notes, covering the ~940 commits on master since the v3.21e tag (sbbs321e). Markdown format with 79-column hand wrapping, mirroring the section layout of prior docs/v3xx_new.txt files: General, Servers, per-server, SBBSecho, SCFG, Customization, Stock Modules, JavaScript, JSexec, chksmb/fixsmb/smbutil, SFTP Server, MQTT Broker, qtmonitor, and sbbsctrl. Will be amended throughout the v3.22 dev cycle. The title letter ('a') is a placeholder -- actual release will be v3.22b or later. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  272. Rob Swindell (on Windows 11)
    Sun May 24 2026 18:08:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfgdefs.h diff
    Fix misleading comments (code suffixes and short codes are 16 chars max now)
  273. Rob Swindell (on Windows 11)
    Sun May 24 2026 15:54:58 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/CLAUDE.md diff
    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/ftpsrvr.h diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/mailsrvr.h diff
    src/sbbs3/ratelimit_filter.hpp diff
    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgsrvr.c diff
    src/sbbs3/services.cpp diff
    src/sbbs3/services.h diff
    src/sbbs3/startup.h diff
    src/sbbs3/websrvr.cpp diff
    src/sbbs3/websrvr.h diff
    startup: consolidate per-server rate_limit_* fields into struct rate_limit_settings The six rate-limit knobs (prefix4, prefix6, filter, filter_duration, filter_silent, filter_subnet_threshold) added to all four per-server startup_t structs by d7c823c9d landed as identical 6-field blocks repeated verbatim across websrvr.h, ftpsrvr.h, mailsrvr.h, services.h. Lift them into a shared `struct rate_limit_settings` in startup.h next to the existing `struct max_concurrent_settings` / `struct login_attempt_settings` patterns, and embed by value (`struct rate_limit_settings rate_limit;`) in each *_startup_t. ABI is preserved: a nested struct is byte-contiguous in declared order, so the on-disk/in-memory layout of each *_startup_t is unchanged. sbbsctrl (which doesn't reference any of these fields by name -- only checks the outer struct size) is unaffected and does not need to be rebuilt for this change. Downstream consolidations enabled by the new struct: - sbbs_ini.c: 8 copies of the 6-field block (4 read + 4 write) collapse into one get_rate_limit_settings() + one set_rate_limit_settings() helper pair, mirroring the existing get/set_login_attempt_settings. - scfg/scfgsrvr.c: rate_limit_cfg_view shrinks from 11 fields to 6 (the 6 individual rate-limit scalar pointers become one `struct rate_limit_settings* rl`). The per-field NULL guards inside rate_limit_cfg() for those knobs collapse out (they are always populated now); the three autofilter_on-guarded blocks merge for the same reason. The four per-server wrappers drop from 11-line to 6-line designated initializers. - ratelimit_filter.hpp: rate_limit_key() and rate_limit_filter() take a `const struct rate_limit_settings*` instead of 2 and 4 scalar args respectively, simplifying the call sites in all four server .cpp files (web request + connect paths, ftp request, mail SMTP + POP3, services connect). Net: -77 lines across the touched files, no functional change. Tested: Release|Win32 build clean for sbbs.dll, ftpsrvr.dll, mailsrvr.dll, services.dll, websrvr.dll, scfg.exe; user-verified SCFG menus and runtime behavior. Also document the consolidation pattern in src/sbbs3/CLAUDE.md (new "Prefer consolidation when the same lines repeat" section, citing the sub-struct / ini-helper / scfg-view / shared-hpp recipes with concrete Synchronet references) so the next per-server block that starts to accumulate the same N-line cluster gets folded the same way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  274. Rob Swindell (on Windows 11)
    Sun May 24 2026 15:52:28 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    .claude/skills/javascript: document constant namespaces and terminal-capability checks Two new subsections after load()/require(): one breaks down where constants come from (host-injected globals like LOG_*/INVALID_SOCKET, class-static properties like FileBase.DETAIL/SORT and CryptContext.ALGO_*, and JS-library files under exec/load/ that need load()/require()); the other distinguishes console.term_supports(USER_UTF8) (live connection capability) from user.settings & USER_UTF8 (stored preference) and explains when to use which. Triggered by C-vs-JS namespace confusion (bare UTF8 in sbbsdefs.h vs USER_UTF8 in exec/load/userdefs.js, same (1<<29) value) hit during LLM-guru language-output work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  275. Rob Swindell (on Debian Linux)
    Sun May 24 2026 15:00:58 GMT-0700 (PDT)
    Modified Files:
    

    CLAUDE.md diff
    CLAUDE.md: forbid creating identifiers in the C/C++ reserved namespace Codifies the header-guard rename in ecb854b2c as a project-wide rule for future code, with guidance to fix opportunistically rather than mass-rewrite the existing tree.
  276. Rob Swindell (on Debian Linux)
    Sun May 24 2026 14:59:48 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ratelimit.hpp diff
    src/sbbs3/ratelimit_filter.hpp diff
    sbbs3: rename ratelimit header guards out of reserved namespace Identifiers beginning with an underscore followed by an uppercase letter are reserved by the C/C++ standard for the implementation.
  277. Rob Swindell (on Debian Linux)
    Sun May 24 2026 14:31:31 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/javascript/SKILL.md diff
    .claude/skills/jsexec/SKILL.md diff
    .claude/skills/logs/SKILL.md diff
    .claude/skills/menus/SKILL.md diff
    .claude/skills/mqtt/SKILL.md diff
    .claude/skills/smbutils/SKILL.md diff
    .claude/skills/text/SKILL.md diff
    .claude/skills: replace invented $SBBS env var with <sbbs> placeholder $SBBS isn't a standard Synchronet env var (the real ones are SBBSCTRL, SBBSNODE, SBBSEXEC) and isn't set on user systems. The shorthand misled another Claude session into emitting `gmake $(SBBS)` as if it were a real make variable. Use the literal `<sbbs>` placeholder instead -- visually obvious as a fill-in, no false implication of a shell var. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  278. Deucе
    Sun May 24 2026 13:13:47 GMT-0700 (PDT)
    Added Files:
    

    exec/examples/sqlite_example.js diff
    exec/tests/sqlite/basic.js diff
    exec/tests/sqlite/bind.js diff
    exec/tests/sqlite/coercion.js diff
    exec/tests/sqlite/declared_types.js diff
    exec/tests/sqlite/errors.js diff
    exec/tests/sqlite/extras.js diff
    exec/tests/sqlite/fk.js diff
    exec/tests/sqlite/injection.js diff
    exec/tests/sqlite/insert.js diff
    exec/tests/sqlite/record.js diff
    exec/tests/sqlite/remove.js diff
    exec/tests/sqlite/schema.js diff
    exec/tests/sqlite/select.js diff
    exec/tests/sqlite/skipif diff
    exec/tests/sqlite/step.js diff
    exec/tests/sqlite/table.js diff
    exec/tests/sqlite/transaction.js diff
    exec/tests/sqlite/types.js diff
    exec/tests/sqlite/update.js diff
    exec/tests/sqlite/utf8.js diff
    src/sbbs3/js_sqlite.cpp diff
    Modified Files:

    exec/jsdocs.js diff
    src/sbbs3/CMakeLists.txt diff
    src/sbbs3/GNUmakefile diff
    src/sbbs3/jsdoor.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/objects.mk diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/services.cpp diff
    Merge branch 'sqlite3' into 'master' sbbs3: add SQLite JavaScript class See merge request main/sbbs!681
  279. Deucе
    Sun May 24 2026 13:13:47 GMT-0700 (PDT)
    Added Files:
    

    exec/examples/sqlite_example.js diff
    exec/tests/sqlite/basic.js diff
    exec/tests/sqlite/bind.js diff
    exec/tests/sqlite/coercion.js diff
    exec/tests/sqlite/declared_types.js diff
    exec/tests/sqlite/errors.js diff
    exec/tests/sqlite/extras.js diff
    exec/tests/sqlite/fk.js diff
    exec/tests/sqlite/injection.js diff
    exec/tests/sqlite/insert.js diff
    exec/tests/sqlite/record.js diff
    exec/tests/sqlite/remove.js diff
    exec/tests/sqlite/schema.js diff
    exec/tests/sqlite/select.js diff
    exec/tests/sqlite/skipif diff
    exec/tests/sqlite/step.js diff
    exec/tests/sqlite/table.js diff
    exec/tests/sqlite/transaction.js diff
    exec/tests/sqlite/types.js diff
    exec/tests/sqlite/update.js diff
    exec/tests/sqlite/utf8.js diff
    src/sbbs3/js_sqlite.cpp diff
    Modified Files:

    exec/jsdocs.js diff
    src/sbbs3/CMakeLists.txt diff
    src/sbbs3/GNUmakefile diff
    src/sbbs3/jsdoor.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/objects.mk diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/services.cpp diff
    sbbs3: add SQLite JavaScript class
  280. Rob Swindell (on Debian Linux)
    Sun May 24 2026 03:00:54 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/scfg/CLAUDE.md diff
    scfg: add CLAUDE.md with menu help text and search index conventions Captures lessons learned across recent SCFG work: * uifc.helpbuf format conventions (title in backticks, ~72-col wrap, backtick-quoted option names, sysop voice, no internal-symbol references) with canonical examples to template from. * Helpbufs live inline in scfg*.c, NOT in ctrl/text.dat -- different system from the text[] runtime string database. * Shared menu helpers (js_startup_cfg, login_attempt_cfg, rate_limit_cfg, max_concurrent_cfg) -- one edit changes every caller's menu. * No automated test; validate F1-rendered help interactively. * CP437 box-drawing characters in helpbufs are corrupted by string- substitution editors; preserve raw bytes. * scfgindex.h is auto-generated by gen_option_index.py; regenerate after menu changes; out-of-tree builds skip the auto-regen rule so always re-run by hand. * When adding a new dispatch pattern, verify is_navigable_list() still classifies the menu correctly or the options silently drop from search. In-repo home for these makes them visible to future sessions and teammates working in src/sbbs3/scfg/ without having to consult any per-user memory.
  281. Rob Swindell (on Debian Linux)
    Sun May 24 2026 02:49:17 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsrvr.c diff
    scfg: flesh out JavaScript Settings menu help text The Servers > * > JavaScript Settings menu (shared by Global/Term/Mail/ FTP/Web/Services via js_startup_cfg) previously had a one-line helpbuf just saying "Settings that control the server-side JavaScript execution environment." Replace with per-option descriptions for Heap Size, Time Limit, GC Interval, Yield Interval, and Load Path -- explaining that the three "interval" knobs count SpiderMonkey operation callbacks (NOT wall-clock ticks; the sbbs.ini comment is wrong) and that values take effect after a server recycle. Companion wiki update at config:javascript mirrors the corrected "operation callbacks" wording and the JIT-off-by-default change from edf752429 (#1143). First menu in the SCFG menu-help audit -- 38 sparse menu helpbufs to go.
  282. Rob Swindell (on Debian Linux)
    Sun May 24 2026 02:08:04 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/gen_option_index.py diff
    src/sbbs3/scfg/scfgindex.h diff
    scfg: index helper-menu options under every caller's path The Ctrl-F search index generator only attributed shared config helpers (rate_limit_cfg, login_attempt_cfg, js_startup_cfg, edit_sys_timezone, choose_io_method, dir_toggle_options, edit_fixed_event) to the single alphabetically-first caller. Options reachable from multiple servers were therefore missing from the search index for all but one server - e.g. "Failed Login Threshold" only appeared under FTP Server even though it exists under Global Settings, Terminal, Web, Mail, and Services too. Two fixes in gen_option_index.py: 1. Filter cfg_wizard out of the caller graph. It runs once at first-install, before main's Configure-menu loop, so its callees aren't reachable via Ctrl-F search. Indexing them produced bare path entries with no "Configure >" anchor AND stole attribution from sys_cfg (the real caller) via the alphabetical-first heuristic -- e.g. timezone choices were filed under "Non-U.S. Time Zone > Australian Central" instead of "System > Local Time Zone > ...". 2. Replace build_path's single-caller walk with build_paths, which DFS-enumerates every distinct caller chain from option_func up to main and returns one path per chain. The existing dedup-by-(label, menu, path) at the entries level collapses identical chains. Regenerated index grows from 800 to 2077 entries. Audit (against the parser's own option extraction plus an independent raw-source grep for opt[] writes and string-array initializers) confirms every navigable SCFG option reachable from main is indexed at least once.
  283. Rob Swindell (on Debian Linux)
    Sun May 24 2026 01:36:31 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/gen_option_index.py diff
    src/sbbs3/scfg/scfgindex.h diff
    scfg: index rate-limit submenu options (Count IPv6 Clients By, etc.) The Ctrl-F search index generator was skipping every option inside rate_limit_cfg() (Limit Rate of Connections/Requests, Count IPv4/IPv6 Clients By, Auto-Filter Threshold/Duration/Silently, Subnet Filter Threshold) because is_navigable_list() only recognized the direct dispatch pattern `switch (sel)` and missed the indirection-table form `switch (action[sel])` used by that function (it needs the parallel action[] array since options are conditionally hidden). Extend the regex to also match `switch (table[var])`, then regenerate.
  284. Rob Swindell (on Windows 11)
    Sat May 23 2026 22:50:01 GMT-0700 (PDT)
    Modified Files:
    

    exec/tests/msgbase/get_all_msg_headers.js diff
    src/sbbs3/js_msgbase.cpp diff
    src/sbbs3/sbbsdefs.h diff
    sbbsdefs: disable JSOPTION_JIT (TraceMonkey) by default -- fixes #1143 JAVASCRIPT_OPTIONS dropped from 0x810 to 0x10 (kept JSOPTION_COMPILE_N_GO, dropped JSOPTION_JIT / bit 0x800 / TraceMonkey). TraceMonkey is the true root cause of MsgBase.get_all_msg_headers() returning `undefined` on cold first access of LAZY_STRING_TRUNCSP_NULL fields (to_ext, from_ext, replyto, to_list, cc_list, summary, tags, from_org, ...) on bulk-fetched headers: the trace recorder's shape-guarded GETPROP over a hot for..in dot-access loop mis-replays `undefined` across the shared header shape, since the *_NULL fields are conditionally-resolved own properties (present on some headers, absent on others). Confirmed via A/B on the same build by flipping only this bit (js.options 0x810 -> 0x10) on a live ~7.7k-msg mail base: cold hdr.to_ext undefined collapses from thousands of spurious mispredicts to exactly the genuinely-NULL count (216 of 7698 headers, matching the primed count). Reproduced on Linux/gcc and Windows/MSVC (both compile JS_TRACER into libmozjs); does not reproduce on FreeBSD/Clang (which builds with --disable-tracejit per 3rdp/build/GNUmakefile:38-43). JSOPTION_METHODJIT (bit 0x4000) was already off and remains off. Its PolyIC has a similarly shape-guarded structure and would warrant a re-run of the issue's probe_to_ext.js / probe_enum.js before being enabled. Prior art: a0607c011 dropped METHODJIT for analogous reasons (xtrn_sec.js misbehavior). Reverts two prior workarounds, which both named the wrong cache (interpreter PropertyCache rather than the trace JIT): ca448cb8b - eager JS_DefineProperty("number") (ineffective on real bases) 666ff71ce - eager full js_get_msg_header_resolve(JSID_VOID) + defer_listing (effective but at the cost of laziness, on the wrong diagnosis) Tests: rewrites exec/tests/msgbase/get_all_msg_headers.js to (1) hard- assert JSOPTION_JIT is off in js.options, failing fast with a clear message if re-enabled, and (2) keep the behavioral bulk-fetch contract guard. Drops the now-stale single-message scenario. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  285. Rob Swindell (on Windows 11)
    Sat May 23 2026 21:41:38 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/MainFormUnit.cpp diff
    sbbsctrl: fflush per-server disk-log streams after each write Follow-up to 9a207243c (resolved #1146): with the Stop->Enabled gate removed, the shutdown-summary line now reaches the disk-log fwrite() in each of the four xxx_log_msg() handlers -- but rob noticed that the line still didn't *appear* in the file until sbbsctrl was quit. Cause: _fsopen() returns a fully-buffered stream, and the handlers never flush; written data sits in the CRT buffer until the close sentinel (msg==NULL) calls fclose() at sbbsctrl shutdown, or until the next date rollover recreates the file. Add a plain fflush(LogStream) after each fwrite(). On Windows this is essentially free -- fflush copies the CRT buffer to the OS page cache (no FlushFileBuffers, no disk sync), and the OS handles physical writeback on its own schedule -- so per-line flushing is fine and avoids the "lines stranded indefinitely after the last write of a quiet period" failure mode of any timer/threshold-based scheme. Same provenance as the previous fix: the missing-fflush has been in the Mail/FTP handlers since the initial v3.00c import in 2000 (7e3e47141), and was propagated to the new Telnet/Web handlers in d0252720e (resolves #1108). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  286. Rob Swindell (on Windows 11)
    Sat May 23 2026 21:25:05 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/MainFormUnit.cpp diff
    sbbsctrl: don't drop shutdown-summary log lines from disk logs The four per-server log handlers (bbs/mail/ftp/web_log_msg) gated disk writes on `XxxLogFile && XxxStop->Enabled`, but the latter half closed the window too early during a graceful shutdown -- the final `#### XxxX Server thread terminated (NN clients served, ...)` summary line was logged to the GUI but never persisted to disk, hiding useful operational data (e.g. rate-limit / IP-filter denial counts) from post-hoc analysis. The `Stop->Enabled` half of the gate is original 2000-era code (7e3e47141, "Initial check-in: v3.00c") and worked in practice for 25+ years by accident: the old `xxx_set_state` switch only handled SERVER_STOPPED and SERVER_READY explicitly, so transitional states fell through and left `Stop->Enabled` at its previous (true) value through the entire shutdown sequence -- only flipping false on the final SERVER_STOPPED callback that fires *after* the summary line is emitted. The gate happened to be open at exactly the right moment. f65fd89a1 ("Resolve crashes during graceful server termination(s)") moved control updates out of the background-thread state callbacks onto the 2 Hz LogTimer and rewrote the mapping as a strict flat assignment: MainForm->FtpStop->Enabled = (state == SERVER_READY); That tightened `Stop->Enabled` from "best-effort proxy for is-server-running" to literally `state == SERVER_READY`. As soon as shutdown begins, state leaves SERVER_READY, the next LogTimer tick flips the flag to false, and disk logging stops -- well before the summary line is emitted. The fix didn't introduce the gate; it just removed the slack that was masking it. d0252720e ("sbbsctrl: Add 'Log to Disk' option for Terminal and Web servers", resolves #1108) then added disk logging to Telnet and Web and copied the Mail/FTP gate pattern verbatim, so those two arrived broken. Drop the `XxxStop->Enabled` half of the gate in all four handlers; the user's "Log to Disk" preference alone is the right condition. Resolves #1146 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  287. Rob Swindell (on Windows 11)
    Sat May 23 2026 21:14:00 GMT-0700 (PDT)
    Modified Files:
    

    .claude/skills/logs/SKILL.md diff
    .claude/skills/logs: lessons from FTP rate-limit testing session Three small additions captured while testing the d7c823c9d rate-limit auto-filter rollout against the live FTP server on vert: - "Common mistakes" gains a note that `ls`/`stat` mtime lies when reading sbbsctrl-written `data/logs/{TS,MS,FS,WS}<MMDDYY>.LOG` over SMB/Samba while sbbsctrl still holds the file open. Observed: `tail -F` over the mount happily streamed new lines while `ls -la` reported a 20-minute-old mtime. Local NTFS on the same Windows host updates mtime as expected, so this is an SMB metadata-caching gotcha, not a Windows-general one. - "Quick map" entry for active rate-limit deny pressure updated to match the new log format the rollout introduced ("Too many requests per rate limit (NN over NNs) for <IP-or-CIDR>") -- the trailing `for <IP-or-CIDR>` makes attribution by host or subnet trivial when RateLimitSubnetPrefixN > 0. - "Investigation cookbook" gains a recipe for live-watching a Windows sbbsctrl disk log with `tail -F | grep --line-buffered`, including a small patterns table. Notes that the post-shutdown `#### Xxx Server thread terminated (...)` summary line only reaches the sbbsctrl GUI Log control, not the disk file (just describes the symptom; the underlying bug is now tracked as gitlab issue #1146). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  288. Rob Swindell (on Windows 11)
    Sat May 23 2026 21:12:57 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsrvr.c diff
    scfg: refine Rate Limiting menus -- hide dead options, restore labels Three small fixups to the SCFG rate-limit menu work added in d7c823c9d: 1) In rate_limit_cfg() (the shared submenu), hide the subnet-prefix and auto-filter options entirely when no rate limit is configured -- they have nothing to act on. Likewise hide the auto-filter duration / silent / subnet-threshold trio when the filter threshold is 0. Net result: switching a rate limit off collapses the submenu to just the "Limit Rate of ..." item; turning the auto-filter on/off reveals/hides only the sub-knobs that depend on it. 2) For servers that support only one type of rate limit (FTP, Mail, Services), restore the parent menu label from the post-refactor "Rate Limiting..." back to the original "Limit Rate of Requests" (FTP, Mail) / "Limit Rate of Connections" (Services), and inline the live values ("3600 per 1 hour, Auto-Filter") the way the pre-refactor menus did. Web keeps "Rate Limiting..." since it has both connect-rate AND request-rate items. 3) Expand the "Count IPv4/IPv6 Clients By" help text to explain why the IPv6 default is /64 while IPv4 stays per-host (paired with the ini default change in the previous commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  289. Rob Swindell (on Windows 11)
    Sat May 23 2026 21:12:41 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbs_ini.c diff
    sbbs_ini: default IPv6 rate-limit subnet prefix to /64 Per-host IPv6 counting is naive: a typical IPv6 subscriber gets a /64 (or larger) allocation from their ISP, so a single attacker can trivially cycle through addresses they own and evade per-host rate limits. /64 is the smallest unit that meaningfully represents "one subscriber". Bump the default RateLimitSubnetPrefix6 from 0 (per-host) to 64 for all four servers that have rate limits (web/ftp/mail/services). IPv4 stays at 0 (per-host) since v4 addresses are meaningfully individual. Existing installs with an explicit RateLimitSubnetPrefix6=0 in their sbbs.ini keep that value; only the missing-key case picks up the new default. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  290. Rob Swindell (on Windows 11)
    Sat May 23 2026 19:44:12 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/ratelimit_filter.hpp diff
    Modified Files:

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/ftpsrvr.h diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/mailsrvr.h diff
    src/sbbs3/ratelimit.hpp diff
    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgsrvr.c diff
    src/sbbs3/services.cpp diff
    src/sbbs3/services.h diff
    src/sbbs3/websrvr.cpp diff
    ftp/mail/services: add rate-limit auto-filter, mirroring web The web server has had a rate-limit auto-filter since bd375c1e4 (Feb 2026): once a client (or aggregated subnet) trips the rate limit rate_limit_filter consecutive times while continuously active, it's added to ip.can or ip-silent.can. This commit rolls the same machinery out to the other rate-limited servers (FTP, SMTP, POP3, Services) so they can defend against low-grade flood abuse without sysop intervention. Per-server changes (added to ftp_startup_t / mail_startup_t / services_startup_t, exposed via sbbs.ini and SCFG > "Rate Limiting..." submenu): - rate_limit_prefix4 / rate_limit_prefix6: aggregate counting per subnet - rate_limit_filter: violations threshold (0 = disabled, current default) - rate_limit_filter_duration: lifetime of the .can entry - rate_limit_filter_silent: write to ip-silent.can vs ip.can - rate_limit_filter_subnet_threshold: distinct-IP guard (default 2) Mechanically: - rate_limit_key() and rate_limit_filter() extracted from websrvr.cpp into a new shared header ratelimit_filter.hpp (static-inline, with per-server lprintf passed as a function pointer). No new .cpp / build graph changes; websrvr.cpp now calls the shared helpers. - ratelimit.hpp gets a long-missing include guard. - web_rate_limit_cfg() in scfgsrvr.c generalized into rate_limit_cfg() taking a view struct with nullable field pointers, so the same menu + help text serves all four servers (web has connect+request; FTP/Mail have request only; Services has connect only). - Services previously dropped rate-limited connections silently; it now logs a NOTICE before dropping, matching the other servers. Touches *_startup_t for three servers, so sbbsctrl.exe needs rebuilding on Windows hosts alongside this. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  291. Rob Swindell (on Windows 11)
    Sat May 23 2026 19:35:58 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/trash.c diff
    trash.c: have filter_ip() prune expired entries before re-blocking The previous commit (43f3b369a) made filter_ip() return false when the IP was already in the filter file, eliminating the redundant BLOCKING IP ADDRESS log spam (6099 lines for a single IP in one day, on vert). But that left a latent hole: if the existing entry's `e=` expiry had passed, the IP would be silently never re-blocked. trashcan2() already treats expired entries as not-listed (connections allowed), and trashman only sweeps as a [monthly_event], so a re-offender could go unfiltered for up to ~30 days. Switch the dup-detection from findstr() to find2strs() with metadata extraction, parse the existing entry's `e=` field, and: - If the match is still active (no expiry, future expiry, or unparseable metadata): return false, no log, no dup -- same as 43f3b369a. - If the match is expired: call a new remove_expired_filter_entries() helper to prune ALL expired entries from the file (not just our IP's -- opportunistic cleanup, same shape as trashman's maint() but inline), then fall through to append a fresh entry so the IP is re-blocked immediately. The helper holds the file open O_RDWR across the read/rewrite so a concurrent O_APPEND writer can't slip in a new entry between a close-and-reopen and have it truncated away. It only rewrites if at least one line was actually removed, uses strListDelete() to avoid leaking the removed strings, treats strListReadFile() OOM as a hard failure (not "nothing to clean"), and honors fflush() failures. Match trashcan2()/trash_in_list()'s `expires <= now` semantics (rather than trashman's strict `<`): only remove entries that the lookup path is already ignoring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  292. Rob Swindell (on Windows 11)
    Sat May 23 2026 19:04:51 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgsrvr.c diff
    src/sbbs3/websrvr.cpp diff
    src/sbbs3/websrvr.h diff
    websrvr: make rate-limit subnet auto-filter threshold configurable Commit 52db12ec6 added a distinct-IP guard to rate_limit_filter() that suppresses subnet-wide filtering when only one host in the bucket abused the rate limit. The threshold was hardcoded at "more than 1 distinct host," which is a reasonable default for tight prefixes but too aggressive for wide ones (/16, /48), where collateral risk to innocent neighbors warrants requiring more distinct abusers first. Expose it as web_startup_t.rate_limit_filter_subnet_threshold, with: - new ini key RateLimitFilterSubnetThreshold (default 2 preserves current behavior; clamped >=1 -- set to 1 to filter on the first abuser, no neighbor required) - new SCFG entry under Web Server > Rate Limiting > Subnet Filter Threshold Touches the web_startup_t struct, so sbbsctrl.exe needs rebuilding on Windows hosts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  293. Rob Swindell (on Windows 11)
    Sat May 23 2026 18:43:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/trash.c diff
    trash.c: have filter_ip() return false when IP is already in the filter filter_ip() returned true both when it newly added the IP to ip.can AND when the IP was already present (the dedup-guarded early return). Since commit be8ba77c2 ("Only log !BLOCKING IP ADDRESS when filter_ip() actually filters the IP") gated the "!BLOCKING IP ADDRESS" notices on the return value, the already-present case kept emitting fresh BLOCKING notices on every subsequent connection from a long-since-blocked IP. Observed in a recent web server log: 6099 redundant "!BLOCKING IP ADDRESS" lines for a single IP in one day. Flip the already-present branch to return false. The dedup behavior is preserved (no duplicate append to ip.can), but the return now means "this call added the IP", which is what every caller updated by be8ba77c2 actually wanted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  294. Rob Swindell (on Windows 11)
    Fri May 22 2026 22:25:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_client.cpp diff
    Resolve MSVC warning
  295. Rob Swindell (on Debian Linux)
    Fri May 22 2026 19:16:48 GMT-0700 (PDT)
    Added Files:
    

    .claude/skills/control/SKILL.md diff
    .claude/skills/javascript/SKILL.md diff
    .claude/skills/jsexec/SKILL.md diff
    .claude/skills/logs/SKILL.md diff
    .claude/skills/menus/SKILL.md diff
    .claude/skills/mqtt/SKILL.md diff
    .claude/skills/smbutils/SKILL.md diff
    .claude/skills/text/SKILL.md diff
    .claude/skills: drop redundant 'synchronet-' prefix from in-repo skill names The repo's .claude/skills/ directory is by definition the Synchronet skill namespace; prefixing every skill name with 'synchronet-' was 11 characters of redundant context per name. Renamed: synchronet-control -> control synchronet-javascript -> javascript synchronet-jsexec -> jsexec synchronet-logs -> logs synchronet-menus -> menus synchronet-mqtt -> mqtt synchronet-smbutils -> smbutils synchronet-text -> text Updated each skill's frontmatter 'name:' field and all cross-references between skills. Personal-only skills (synchronet-build/gitlab/wiki, which require local credentials/access and live outside this repo) keep their prefix; references to them from in-repo skills are unchanged.
  296. Rob Swindell (on Debian Linux)
    Fri May 22 2026 18:57:44 GMT-0700 (PDT)
    Added Files:
    

    .claude/skills/synchronet-smbutils/SKILL.md diff
    .claude/skills: add synchronet-smbutils Migrated from the personal ~/.claude/skills/synchronet-smbutils/ version. The skill covers the three SMB-level command-line tools that operate on Synchronet Message Base files directly (below the level of the BBS itself or the JS MsgBase API): * smbutil -- inspect/list/view/import/pack/renumber/lock/unlock operations on a base, plus the verbose-dump V command and the per-user mail-extraction recipe. * chksmb -- integrity scan; reports header / data / index / hash anomalies without modifying anything. * fixsmb -- destructive repair that rebuilds indexes/hashes and can renumber; covers when (not) to reach for it. Includes the SMB on-disk format references (wiki ref:smb, the 1993 historical spec, the SMBLIB C source), the filespec convention shared by all three tools (path with no extension, or with a specific .shd / .sdt / .sid / .sha component), the [n] index-pitfall warning, a full investigate-a-corrupt-base workflow, and the boundary on when to use these tools versus the in-BBS interface or the MsgBase JS API. Only one host-specific bit needed scrubbing: an example Windows install path was S:\sbbs\ctrl (the personal versions drive letter); the in-repo version uses the conventional C:\sbbs\ctrl plus the Git Bash / MSYS mount-point form. Otherwise the personal SKILL.md was already host-agnostic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  297. Rob Swindell (on Debian Linux)
    Fri May 22 2026 18:51:17 GMT-0700 (PDT)
    Added Files:
    

    .claude/skills/synchronet-jsexec/SKILL.md diff
    .claude/skills: add synchronet-jsexec Migrated from the personal ~/.claude/skills/synchronet-jsexec/ version with one section rewritten for the public repo. The skill covers driving the jsexec runner: the two invocation modes (-r inline expression, scripted), the flags worth remembering, the jsexec-vs-BBS-session global surface (system/User/MsgBase/etc. available; bbs/console/client are not), a minimal MsgBase probe, step-tagged crash tracing with log() vs print() and the -A merge, when to use jsexec vs smbutil, runtime constraints, common pitfalls, and the Windows / debug-build invocation gotchas. The Windows section was rewritten to scrub local install layout (replaced literal C:/sbbs and S:/sbbs paths with <sbbs-src> and <install> placeholders) and to generalize the 'live BBS is holding the debug DLL lock' situation -- the personal version was scoped to the specific host VERT, the public version explains the same lesson in platform-neutral terms (three escalation options: switch configuration, build in an isolated git worktree, or stop the BBS briefly). Added a cross-reference to synchronet-control for the graceful-drain mechanism in the stop-restart option. Cross-references synchronet-javascript (the JS language and host API), synchronet-smbutils (storage-layer message-base repair), and synchronet-build (compiling Synchronet). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  298. Rob Swindell (on Debian Linux)
    Fri May 22 2026 18:44:58 GMT-0700 (PDT)
    Added Files:
    

    .claude/skills/synchronet-text/SKILL.md diff
    Modified Files:

    .claude/skills/synchronet-logs/SKILL.md diff
    .claude/skills/synchronet-menus/SKILL.md diff
    .claude/skills: add synchronet-text; refine menus & logs Add a new synchronet-text skill that covers the runtime text[] string database (ctrl/text.dat), the v3.20+ ctrl/text.ini runtime override file (default-section by-ID overrides, [substr] global substitution, [JS] for gettext()-wrapped strings), and ctrl/text.<lang>.ini per-language overlays. Sysop-focused; the developer-only textgen workflow for adding new string IDs stays in the CLAUDE.md instructions, not in this skill. synchronet-menus: trim the description to drop the text.dat reference (now covered by synchronet-text), update the intro to clarify that this skill owns the display-file side while synchronet-text owns the string database, and add a prominent redirect block inside the Ctrl-A section pointing readers at text.ini [substr] for BBS-wide Ctrl-A colour retheming -- a real failure path observed in testing where sysops (and agents) reach for attr.ini, find that it can't remap literal embedded Ctrl-A bytes, and conclude the goal is impossible when in fact text.ini [substr] does exactly that. synchronet-logs: correct the csts.tab description -- it's tab-separated ASCII with a header row, readable with cat/awk/a spreadsheet, not the 'binary-ish' file the original entry implied. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  299. Rob Swindell (on Debian Linux)
    Fri May 22 2026 18:23:42 GMT-0700 (PDT)
    Added Files:
    

    exec/tests/system/findstr.js diff
    Modified Files:

    src/sbbs3/findstr.c diff
    findstr: add IPv6 CIDR support for ip.can / .can matching (issue #1145) The .can filter matching path (findstr.c) was IPv4-only — parse_cidr used sscanf("%u.%u.%u.%u/%u", …) and is_cidr_match did a 32-bit XOR-shift, so an IPv6 CIDR entry like `2001:db8::/32` in ip.can / ip-silent.can was silently treated as a literal-string pattern and never matched a real connecting IPv6 client. This made the web server's rate-limit auto-filter asymmetric: rate_limit_key() correctly bucketizes IPv6 traffic by RateLimitSubnetPrefix6 and writes the subnet to ip.can, but the accept-time read-back didn't enforce it. Add parallel IPv6 functions: parse_ipv6_address (inet_pton), parse_ipv6_cidr, is_ipv6_cidr_match (byte-then-bit prefix compare). findstr_compare dispatches to the right family by which one the input parsed as; find2strs_in_list and find2strs pre-parse both forms up-front into a small internal findstr_ip_t. Also fix two latent bugs surfaced by the new test cases: - is_cidr_match's `>> (32 - subnet)` is undefined behavior when subnet is 0 (a 32-bit shift by 32); on x86 it modulo-32s to 0, so /0 never matched anything except via exact host hit. Now guarded. - parse_cidr returned uint32_t with 0 overloaded as both "parse failed" and "0.0.0.0", so the literal pattern `0.0.0.0/0` (the canonical "match-any IPv4") fell through to string-only matching. Changed to bool + out-param, matching the new IPv6 convention. Add exec/tests/system/findstr.js covering string-pattern features, IPv4 CIDR (including /0 and boundaries), IPv6 exact + CIDR (/32 through /128, with bit-boundary stress at /33), reverse-match (!) for both families, cross-family no-false-match, and malformed-pattern rejection. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  300. Rob Swindell (on Debian Linux)
    Fri May 22 2026 18:23:42 GMT-0700 (PDT)
    Added Files:
    

    .claude/skills/synchronet-control/SKILL.md diff
    .claude/skills/synchronet-javascript/SKILL.md diff
    .claude/skills/synchronet-logs/SKILL.md diff
    .claude/skills/synchronet-menus/SKILL.md diff
    .claude/skills/synchronet-mqtt/SKILL.md diff
    .claude/skills: add Synchronet authoring skills for Claude Code Five Claude Code skills covering the day-to-day sysop / developer workflows on a Synchronet install. The skills are self-contained reference docs that the Claude Code CLI auto-discovers when working in the sbbs tree; they trigger on natural-language descriptions of common tasks and load only the relevant body on demand. - synchronet-menus: authoring text/menu/* files, Ctrl-A and @-codes, the file-extension priority by terminal type, .Xcol width variants, language overlays, security gating, mouse hotspots. - synchronet-logs: locating the right log file or stream on Windows or *nix — per-category files in data/, the per-day Terminal Server log, the Web Server access log, the lprintf console stream and its routing to syslog/journalctl/console depending on daemon mode and init system, the sbbsctrl-on-Windows-only data/logs/{TS,WS,MS,FS}*.LOG trap, multi-instance traps when sibling BBSes share a text/ directory across hosts, the ip.can/ip-silent.can field format. - synchronet-mqtt: discovering whether MQTT is enabled (the [MQTT] section of ctrl/main.ini, not sbbs.ini), reading the broker config, connecting with mosquitto_sub/pub (anonymous / user+pass / four TLS modes), full topic hierarchy, retained vs event semantics, and the control-plane topics (recycle/pause/resume/clear/node-set/input/msg) with production-impact warnings. - synchronet-control: controlling a running Synchronet instance via cross-platform semaphore files in ctrl/ and data/ (recycle, shutdown, pause, clear, with per-server .<service> and per-host .<hostname> suffix variants for shared-ctrl/ setups), POSIX signals to sbbscon, OS service managers (systemctl, service, launchctl, sc.exe), the Windows front-ends (sbbsctrl, sbbsNTsvcs, sbbs.exe), the node rerun utility, the won't-recycle-while-in-use gotcha, and the NO_RECYCLE flag. Cross-references synchronet-mqtt for situations where MQTT control is preferable even on the same host. - synchronet-javascript: the SpiderMonkey 1.8.5 dialect, the host object model (MsgBase, FileBase, User, system, msg_area/file_area, Socket, MQTT, etc.), how those APIs actually behave (including the get_all_msg_headers() lazy-field gotcha — touch a non-NULL field before any *_NULL field, with bracket-access as an escape hatch), writing tests in exec/tests/, and the stock exec/*.js ecosystem. Each skill is host-agnostic — no embedded local hostnames, IP addresses, BBSIDs, systemd unit names, or absolute filesystem paths. All four new-from-scratch skills (logs, mqtt, control, javascript-fix) were subagent-tested against realistic retrieval and diagnostic tasks on a live install before being committed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  301. Deucе
    Fri May 22 2026 10:40:06 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    src/syncterm/bbslist.c diff
    src/syncterm/syncterm.c diff
    src/xpdev/CMakeLists.txt diff
    src/xpdev/Common.gmake diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpbeep.h diff
    xpbeep: add native PipeWire backend Pull-based pw_stream driver via pw_thread_loop, probed between PortAudio and SDL. Uses libpipewire-0.3 via xp_dlopen so no link-time dependency; SPA is consumed only through inline header helpers. Avoids the pipewire-pulse compatibility shim, which has been observed to hang on some Wayland installs even when libpulse's async API is used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  302. Rob Swindell (on Windows 11)
    Fri May 22 2026 01:24:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ratelimit.hpp diff
    src/sbbs3/websrvr.cpp diff
    websrvr: filter the lone offending IP, not the whole subnet, for single abusers With subnet aggregation enabled (RateLimitSubnetPrefix4/6), the rate-limit auto-filter added in the prior commit always blocked the entire subnet CIDR once the violation threshold was reached -- so a single bad actor could get an innocent /24 of neighbors filtered. Track the distinct host IPs that have been *denied* within each subnet bucket (deny-path only, so light legitimate traffic sharing the subnet doesn't count) and only escalate to a subnet-wide filter when more than one distinct IP is responsible (i.e. the abuse really is distributed). A single offender is filtered by its host IP (/32) instead. The ip.can reason records the distinct IP count for subnet filters ("N rate-limit violations from M IPs"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  303. Rob Swindell (on Windows 11)
    Fri May 22 2026 01:24:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ratelimit.hpp diff
    ratelimit: use pthread_mutex_t instead of std::mutex (issue #1089) The std::mutex added in e57ac918b ("Mutex-protect the map") carries the same latent crash that filterfile.hpp hit: std::mutex/std::lock_guard crash in MSVCP140.dll with older MSVC++ runtime libraries (issue #1089). Apply the same remedy used for filterfile.hpp in fb6c6aadd: an explicitly-initialized pthread_mutex_t (init in constructor, destroy in destructor), explicit lock/unlock (each method restructured to a single return), and deleted copy/assignment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  304. Rob Swindell (on Windows 11)
    Fri May 22 2026 00:22:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/MainFormUnit.dfm diff
    sbbsctrl: Put the Terminal Server log view under the Terminal menu Fixes a regression from 80b3da6c7, which set Hint='ts' on the existing "BBS > View > Today's/Yesterday's/Another Day's Log" items. Those have no prefix on purpose - they view the consolidated user-session/system daily log (data/logs/<mmddyy>.LOG). The commit redirected them to the new Control-Panel terminal-server capture (TS<mmddyy>.LOG). Restore the BBS > View items to their original (no-prefix) session logs, and instead add a "View" submenu to the Terminal menu (which previously had none) with Today's/Yesterday's/Another Day's Log items using the 'ts' prefix - mirroring the Mail, FTP, and Web server menus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  305. Rob Swindell (on Windows 11)
    Fri May 22 2026 00:13:40 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/sbbs.ini diff
    src/sbbs3/ratelimit.hpp diff
    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgsrvr.c diff
    src/sbbs3/websrvr.cpp diff
    src/sbbs3/websrvr.h diff
    websrvr: add subnet-aggregated connection rate limiter with auto-filter Add a connection rate limiter to the web server, enforced at accept() before a session thread or TLS handshake is spawned, complementing the existing post-parse request rate limiter. This rejects a connection flood at the cheapest possible point and counts connections (e.g. aborted TLS handshakes) that never produce a parseable request. Both limiters can now optionally aggregate clients by IPv4/IPv6 subnet prefix, so distributed abuse spread thinly across many addresses in a hosting provider's range is counted (and filtered) as a single CIDR bucket rather than slipping under per-host-IP limits. Repeat offenders that exceed a rate limit RateLimitFilterThreshold times are auto-filtered: the offending IP or subnet (in CIDR notation) is written to ip.can, or ip-silent.can (dropped at accept) when RateLimitFilterSilent, with an optional expiry. The connection and request limiters share one set of auto-filter/subnet settings (each keeps its own independent denial counter). New [Web] ini settings, also configurable via SCFG (Web Server Settings -> Rate Limiting...): MaxConnectsPerPeriod, ConnectRateLimitPeriod, RateLimitSubnetPrefix4, RateLimitSubnetPrefix6, RateLimitFilterThreshold, RateLimitFilterDuration, RateLimitFilterSilent. - ratelimit.hpp: allowRequest() optionally reports a per-key denial count (reset when a client goes idle) as an escalation signal for auto-filtering. - websrvr.cpp: rate_limit_key() masks a client IP to its subnet CIDR; rate_limit_filter() writes abusers to the filter file. Connection limiter wired in at accept; request limiter updated to share the same machinery. Also include the protocol in the accept-time MAXIMUM CLIENTS log message, for consistency with the per-request one. - scfg: new "Rate Limiting..." submenu under Web Server Settings, with a status summary on the menu line. - ctrl/sbbs.ini: document the new [Web] keys (disabled by default). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  306. Rob Swindell (on Windows 11)
    Fri May 22 2026 00:13:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/MainFormUnit.cpp diff
    src/sbbs3/ctrl/MainFormUnit.dfm diff
    src/sbbs3/ctrl/MainFormUnit.h diff
    src/sbbs3/ctrl/TelnetCfgDlgUnit.cpp diff
    src/sbbs3/ctrl/TelnetCfgDlgUnit.dfm diff
    src/sbbs3/ctrl/TelnetCfgDlgUnit.h diff
    src/sbbs3/ctrl/WebCfgDlgUnit.cpp diff
    src/sbbs3/ctrl/WebCfgDlgUnit.dfm diff
    src/sbbs3/ctrl/WebCfgDlgUnit.h diff
    sbbsctrl: Add "Log to Disk" option for Terminal and Web servers The Control Panel could already persist Mail and FTP server log messages to daily files in the data/logs dir (MS*.LOG, FS*.LOG), but the Terminal and Web servers had no such option - their log output was lost when sbbsctrl closed. Extend the same capability to those two servers: - bbs_log_msg()/web_log_msg() now write to TS*.LOG / WS*.LOG (mmddyy), gated on the new TelnetLogFile/WebLogFile flags and the server running. - New "Log to Disk" checkbox on the Terminal Server (General tab) and Web Server (Log tab) configuration dialogs, persisted to the registry and read from the [TelnetForm]/[WebForm] ini LogFile keys. - Wire the Terminal "View ... Log" menu items to the 'ts' prefix (they previously had no prefix) and add a "View" submenu to the Web menu with the 'ws' prefix. - ViewLogClick() closes the Terminal/Web log streams too, so a freshly written log can be opened. Unlike Mail/FTP (which default on), the two new flags default to off so existing installs don't silently begin writing new log files on upgrade. Resolves #1108 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  307. Rob Swindell (on Debian Linux)
    Thu May 21 2026 22:14:10 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/putmsg.cpp diff
    Add Wildcat!-style @IFSEC=ars@/@ELSE@/@ENDIF@ conditional display (#941) Conditional-display blocks for message/file rendering, modeled on Wildcat!'s @IFSEC=Profile@ ... @ELSE@ ... @ENDIF@ construct. The argument is a Synchronet Access Requirement String (e.g. SYSOP, LEVEL40, FLAG1A), not a Wildcat! security-profile name. Both @IFSEC=ars@ and @IFSEC:ars@ are accepted. Implemented in putmsgfrag alongside the other Wildcat! display-control codes (@STOP@/@NOSTOP@/@NOCODE@), reusing the @SHOW@/@SYSONLY@ CON_ECHO_OFF output-suppression flag (auto-restored when the msg/file finishes rendering). As in Wildcat!, the blocks do not nest, but unlike Wildcat! they may appear anywhere in a line (not only column 0) and share a line with other text/codes. Verified live: a level-50 reader sees the IFSEC=LEVEL10/LEVEL50 branches and the @ELSE@ branches of IFSEC=SYSOP/LEVEL90, with @ENDIF@ correctly resuming output. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  308. Deucе
    Thu May 21 2026 20:57:22 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ringbuf.c diff
    ringbuf: fix RingBufReInit not clearing highwater_event After pHead=pTail=pStart the ring is empty (fill==0), so the highwater_event should always be in the reset state on return. The existing guard tested `RINGBUF_FILL_LEVEL(rb) >= rb->highwater_mark`, which is never true at this point, so the ResetEvent was never called and a previously-set highwater_event survived the reinit. Whoever next waited on it then took the "highwater reached" path against an empty ring. Drop the unsatisfiable fill check; keep only the "event configured" guards so the call is a no-op when events or a highwater mark aren't in use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  309. Deucе
    Thu May 21 2026 20:57:22 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ringbuf.c diff
    ringbuf: only signal events on state transitions in mutex-held builds RingBufWrite called ResetEvent(empty_event) and SetEvent(data_event) unconditionally on every call, regardless of whether the ring's empty/non-empty state actually changed. In sexyz, send_byte calls RingBufWrite once per transmitted byte, so a download was paying for 2-3 event-object operations per byte. On Win32 those operations are real kernel syscalls; on Unix they go through xpevent.c (pthread mutex+cond) which is much cheaper. A reporter on Windows saw 447 KB/s downloads on a link that delivered ~43 MB/s for the reverse direction (the upload path takes data through recv()->inbuf[] in 64 KiB chunks with no per-byte event signaling, so it wasn't affected). The unconditional signaling exists because RINGBUF_MUTEX is optional: without the mutex you cannot safely check "is this a transition" and then signal, since another writer/reader could change state between the check and the call. But every Synchronet build that defines RINGBUF_EVENT also defines RINGBUF_MUTEX (sbbs.vcxproj, websrvr.vcxproj, sbbsexec.vcxproj, sexyz.vcxproj, sbbsdefs.mk, CMakeLists.txt, and the sbbs.h fallback), so the slow path is the only one in practice. Add a #ifdef RINGBUF_MUTEX fast path inside the #ifdef RINGBUF_EVENT block in RingBufWrite and RingBufRead that uses the pre-update fill level (already captured for the truncation check) together with the post-clamp byte count to signal only on actual empty<->non-empty and below<->at-or-above-highwater transitions. The mutexless #else path preserves the existing always-signal semantics verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  310. Rob Swindell (on Debian Linux)
    Thu May 21 2026 20:32:00 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/atcodes.cpp diff
    Add Wildcat! @-code aliases for existing codes (#941) Wildcat! display macros that already had Synchronet equivalents under a different name, added as aliases so Wildcat!-style files/messages work unmodified: @ACCBAL@ -> @CREDITS@ @USERID@ -> @USERNUM@ @CALLID@ -> @CID@ @CONNECT@ -> @CONN@ @ID@ -> @QWKID@ @ENTER@ -> @PAUSE@ (approximate: Wildcat! waits for Enter, ours is hit-any-key) Remaining missing Wildcat! codes (@CONFOP@, @INCHAT@, @MAXRATIO@, @NETBAL@) have no Synchronet equivalent; @FAX@, @MODEM@, @YNDEF@, @ELSE@/@ENDIF@/@IFSEC@, @NOPREV@ are N/A to Synchronet's model (IFSEC-style conditionals are handled by @SHOW:@). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  311. Rob Swindell (on Debian Linux)
    Thu May 21 2026 19:07:43 GMT-0700 (PDT)
    Modified Files:
    

    exec/CLAUDE.md diff
    exec/CLAUDE.md: document how to edit command shells Add a Command Shells section covering the Baja (.src/.bin) and JavaScript (default.js/shell_lib.js, lbshell.js, menushell) shell inventory, the two-places-to-edit checklist (allowed-key string + handler), the shared maininfo/xferinfo menu display files and their raw-byte editing caveat, the baja recompile + log-off/on reload behavior, and the C-side CS_INFO_* backing in execfunc.cpp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  312. Rob Swindell (on Debian Linux)
    Thu May 21 2026 19:00:50 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/shell_lib.js diff
    exec/wwiv.src diff
    src/sbbs3/execfunc.cpp diff
    text/menu/xferinfo.msg diff
    Add Version command to file-section Information sub-menu The main Information sub-menu offered a 'V'ersion command (info_version / bbs.ver) but the file-transfer Information sub-menu did not. Add it to both the WWIV (Baja) shell and the Synchronet Classic (default.js/shell_lib.js) shell, and to the xferinfo menu display. Also make the Baja info_version command pass verbose=true, matching the JS bbs.ver() default, so the Baja shells show the full version details (revision/git, library versions, OS/CPU) instead of the brief one-liner. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  313. Rob Swindell (on Debian Linux)
    Thu May 21 2026 17:28:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/login.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    Only log !BLOCKING IP ADDRESS when filter_ip() actually filters the IP filter_ip() returns false when the IP/host is exempt (listed in ipfilter_exempt.cfg), when ip_addr is NULL, or when the filter file can't be opened -- in none of those cases is the address actually added to the filter. The "!BLOCKING IP ADDRESS" notices were logged unconditionally before the call, so an exempt IP in the auto-filter paths would still produce a misleading "BLOCKING" log line even though filter_ip() then declined to add it. Gate each notice on filter_ip() returning true. In the SMTP SPAM-bait path, also fold the call into the condition so the " and BLOCKED" tag is only appended on an actual block. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  314. Rob Swindell (on Debian Linux)
    Thu May 21 2026 17:21:57 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/login.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    Log a consistent !BLOCKING IP ADDRESS notice when auto-filtering IPs filter_ip() (trash.c) is a libsbbs function with no logger, so logging the IP-filter addition is the caller's responsibility. Coverage and format were inconsistent: only the max-concurrent (main.cpp) and SPAM-bait (mailsrvr.cpp) paths logged anything, and those two disagreed on whether the filter file was shown as a basename or a full path. The auto-filter-on-failed-logins path in every server (login.cpp, services.cpp, ftpsrvr.cpp, mailsrvr.cpp, websrvr.cpp) added the abuser's IP to ip.can silently, with no log line marking the moment. Emit a uniform "!BLOCKING IP ADDRESS: <ip> in <file>" NOTICE at all of these sites, using the full filter-file path everywhere (drop the getfname() basename in main.cpp) to match the existing "!CLIENT BLOCKED in %s" messages. Each site follows its file's local lprintf prefix convention; login.cpp derives the ip.can path via trashcan_fname() rather than hardcoding it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  315. Rob Swindell (on Debian Linux)
    Thu May 21 2026 17:00:58 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    ftpsrvr: remove duplicate ndx temp-file fopen() from de-dup commit Commit 19c826410 ("add errprintf() and route LOG_ERR/LOG_CRIT through it for de-dup") was meant only to convert the index-file fopen() failure's lprintf(LOG_ERR,...) to errprintf(..., WHERE, ...). A rebase merge resolution instead inserted a second, redundant fopen() of the "ndx" temp file ahead of "success = true;" (giving the new copy the errprintf) while leaving the original lprintf() open untouched. That left two opens: - the getdate (MDTM-style) path opened a temp file it never uses -- contradicting the "No temp file needed for a modification-time query" comment -- and never set tmpfile/delfile, leaking both the descriptor and the temp file (ironic in an EMFILE-motivated change); - the !getdate path opened the file twice, leaking the first descriptor. Drop the spurious block and keep the de-dup conversion on the surviving open inside the else branch, restoring the original single-open logic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  316. Rob Swindell (on Debian Linux)
    Wed May 20 2026 21:52:13 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/atcodes.cpp diff
    Add PCBoard @-code aliases for existing codes (#940) Several PCBoard display @-codes already had Synchronet equivalents under different names. Add them as aliases so PCBoard-style files/messages work without modification: @CARRIER@ -> @CONN@ @PROLTR@ -> @PROT@ @PRODESC@ -> @PROTNAME@ / @PROTOCOL@ @CONFNAME@ -> @CONF@ @CURMSGNUM@-> @MSG_NUM@ @QON@/@QOFF@ were already implemented in putmsg.cpp. The remaining missing PCBoard codes (@FILERATIO@, @HIGHMSGNUM@, @LOWMSGNUM@, @LMR@, @FIRSTU@) would be new value-codes, not aliases, and are left for a follow-up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  317. Rob Swindell (on Windows 11)
    Wed May 20 2026 21:47:49 GMT-0700 (PDT)
    Modified Files:
    

    exec/tests/msgbase/get_all_msg_headers.js diff
    src/sbbs3/js_msgbase.cpp diff
    js_msgbase: fully fix get_all_msg_headers() *_NULL fields on real mailbases Commit ca448cb8b eagerly JS_DefineProperty("number") per header to force a SpiderMonkey shape transition, fixing get_all_msg_headers() header objects returning undefined on first access of a LAZY_STRING_TRUNCSP_NULL field (to_ext, from_ext, replyto, to_list, cc_list, summary, tags, from_org, ...). That masked the bug for tiny/uniform message bases (and the single-message regression test), but NOT for real mailboxes: the resolve hook still leaves a *_NULL field undefined whenever its value is NULL, which corrupts the 1.8.5 property cache for the shared header shape, so every subsequent same-shape header then reads undefined on first access of that field. On a live ~14.4k- message mail base this bit ~98% of headers once the first NULL-to_ext message appeared (cold to_ext==1 count: 0; correct count after priming: ~9150). Fix: eagerly RESOLVE each header's data fields at construction in js_get_all_msg_headers (js_get_msg_header_resolve with JSID_VOID) instead of just defining 'number'. Non-NULL fields become own properties up front, so callers iterating bulk-fetched headers never trigger the GET-path lazy resolve (nor the property-cache fast-path that mis-serves undefined). A new privatemsg_t.defer_listing flag skips the expensive field_list[]/can_read branches during this eager pass (they remain lazily resolved on first access), and p->enumerated is left unset so a later enumeration still builds them. Fetch cost is unchanged (dominated by smb_getmsghdr, not field population). Extend exec/tests/msgbase/get_all_msg_headers.js with a multi-message, mixed-NULL, shape-diverse bulk scenario that reads *_NULL fields as the first access. Note: the real-mailbox property-cache corruption could not be reproduced with synthetic save_msg()'d messages, so the test is a behavioral guard validated against a real base, not a standalone reproducer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  318. Rob Swindell (on Debian Linux)
    Wed May 20 2026 17:05:47 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    ftpsrvr: add errprintf() and route LOG_ERR/LOG_CRIT through it for de-dup The FTP server logged every error via lprintf(LOG_ERR/LOG_CRIT, ...), which writes each record to error.log with no repeat suppression. Under file-descriptor exhaustion (EMFILE) a single busy server flooded error.log with thousands of near-identical entries. Mirror the mailsrvr/websrvr/services pattern: add a static errprintf() that calls repeated_error(line, function) and demotes a consecutive repeat of the same site to LOG_WARNING (which falls out of error.log). Convert all 48 LOG_ERR/LOG_CRIT lprintf() call sites to errprintf(..., WHERE, ...). This is log de-duplication only; it does not address the underlying descriptor exhaustion. Note repeated_error() tracks only the single most-recent (line, function), so interleaved failures from many concurrent sessions are only partially collapsed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  319. Rob Swindell (on Windows 11)
    Wed May 20 2026 16:54:53 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    ftpsrvr: fix file-descriptor leaks in directory/index handlers Two distinct leaks of per-command temp-file descriptors, both surfacing as "ERROR 24 (Too many open files)" once the process exhausts its descriptor table -- after which every temp-file open (LIST/MLSD .lst, index .ndx, QWK packets, and even unrelated subsystems sharing the process) fails in a storm. 1) MDTM <index-file> (getdate) opened a temp .ndx file via ftp_tmpfname() but never closed it: the lone fclose(fp) lives only in the !getdate (download) branch. Every MDTM on the dynamically-generated index file (startup->index_file_name, e.g. 00index) leaked one descriptor and orphaned an sbbstemp/SBBS_FTP.*.ndx file. FTP clients and crawlers MDTM each listed entry, so these accumulated steadily. Fix: don't open the temp file for a getdate/MDTM request at all (it is only needed to generate listing content); move the fopen into the !getdate branch. Introduced in 63e5e08f1c (2007-02-11), which made the index fopen unconditional and added early-outs for SIZE (continue) and MDTM (fall-through) that both bypassed the fclose. ff9ae78f44 (2007-11-28) fixed the SIZE half by moving its check above the fopen, but left the MDTM path leaking -- for ~18 years. 2) The MLSD, LIST and index-generation handlers "continue" out of the command loop on smb_open_dir() failure without closing the already- opened temp fp (and, for LIST/MLSD, without servicing the already- announced data connection). Triggers whenever a directory's .shd is locked or contended. Fix: close fp on the failure path; for LIST/MLSD also run the normal filexfer() cleanup so the data connection is serviced and the temp file removed. Introduced in 925e3b0a2 (2021-04-04), which migrated FTP directory listings to smb_open_dir()/loadfiles(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  320. Deucе
    Wed May 20 2026 16:40:08 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    src/syncterm/conn.c diff
    src/syncterm/ssh.c diff
    syncterm: pin SO_SNDBUF/SO_RCVBUF to 1 MiB on all TCP connections ssh.c set SO_SNDBUF=1 MiB after connect but left SO_RCVBUF at the kernel default, and the other TCP transports (telnet, telnets, rlogin, raw) inherited Windows' ~8 KiB defaults for both directions. A telnet download on Windows therefore capped at roughly RCVBUF/RTT — about 400 KiB/s on a 20 ms link — regardless of how fast the server streamed. Lift the buffer setup into conn_socket_connect() so every TCP transport gets a 1 MiB baseline in both directions. ssh_set_sftp_buffer_mode() still dynamically drops SO_SNDBUF to 64 KiB during SFTP activity so keystrokes don't queue behind bulk data; the connect-time 1 MiB is the value it restores to.
  321. Deucе
    Wed May 20 2026 14:13:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    src/syncterm/HACKING.md diff
    src/syncterm/Manual.txt diff
    src/syncterm/syncterm.c diff
    src/xpdev/xpbeep.c diff
    xpbeep: reorder audio backends to put native + cross-platform first Master's xptone_open_locked() try-order was PulseAudio second (after CoreAudio), which on Linux meant PortAudio and SDL both fell in line behind a libpulse-based path that re-runs the protocol setup on every fallback and can wedge on a misbehaving pipewire-pulse server (ticket 254). PortAudio reuses a persistent stream and is far less exposed. New order in xptone_open_locked() and do_xp_play_sample(): CoreAudio, Win32 WASAPI, PortAudio, SDL, PulseAudio, ALSA, OSS Native platform APIs first (guaranteed-present on their platforms), then cross-platform abstractions (battle-tested for desktop audio), then the server-specific PulseAudio path, then direct-hardware paths in declining modernity. This means PulseAudio is consulted only when the build was specifically configured PA-only or PortAudio/SDL both declined to open. While here, drop the recursive xptone_open_locked() call inside do_xp_play_sample()'s non-THREAD_SAFE PA fallback and legacy ALSA failure paths. Reopening mid-call left do_xp_play_sample()'s need_copy state stale for the new backend (landing on PortAudio / Win32 / SDL after a PA failure would skip the required buffer copy). Close-without-reopen instead; the worker thread's next iteration runs xptone_open_locked() with fresh state and a correctly computed need_copy. The THREAD_SAFE PA path (master's audio overhaul) already handles this through the mixer layer and isn't affected. UI and documentation updated to match: - syncterm.c: audio_output_bits[] and audio_output_types[] reordered so the Audio Output Mode menu in bbslist.c renders backends in the order they will actually be tried. Added the missing CoreAudio entry to both arrays (pre-existing gap: the engine has used CoreAudio as first-try on macOS the whole time but it was absent from the UI list). Numeric INI representation is unchanged because the bit values come from XPBEEP_DEVICE_* constants, not from array position; existing INIs continue to parse correctly and the "WaveOut" read-side alias for "WASAPI" is preserved. - HACKING.md, Manual.txt: backend enumerations reflect the new try order. CHANGES line goes under Version 1.9 because the equivalent fix shipped on the syncterm-1.9 branch as aded625a68.
  322. Deucе
    Wed May 20 2026 10:18:54 GMT-0700 (PDT)
    Modified Files:
    

    src/comio/comio_nix.c diff
    src/syncterm/CHANGES diff
    src/syncterm/bbslist.c diff
    src/syncterm/modem.c diff
    syncterm: cut serial close stall and respect 3-wire flow control Alt-H disconnect on a serial connection could stall for many seconds on Linux/Wayland. Three pieces: * comio/comio_nix.c: tcflush(fd, TCOFLUSH) before close() so the kernel doesn't drain the tty transmit queue when HUPCL drops DTR. At low baud with held-down CTS the per-byte drain timeout multiplied by the queued byte count was the entire visible delay. * syncterm/modem.c: when CONN_TYPE_SERIAL_NORTS, mask COM_FLOW_CONTROL_RTS_CTS off the flow_control bitmask before comSetFlowControl(). 3-wire wiring has no CTS line, so the tty layer's RTS/CTS gate would block all output the moment the kernel sampled CTS low (i.e. immediately). * syncterm/bbslist.c: the Flow Control submenu for a NORTS entry now offers only XON/XOFF and None. Stored fc is not rewritten on cancel; modem.c masks the RTS/CTS bit at connect time regardless. Reported on ticket 246 (Wayland Alt-H hang).
  323. Rob Swindell (on Windows 11)
    Sun May 17 2026 19:36:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    sbbs: reuse ip_can / ip_silent_can .fname instead of rebuilding the path The trashCan instances are already initialized at startup with the full path in .fname; just pick the one matching filter_silent. Matches the existing log at main.cpp:5850 which already uses ip_can.fname directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  324. Rob Swindell (on Windows 11)
    Sun May 17 2026 19:31:07 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgsrvr.c diff
    src/sbbs3/startup.h diff
    sbbs: optional ip-silent.can target for max-concurrent auto-filter (issue #1140) New per-server setting `MaxConConnFilterSilent` (sbbs.ini [bbs] section, SCFG: Servers -> Telnet/RLogin -> Max Concurrent Connections -> Auto-Filter Silently). When enabled, IPs that exceed the concurrent-connection threshold are added to `text/ip-silent.can` instead of `text/ip.can`, so subsequent connections from those IPs are dropped without logging a "blocked" notice. Default is `false` (unchanged behavior). Also log the basename of the .can file the abuser IP is being added to ("BLOCKING IP ADDRESS: <ip> in <ip.can|ip-silent.can>") matching the mailsrvr spam_block precedent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  325. Rob Swindell (on Windows 11)
    Sun May 17 2026 18:44:46 GMT-0700 (PDT)
    Added Files:
    

    docs/boolsrch.md diff
    src/sbbs3/boolsrch.c diff
    src/sbbs3/boolsrch.h diff
    src/sbbs3/boolsrch_test.c diff
    text/menu/textsrch.msg diff
    Modified Files:

    ctrl/text.dat diff
    exec/load/text.js diff
    src/sbbs3/listfile.cpp diff
    src/sbbs3/objects.mk diff
    src/sbbs3/prntfile.cpp diff
    src/sbbs3/readmail.cpp diff
    src/sbbs3/readmsgs.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/sbbs.jsdocs.vcxproj diff
    src/sbbs3/sbbs.vcxproj diff
    src/sbbs3/scandirs.cpp diff
    src/sbbs3/scansubs.cpp diff
    src/sbbs3/text.h diff
    src/sbbs3/text_defaults.c diff
    src/sbbs3/text_id.c diff
    Merge branch 'boolean-search'
  326. Rob Swindell (on Windows 11)
    Sun May 17 2026 18:43:48 GMT-0700 (PDT)
    Modified Files:
    

    text/menu/textsrch.msg diff
    boolsrch: reflow textsrch.msg for 40-col + use @SearchStringPrompt@ Inject Ctrl-A\ at each column boundary so the existing 80-col layout is preserved byte-for-byte while < 80-col terminals get clean line-breaks at the LongLineContinuationPrefix (text.dat #807) instead of mid-word auto-wrap. All visible lines now fit in 40 cols with no last-column wrap risk. While here, replace the literal "Text to search for:" with the @SearchStringPrompt@ @-code so the help text follows any future re-titling of the prompt automatically, and center the title with @CENTER@ so it adapts to the user's terminal width. Issue: #1139
  327. Rob Swindell (on Windows 11)
    Sun May 17 2026 17:49:33 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/text.dat diff
    boolsrch: avoid CGA brown in SearchStringPrompt The previous prompt reset back to dim yellow (`\1n\1y`) for the `(...)` parens around the `?=help` hint, which renders as brown on CGA palettes. Drop the resets and let the leading `\1h` ride sticky-bright through the hint, with white `?` for the keystroke (mirrors UeditPrompt's idiom). Issue: #1139
  328. Rob Swindell (on Windows 11)
    Thu May 14 2026 03:11:28 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/prntfile.cpp diff
    src/sbbs3/readmail.cpp diff
    src/sbbs3/readmsgs.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/scandirs.cpp diff
    src/sbbs3/scansubs.cpp diff
    boolsrch: fold compile + retry-on-error into get_search_string() Reported: typing an invalid query (e.g. "(AND") at the F)ind file description prompt printed the syntax-error message 21 times - once per directory in the selected library. Same shape of bug latent at every loop call site that compiles the boolean expression inside the iterated function: listfiles() / scanposts() / searchmail() each compile on entry and bail on syntax error, so the outer loop just sees "no results, try the next slot" and the user gets one error per slot. Fix: lift the compile up into get_search_string() so each query is validated exactly once, at prompt time. On syntax error the helper prints the InvalidSearchExpression message, displays textsrch.msg (the same help screen '?' would show - so the user sees the syntax reference immediately, not just an error), and re-prompts. The user stays in the prompt loop until they enter a valid expression or abort with empty input. Signature change: returns struct bool_expr* (NULL on abort) instead of bool. Callers that want the compiled expression directly (pager '/', readmsgs.cpp 'F' rebind) use it; callers that just want a validated string for a downstream function (scansubs, scandirs, readmail) free the expression immediately - the downstream function will re-compile, which is fine because the string is now guaranteed-valid so the downstream compile cannot fail. Net: each of the 7 call sites drops its own bool_expr_compile + error- print boilerplate; the F-rebind in readmsgs.cpp shrinks from a 15-line block to a 6-line block; the 21-errors bug is gone; the user gets a re-prompt with help on screen the moment they hit a syntax error. scansubs.cpp and scandirs.cpp gained an #include "boolsrch.h" (needed for bool_expr_free()). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  329. Rob Swindell (on Windows 11)
    Thu May 14 2026 03:04:28 GMT-0700 (PDT)
    Modified Files:
    

    docs/boolsrch.md diff
    src/sbbs3/prntfile.cpp diff
    boolsrch: wire '?' inline help into the file-pager '/' search too The pager's '/' search was the documented exception that didn't use get_search_string() and therefore didn't recognize '?' as the help-menu key. The original reasoning was that the help menu's output would scroll the pager's "stay in place" display — but the pager already has a main-prompt '?' binding (line 472) that does the same scrolling thing (it prints text[SeekHelp]). So the exception was inconsistent. Routed the pager's '/' through get_search_string() like the other six sites. Pager-specific cleanup (carriage_return + cleartoeol after K_NOCRLF input) runs in both the success and abort paths now, so the prompt line is always cleared. docs/boolsrch.md updated to remove the "exception" paragraph and note that the help scrolls the same way the main-prompt '?' already does. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  330. Rob Swindell (on Windows 11)
    Thu May 14 2026 01:56:50 GMT-0700 (PDT)
    Added Files:
    

    text/menu/textsrch.msg diff
    Modified Files:

    docs/boolsrch.md diff
    src/sbbs3/prntfile.cpp diff
    boolsrch: rename inline-help menu file srchhelp.msg -> textsrch.msg textsrch.msg mirrors the prompt wording (SearchStringPrompt = "Text to search for"), is more specific than the generic "srchhelp", and matches the directory's convention of descriptive single-word names (msgscan, batchxfr, allmail). Also updated the menu("...") call in get_search_string() and the path reference in docs/boolsrch.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  331. Rob Swindell (on Windows 11)
    Thu May 14 2026 00:38:23 GMT-0700 (PDT)
    Added Files:
    

    text/menu/srchhelp.msg diff
    Modified Files:

    docs/boolsrch.md diff
    src/sbbs3/prntfile.cpp diff
    boolsrch: rename inline-help menu file boolsrch.msg -> srchhelp.msg The help screen describes the search prompt syntax generally (and is reused by every boolean-search prompt), not anything specific to the boolsrch code module. srchhelp.msg is the better name. Also updated get_search_string()'s menu("...") call and docs/boolsrch.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  332. Rob Swindell (on Windows 11)
    Thu May 14 2026 00:35:00 GMT-0700 (PDT)
    Modified Files:
    

    docs/boolsrch.md diff
    boolsrch: document the '?' inline-help binding New "Inline help" section in docs/boolsrch.md explaining that '?' at a search prompt displays text/menu/boolsrch.msg and re-prompts, and that the file pager's '/' sub-prompt is the documented exception (its '?' is a literal character; the pager has its own SeekHelp binding). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  333. Rob Swindell (on Windows 11)
    Thu May 14 2026 00:33:28 GMT-0700 (PDT)
    Added Files:
    

    text/menu/boolsrch.msg diff
    Modified Files:

    ctrl/text.dat diff
    src/sbbs3/prntfile.cpp diff
    src/sbbs3/readmail.cpp diff
    src/sbbs3/readmsgs.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/scandirs.cpp diff
    src/sbbs3/scansubs.cpp diff
    src/sbbs3/text_defaults.c diff
    boolsrch: add inline help via '?' at search prompts Sysops asked for a user-discoverable way to learn the boolean-search syntax. Now: at any boolean-search prompt, entering a lone '?' displays text/menu/boolsrch.msg (a one-screen quick reference covering the operators, quoted whole-word match, grouping, and a few worked examples), then re-prompts for the search query. Any other input is treated as a normal query. SearchStringPrompt: "Text to search for (?=help): " — the prompt itself now hints at the binding so the help path is discoverable without needing to read the docs first. sbbs_t::get_search_string(buf, maxlen, kmode) wraps the bputs(SearchStringPrompt) / getstr() / strcmp(buf, "?") / menu() loop so the six call sites that prompt for a boolean query don't each have to reimplement it: scansubs.cpp (2 sites - single sub, group/all) scandirs.cpp (2 sites - single dir, group/all) readmsgs.cpp ('F' rebind inside the message read loop) readmail.cpp ('/' search at the mail-read prompt) Also caught a stale getstr(str, 40, ...) in scandirs.cpp:64 that the earlier buffer-size pass missed; now uses the helper's 120-byte cap like every other call site. The file-pager '/' sub-prompt (P_SEEK in prntfile.cpp) intentionally does NOT use the helper - displaying the help menu mid-pager would scroll the screen and break the 'stay in place' invariant. The pager already has its own '?' binding (SeekHelp) for in-pager help, and its search sub-prompt continues to treat '?' as a literal character. text.dat record 076 (SearchStringPrompt) updated; textgen regenerated text_defaults.c. No new enum, so text.h / text_id.c / text.js are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  334. Rob Swindell (on Windows 11)
    Thu May 14 2026 00:24:00 GMT-0700 (PDT)
    Modified Files:
    

    docs/boolsrch.md diff
    boolsrch: correct default.js command bindings in docs/boolsrch.md The "Where it works" table claimed file-section `S` triggered boolean text search, but in exec/default.js the boolean-search command is actually `F` ("Find Text in File Descriptions" via FL_FINDDESC). File-section `S` is "Search for Filename(s)" (FL_NO_HDR) — a wildcard filename pattern match that doesn't use the parser at all. Also clarified the message side (separate rows for main-menu `F` scan vs the read-loop `F` re-prompt) and added a note that the parser engages whenever SCAN_FIND / FL_FIND reaches the underlying scan function, so sysops with custom shells get the same behavior regardless of how they bind the entry points. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  335. Rob Swindell (on Windows 11)
    Wed May 13 2026 02:05:04 GMT-0700 (PDT)
    Added Files:
    

    docs/boolsrch.md diff
    src/sbbs3/boolsrch.c diff
    src/sbbs3/boolsrch.h diff
    src/sbbs3/boolsrch_test.c diff
    Modified Files:

    ctrl/text.dat diff
    exec/load/text.js diff
    src/sbbs3/listfile.cpp diff
    src/sbbs3/objects.mk diff
    src/sbbs3/prntfile.cpp diff
    src/sbbs3/readmail.cpp diff
    src/sbbs3/readmsgs.cpp diff
    src/sbbs3/sbbs.jsdocs.vcxproj diff
    src/sbbs3/sbbs.vcxproj diff
    src/sbbs3/scandirs.cpp diff
    src/sbbs3/scansubs.cpp diff
    src/sbbs3/text.h diff
    src/sbbs3/text_defaults.c diff
    src/sbbs3/text_id.c diff
    boolsrch: add boolean text-search engine (issue #1139) PCBoard/Wildcat-compatible boolean search syntax (AND/OR/NOT with parens, quoted phrases for whole-word match) for the four "Text to search for" prompts: message-base scan/F-find, private mail / search, file-listing search, and the less-style file pager. Bare-word queries keep their current case-insensitive substring semantics, so existing usage is unchanged; the new operators unlock multi-term searches that the old strcasestr-only code couldn't express. New engine (src/sbbs3/boolsrch.[ch], one TU): bool_expr_compile() parses query into AST, malloc'd errmsg on fail bool_expr_match() evaluate against a single haystack bool_expr_match_fields() evaluate against N fields (term hits doc if it appears in ANY field) - the shape callers want for scanning subj/body/tags or name/desc/tags/ author bool_expr_free() frees the compiled expression tree Recursive-descent parser, precedence NOT > AND > OR, supports both symbol (& | !) and keyword (AND OR NOT, case-insens, whole-word) forms. Quoted "..." phrases apply a word-boundary check at each side that contains no whitespace inside the quotes - so "TEST" won't match TESTING/BACKTEST while " TEST " is pure substring (escape hatch). Implicit AND is inserted before ! / NOT (Wildcat's "(windows|DOS) & (modem|comm) !OS/2" idiom). Wired through: readmsgs.cpp scanposts, searchposts (F find at scan and read-loop) listfile.cpp listfiles (FL_FIND across name/desc/extdesc/tags/author) prntfile.cpp printfile() P_SEEK / and n search readmail.cpp searchmail / at the mail read prompt scansubs.cpp getstr buffer raised from 40 to 120 for boolean queries scandirs.cpp same Engine compiles the expression once per scan and reuses it across every document - no per-record parsing cost. Malformed queries print the new text.dat string InvalidSearchExpression (#948) and return cleanly to the prompt; pre-existing stale defaults for SeekPrompt (#944) and SeekHelp (#947) were re-emitted by textgen at the same time. Standalone unit test (src/sbbs3/boolsrch_test.c, TU-include of boolsrch.c) covers 95 cases: precedence, parens, quoting + whitespace- boundary suppression, keyword-vs-substring discrimination (BANDIT not split on AND), implicit AND, syntax errors, multi-field matching. Public API is exactly the four entry points above; diagnostic helpers (describe/is_simple/simple_text) stay file-local. User-facing docs at docs/boolsrch.md for later wiki import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  336. Rob Swindell (on Debian Linux)
    Thu May 14 2026 03:17:10 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mailsrvr.cpp diff
    mailsrvr: POP3 reply -ERR (not !UNSUPPORTED) to USER/PASS in TRANSACTION state Some clients (e.g. Thunderbird) reuse an already-authenticated TCP socket and re-issue USER/PASS. Per RFC 1939 these are AUTHORIZATION-state-only commands, so respond with a plain -ERR and keep the session alive, matching Dovecot/Courier behavior and suppressing the misleading "!UNSUPPORTED COMMAND" log notice. Refs main/sbbs#1123 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  337. Rob Swindell (on Windows 11)
    Thu May 14 2026 02:46:47 GMT-0700 (PDT)
    Added Files:
    

    exec/tests/filebase/basic.js diff
    exec/tests/filebase/skipif diff
    exec/tests/msgbase/basic.js diff
    exec/tests/msgbase/get_all_msg_headers.js diff
    exec/tests/msgbase/path_save.js diff
    tests: add basic MsgBase + FileBase coverage; rename existing msgbase test New tests in exec/tests/, all using ad-hoc (is_path) bases under system.temp_dir so they don't touch the live install: msgbase/basic.js Round-trip: open, save_msg, reopen, verify total_msgs/first_msg/ last_msg, get_msg_header by number and by offset, get_msg_body, remove_msg, verify MSG_DELETE attr round-trips. msgbase/path_save.js Regression for the savemsg() segfault fixed in e5ddda76d (cfg->sub[smb->subnum] unguarded deref for is_path msgbases when saving a message with to_ext set to a real local user number). filebase/skipif ('typeof FileBase === \"undefined\"') filebase/basic.js Round-trip: open, add a real on-disk file, get, get_list, get_size (compared to actual on-disk size to be tolerant of CRLF text-mode translation on Windows), hash (verifies md5 is 32-char hex), remove(name, true) deletes both the index entry and the on-disk file. This test would have masked the index-format bug fixed in 1767046fd. Rename the existing msgbase regression test from get_all_msg_headers_to_ext.js to get_all_msg_headers.js — short name now that it sits alongside other msgbase tests.
  338. Rob Swindell (on Windows 11)
    Thu May 14 2026 02:46:21 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_filebase.cpp diff
    js_filebase: initialize SMB_FILE_DIRECTORY for fresh ad-hoc (is_path) bases For an is_path FileBase, js_open() called smb_open() directly instead of smb_open_dir(). smb_open_dir() initializes a fresh file base by setting status.attr = SMB_FILE_DIRECTORY (and any dir-specific limits) then calling smb_create() to write the SMB header to disk. Plain smb_open() does neither — it just opens the .shd file (creating an empty one if absent) and reads existing status only if the file is already big enough. Without SMB_FILE_DIRECTORY in status.attr, smb_idxreclen() (smblib.c) returns sizeof(idxrec_t) (smaller, msg-style) instead of sizeof(fileidxrec_t). That meant smb_addfile() wrote a corrupted .sid index — small msg records where the rest of FileBase expects file records — and loadfilenames(), iterating the index using sizeof(fileidxrec_t), saw garbage / zero records. Net effect: add() returned true, but get(), get_list(), and get_names() never found the added file. Reproducer: var fb = new FileBase(system.temp_dir + "x", true); fb.open(); fb.add({name: "f.dat", desc: "d", from: "u"}); fb.get("f.dat"); // -> null fb.get_list().length; // -> 0 Fix: when js_open()'s !dirnum_is_valid (is_path) branch is taken and smb_open() succeeds against an empty .shd, set status.attr = SMB_FILE_DIRECTORY and call smb_create() — mirroring smb_open_dir()'s first-time-init for ad-hoc bases. Like the savemsg/votemsg fix in e5ddda76d, this is a latent bug exposed when 93b4d946c added the is_path constructor option.
  339. Rob Swindell (on Windows 11)
    Thu May 14 2026 02:08:11 GMT-0700 (PDT)
    Modified Files:
    

    exec/tests/msgbase/get_all_msg_headers_to_ext.js diff
    tests/msgbase: mkpath(system.temp_dir) for fresh-install / CI runners The get_all_msg_headers_to_ext.js test failed in CI with: smb_open_fp 2 'No such file or directory' opening .../temp/test_..._N.shd system.temp_dir is configured but its filesystem path doesn't necessarily exist yet on a freshly-cloned tree (e.g. a GitLab runner workspace), and smb_open / smb_open_fp uses O_CREAT for the file but does not mkdir the parent. Call mkpath(system.temp_dir) before constructing the MsgBase so the test is self-sufficient.
  340. Rob Swindell (on Windows 11)
    Thu May 14 2026 01:58:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgindex.h diff
    Indexed new "Internal Broker" MQTT option
  341. Rob Swindell (on Windows 11)
    Thu May 14 2026 01:49:52 GMT-0700 (PDT)
    Added Files:
    

    CLAUDE.md diff
    src/sbbs3/CLAUDE.md diff
    docs: add CLAUDE.md project-instruction files for AI-assisted edits Two CLAUDE.md files describing project conventions for AI coding assistants (Claude Code, etc.) operating on this tree. They are plain-Markdown notes; no tooling depends on them. CLAUDE.md (repo root): - Points at CONTRIBUTING.md for the canonical coding guidelines. - "Segfaults are bugs — always investigate": any crash of a Synchronet executable must be root-caused, never worked around or ignored. src/sbbs3/CLAUDE.md: - C/C++ formatting: defer to ../uncrustify.cfg. - text[] string workflow: ctrl/text.dat is the single source of truth; text.h / text_id.c / text_defaults.c / exec/load/text.js are regenerated by textgen and must never be hand-edited. Documents the edit-then-regenerate-then-commit-together flow.
  342. Rob Swindell (on Windows 11)
    Thu May 14 2026 01:42:29 GMT-0700 (PDT)
    Added Files:
    

    exec/tests/msgbase/get_all_msg_headers_to_ext.js diff
    exec/tests/msgbase/skipif diff
    Modified Files:

    src/sbbs3/js_msgbase.cpp diff
    js_msgbase: fix get_all_msg_headers() returning undefined for *_NULL fields MsgBase.get_all_msg_headers() returned header objects whose LAZY_STRING_TRUNCSP_NULL-defaulted fields (to_ext, from_ext, replyto, replyto_ext, replyto_list, to_list, cc_list, summary, tags, from_org, etc.) yielded `undefined` on first JS access — even when the underlying p->msg.<field> was populated. Touching ANY other property first (e.g. h.number, h.attr, or JSON.stringify(h) which enumerates) then "primed" the object's SpiderMonkey shape and made all subsequent lazy resolves work normally on the same object. Stock callers (hotline.js, msglist.js, msgutil.js, etc.) didn't trip on this because they all happen to access a non-NULL field (number, attr, when_imported, ...) before any *_NULL field, masking the bug. It only surfaces in code that reads a *_NULL field as the first property touched on a bulk-fetched header — which is exactly what filtering by to_ext or from_ext naturally does. get_msg_header() does not exhibit this because each retrieval is followed by user code that organically touches a non-NULL field, again priming the shape before any *_NULL access. Fix: in js_get_all_msg_headers, eagerly JS_DefineProperty("number", ...) immediately after JS_SetPrivate on each fresh header object. That single defineProperty triggers the SpiderMonkey shape transition once per header at construction time, so the first lazy resolve of a *_NULL-defaulted field operates on a settled shape and returns the correct value. Add a regression test in exec/tests/msgbase/: - get_all_msg_headers_to_ext.js — creates a temp msgbase, saves a message with to_ext="1", reopens, fetches all headers, and reads h.to_ext as the FIRST property touched on each bulk-fetched header. Throws if the value is anything other than "1". - skipif — skips the entire msgbase/ test category when MsgBase isn't available (e.g. under JSDoor).
  343. Rob Swindell (on Windows 11)
    Thu May 14 2026 01:40:09 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/postmsg.cpp diff
    postmsg: fix segfault in savemsg/votemsg for ad-hoc (is_path) MsgBases savemsg() and votemsg() both unconditionally dereferenced cfg->sub[smb->subnum] when posting the "MsgPostedToYou" / vote notification text — gated only by the subnum != INVALID_SUB check, not by subnum_is_valid(). For an "is_path" MsgBase (created from JS as "new MsgBase(path, true)"), js_msgbase_constructor sets p->smb.subnum = scfg->total_subs (intentionally not INVALID_SUB so it isn't treated as the mail base), which is one past the end of the cfg->sub[] array. The deref was a guaranteed segfault whenever a to_ext-tagged message was saved/voted on into such a base. Reproducer (against any Synchronet install, via jsexec): var p = system.temp_dir + "tmp_" + Date.now(); var mb = new MsgBase(p, true); mb.open(); mb.save_msg({to:"x", to_ext:"1", from:"y", subject:"z"}, "body"); // -> SIGSEGV inside savemsg, after smb_addmsg() succeeds The crashing path: msg has to_ext set and resolves to a real local user (usernum > 0); subnum != INVALID_SUB so the else-branch fires; that branch formats cfg->text[MsgPostedToYouVia] using cfg->grp[cfg->sub[subnum]->grp]->sname and cfg->sub[subnum]->lname. With subnum == total_subs both reads are out of bounds. Fix: gate the else branch in savemsg on subnum_is_valid(), and add the same guard to the entire vote-notification block in votemsg. When the sub isn't a known sub-board (is_path / ad-hoc msgbase), skip the local "you have a message" putsmsg notification — there's no sub/grp metadata to format the standard text against, and the JS caller didn't ask for sub-board side effects. The unguarded deref itself dates back further, but it became reachable when commit 93b4d946c ("Security improvements to MsgBase and FileBase constructors") added the "is_path=true" constructor option that sets subnum = total_subs.
  344. Rob Swindell (on Debian Linux)
    Mon May 11 2026 23:57:24 GMT-0700 (PDT)
    Modified Files:
    

    src/smblib/smblib.c diff
    smb_new_msghdr: auto-repair small .sid/.shd status mismatches; check smb_putstatus return value Problem: if the .sid index file length didn't exactly match total_msgs * idxreclen, smb_new_msghdr returned SMB_ERR_FILE_LEN (-206) hard, blocking all further message adds until the msgbase was manually repaired. On 2026-05-09 this caused the mail SMB to reject every incoming message for ~14 hours (647 logged errors), resulting in SMTP "452 Insufficient system storage" responses. Root cause of the corruption: smb_putstatus() was called but its return value was silently discarded. If the status write failed after the index record was already appended to .sid, the two files diverged: .sid had one more record than total_msgs reflected (or vice-versa). Fix 1 — propagate smb_putstatus() failures: the return value is now assigned back to i and returned to the caller, so a failed status write is no longer silent. Fix 2 — auto-repair small discrepancies in smb_new_msghdr (rather than hard-failing) when the mismatch is 1-2 records and the .sid length is an exact multiple of idxreclen: - LONG by 1 (.sid has one orphan record): truncate the extra record with chsize() and continue. This was the May 9 failure mode. - SHORT by 1-2 (.sid is missing 1-2 records): reduce total_msgs to match the actual index length, persist the correction with smb_putstatus(), and continue. The orphaned header/data space is harmless and can be reclaimed by smbpack. - Mismatch of 3+ records, or non-aligned length: still returns SMB_ERR_FILE_LEN as before. Test cases (smblib/smbidxtest.c, using CuTest framework): Test_NoCorruption - baseline: normal add still works Test_ShortIndexByOne - .sid truncated by 1 record: auto-repairs Test_ShortIndexByTwo - .sid truncated by 2 records: auto-repairs Test_LongIndexByOne - .sid extended by 1 record: truncates and proceeds Test_LargeCorruptionFails - .sid truncated by 3 records: still returns -206 All 5 tests pass with the fix; 3 of 5 fail against the original code.
  345. Rob Swindell (on Debian Linux)
    Mon May 11 2026 23:57:24 GMT-0700 (PDT)
    Modified Files:
    

    docs/copyright.html diff
    Update docs/copyright.html to match site theme Replace FrontPage-era markup (msnavigation tables, font tags, inline styles) with the modern site theme matching docs/index.htm. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  346. Rob Swindell (on Debian Linux)
    Mon May 11 2026 23:57:24 GMT-0700 (PDT)
    Modified Files:
    

    docs/platforms.html diff
    Update platforms.html to match site theme; add v3.15–v3.21 platform data Rework the old all-caps inline-styled table into the modern site theme (header/nav/content-card/footer matching docs/index.htm). Condense the version column to remove insignificant revision letters and ranges. Reduce column count from 17 to 11 by merging BSD variants, dropping Solaris/QNX as columns (noted in footer), and removing DOS/OS2 columns (noted via [d] footnote on the two legacy rows that need it). Split the old "Vista–8.1" Windows column into "Vista/7" and "8–11" to correctly show v3.21 dropping Windows 7 support. Add rows for v3.15 through v3.21 with accurate release dates sourced from the syncanno message base. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  347. Rob Swindell
    Mon May 11 2026 11:48:49 GMT-0700 (PDT)
    Modified Files:
    

    exec/init-fidonet.ini diff
    Merge branch 'cheesenet' into 'master' Edit init-fidonet.ini to add ch3323net See merge request main/sbbs!679
  348. HM Derdok
    Thu May 07 2026 20:16:56 GMT-0700 (PDT)
    Modified Files:
    

    exec/init-fidonet.ini diff
    Edit init-fidonet.ini to add ch3323net
  349. Deucе
    Mon May 11 2026 11:39:55 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: populate Statistics pane from mqtt_stats.js retained topics Subscribe to sbbs/{id}/stats/# and map the per-property topics published by mqtt_stats.js to the StatsWidget fields. Since the messages are retained, the pane populates immediately on connect. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  350. Deucе
    Mon May 11 2026 11:24:10 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/atlantis/service.js diff
    And erm... exit when the remote goes away.
  351. Deucе
    Mon May 11 2026 11:21:54 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/atlantis/service.js diff
    Try not using 100% CPU.
  352. Deucе
    Mon May 11 2026 11:02:47 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt.cpp diff
    mqtt.cpp: guard internal-client-only statics with #ifndef USE_MOSQUITTO The lputs adapter, its mutex, and pthread_once init are only used by the internal client connect path, not the mosquitto path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  353. Deucе
    Mon May 11 2026 10:45:07 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_broker.cpp diff
    src/sbbs3/mqtt_topic.cpp diff
    src/sbbs3/mqtt_topic.h diff
    mqtt_broker: periodic cleanup of expired retained messages Walk the topic tree once per minute and remove retained messages whose MESSAGE_EXPIRY property has elapsed. Previously, expired retained messages were only skipped on delivery to new subscribers but never actually removed from the tree. Nothing currently sets MESSAGE_EXPIRY, but this prevents unbounded accumulation if/when publishers start using it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  354. Deucе
    Mon May 11 2026 09:51:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    src/sbbs3/qtmonitor/settingsdialog.cpp diff
    src/sbbs3/qtmonitor/settingsdialog.h diff
    qtmonitor: add configurable publish QoS setting (0 or 2) Control messages (recycle, node set, etc.) are published at QoS 0 by default. QoS 1 is intentionally excluded since at-least-once delivery could duplicate actions like server recycles. QoS 2 (exactly once) is available for setups where reliability over lossy links matters. Local broker connections skip the QoS handshake entirely — delivery is a direct in-process function call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  355. Deucе
    Mon May 11 2026 09:39:40 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/atlantis/service.js diff
    Check result of readln() against null before toUpperCase()ing it.
  356. Deucе
    Mon May 11 2026 09:27:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: hide server control menu on Events and Broker log panes These panes have no server-side recycle/pause/resume controls. The menu was appearing because serverId was set (needed for level change signals) and the menu condition only checked serverId. Now it also requires a non-empty controlLabel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  357. Deucе
    Mon May 11 2026 09:00:51 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mqttclient.cpp diff
    qtmonitor: set MQTT client ID to qtmonitor-{hostname}-{pid} Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  358. Deucе
    Mon May 11 2026 08:05:35 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: filter log panes by selected host without discarding messages Log messages are no longer dropped when they don't match the selected host. Instead, all messages are retained and the host combo controls display visibility via the existing block filter mechanism. Each log block stores its source host in LogBlockData. Changing the host combo triggers a re-filter across all log panes, hiding/showing blocks based on their host. Lines with no host (broker logs) are always visible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  359. Deucе
    Mon May 11 2026 07:23:35 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt.cpp diff
    src/sbbs3/scfg/scfgnet.c diff
    mqtt: start internal broker only when broker_addr matches local hostname Previously, enabling "Internal Broker" in SCFG forced broker_addr to 127.0.0.1 and always started the in-process broker. Now the address is configurable and compared against the local hostname (startup->host_name from the INI, falling back to gethostname()). localhost, 127.0.0.1, and ::1 are always treated as local. When the address doesn't match, the server falls through to the normal TCP client path, connecting to the remote broker with whatever TLS mode is configured. This allows multi-host BBS setups where one instance runs the internal broker and the others connect to it over the network. SCFG now shows and allows editing Broker Address in internal broker mode, and only defaults to "localhost" when the field is empty. Note: MQTT still borrows whichever startup struct happens to be passed in by the calling server — there is no mqtt_startup_t of its own. This means host_name resolution depends on the caller, and the lputs callback has to be threaded through from whoever called mqtt_startup() rather than being a first-class member of the MQTT config. A dedicated startup struct would clean up both of these and avoid the gethostname() fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  360. Deucе
    Sun May 10 2026 22:01:51 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: per-pane log level subscriptions to reduce MQTT traffic Replace wildcard log/# subscriptions with per-level subscriptions for each pane. Each server (term, mail, ftp, web, srvc), events, and broker pane independently subscribes to only the log levels it needs. Default is levels 0-6 (Normal), excluding Debug. BBS aggregate pane has no MQTT subscription of its own — it only displays messages received by other panes. Level selections are persisted via QSettings and restored on restart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  361. Deucе
    Sun May 10 2026 21:11:20 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbs.jsdocs.vcxproj diff
    src/sbbs3/sbbs.vcxproj diff
    vcxproj: update for mqtt.c→cpp rename, add mqtt_client, remove glue - mqtt.c → mqtt.cpp (both vcxproj files) - js_mqtt.c → js_mqtt.cpp (jsdocs only, was already correct in main) - Remove mqtt_broker_glue.cpp (eliminated in 2ddba195bd) - Add mqtt_client.cpp (added in 9a5dbe39f8) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  362. Deucе
    Sun May 10 2026 20:50:42 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_protocol.h diff
    mqtt_protocol.h: rename header guard to avoid collision with libmosquitto Mosquitto installs its own mqtt_protocol.h system-wide using the same MQTT_PROTOCOL_H guard. When USE_MOSQUITTO is enabled, mqtt.h pulls in the system header first via <mqtt_protocol.h>, blocking our local file from being included by mqtt_client.h / mqtt_broker.h / mqtt_topic.h. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  363. Deucе
    Sun May 10 2026 20:38:53 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_broker.cpp diff
    mqtt_broker: add DEBUG logging for unsubscribe and ping Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  364. Deucе
    Sun May 10 2026 20:31:16 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mqttclient.cpp diff
    qtmonitor: populate Clients pane from retained client/list on connect Subscribe to client/list topics on connect to populate existing clients immediately. Unsubscribe after 2 seconds since we only need the retained snapshot — ongoing updates come from client/action/* which stays subscribed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  365. Deucе
    Sun May 10 2026 20:24:16 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/statswidget.cpp diff
    qtmonitor: rearrange Statistics pane to 2x2 grid layout Today and Total side by side, Uploads Today and Downloads Today side by side. Total group top-aligned so rows line up with Today. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  366. Deucе
    Sun May 10 2026 20:20:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: display BBS system name in status bar and window title Subscribe to the BBS-level topic (sbbs/{id}) which carries the system name as a retained payload. Display it at the start of the status bar and use it as the window title. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  367. Deucе
    Sun May 10 2026 20:17:14 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: aggregate event log messages into BBS pane Event log entries were only shown in the Events pane. Now also forwarded to the BBS aggregate pane with an [Events] prefix, matching the existing pattern for server log messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  368. Deucе
    Sun May 10 2026 20:14:29 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/qtmonitor/maxconcurrentwidget.cpp diff
    src/sbbs3/qtmonitor/maxconcurrentwidget.h diff
    Modified Files:

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: add Max Connections pane, fix Reset Layout New pane showing per-IP, per-server max concurrent connection strike counts from the max_concurrent/{ip} MQTT topics. Color coded: magenta at 5+ strikes, red at 10+. Fix Reset Layout to tabify all data panes together (Activity was previously left with the log panes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  369. Deucе
    Sun May 10 2026 19:27:51 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    qtmonitor: fix action timestamps and format log timestamps Action topics use timestamp\tpayload wire format, not user properties. Parse TSV only for action topics specifically, not as a global fallback. Log timestamps: parse ISO format and display as "MMM dd hh:mm:ss" matching the format used in Activity and other panes. Formatting happens in mqttclient.cpp before signals are emitted. LogBlockData tracks timestamp length so recolorBlocks (dark mode toggle) can re-apply grey/colored split correctly without guessing the boundary by searching for spaces. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  370. Deucе
    Sun May 10 2026 19:05:26 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_broker.cpp diff
    src/sbbs3/mqtt_broker.h diff
    mqtt_broker: fix session map key collision on socket descriptor reuse Sessions were keyed by "pending-{socket}" strings. When a client disconnected, the session stayed in the map with socket=-1. When the OS reused the socket descriptor for a new connection, the old dead session was silently overwritten, corrupting broker state and causing subsequent connections to fail. Fix: key sessions by SOCKET descriptor directly. Dead sessions are erased before creating new entries for the same descriptor. Cleanup (unsubscribe, will delivery) now happens before socket close to ensure the descriptor isn't reused while cleanup is in progress. Also: use INVALID_SOCKET consistently instead of -1 for invalid socket descriptors (portability). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  371. Deucе
    Sun May 10 2026 18:05:45 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/mqtt.cpp diff
    Modified Files:

    src/sbbs3/mqtt.h diff
    src/sbbs3/mqtt_client.cpp diff
    src/sbbs3/mqtt_client.h diff
    src/sbbs3/objects.mk diff
    Removed Files:

    src/sbbs3/mqtt_broker_glue.cpp diff
    Convert mqtt.c to C++, eliminate glue layer and C wrappers All callers of the mqtt API are C++, so mqtt.c can be compiled as C++ directly. This eliminates the entire indirection layer: - mqtt.c renamed to mqtt.cpp - mqtt_broker_glue.cpp deleted — broker_lputs_adapter, local_message_callback, mqtt_internal_startup/publish/subscribe/shutdown all merged into mqtt.cpp with direct C++ calls - C-callable wrappers removed: mqtt5client_open/close/connect/disconnect/ publish/subscribe/pump/read/read_free/set_will (mqtt_client.cpp), mqtt_props_new/add_string_pair/free (mqtt_broker_glue.cpp) - mqtt.h: removed mqtt_internal_* and mqtt_props_* declarations - mqtt_client.h: removed C wrapper declarations and mqtt5client_msg struct - mqtt.cpp uses mqtt5::Client, mqtt5::Broker, mqtt5::Properties directly - Pump thread reads from Client::read() directly — no alloc/copy/free needed since caller and client share the same heap - objects.mk: removed mqtt_broker_glue NOTE: clean sbbs3 before building (stale mqtt.d references mqtt.c) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  372. Deucе
    Sun May 10 2026 15:18:42 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    xpbeep: init mixer_lock from xp_mixer_pull too, not just xp_audio_open mixer_lock is initialized lazily via pthread_once(&mixer_once, ...) in xp_audio_open. The pull side (push-backend device threads, pull- backend callbacks like the WASAPI render thread) was relying on at least one xp_audio_open having run first to set the mutex up. That held for ANSI-music / APC patches because those paths always open at least one xp_audio stream before producing samples, but the OOII enable path just calls xptone_open() with no stream, and on Win32 the WASAPI render thread fired before the mixer once-init had ever run. First xp_mixer_pull call locked an uninitialized mutex and the SEH exception terminated the whole process. POSIX is more forgiving about zero-initialized pthread_mutex_t values so the same call path didn't crash there, but it was still technically uninitialized. Calling the once-init from xp_mixer_pull makes the mutex safe regardless of which side runs first. Fixes ticket 249. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  373. Deucе
    Sun May 10 2026 13:32:07 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mqttclient.cpp diff
    qtmonitor: remove legacy TSV timestamp fallback Timestamps now come exclusively from MQTT 5.0 user properties. The tab-separated payload format was only in master for two days and all broker paths now publish proper user properties. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  374. Deucе
    Sun May 10 2026 13:16:04 GMT-0700 (PDT)
    Added Files:
    

    3rdp/build/cl-psk-only-client.patch diff
    src/sbbs3/mqtt_client.cpp diff
    src/sbbs3/mqtt_client.h diff
    Modified Files:

    3rdp/build/CMakeLists-cl.txt diff
    3rdp/build/CMakeLists.txt diff
    3rdp/build/GNUmakefile diff
    src/sbbs3/js_mqtt.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/mqtt.c diff
    src/sbbs3/mqtt.h diff
    src/sbbs3/mqtt_broker.cpp diff
    src/sbbs3/mqtt_broker.h diff
    src/sbbs3/mqtt_broker_glue.cpp diff
    src/sbbs3/objects.mk diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    src/sbbs3/scfg/scfgnet.c diff
    src/sbbs3/ver.cpp diff
    Add internal MQTT 5.0 client, fix broker user properties and PSK auth Internal MQTT 5.0 client (mqtt_client.h/.cpp): - Synchronous pump-based client sharing wire protocol with internal broker - TLS-PSK and certificate support via Cryptlib - Will message support for server disconnect detection - IPv6 support via getaddrinfo (iterates all resolved addresses) - PINGREQ keepalive (sent at keepalive/2 intervals when idle) - C-callable wrappers (mqtt5client_*) for use from mqtt.c - mqtt5client_read returns heap-allocated copy; mqtt5client_read_free frees from the same heap (safe across DLL boundaries) - PSK hex-decode for MQTT_TLS_PSK mode (scfg stores hex, Cryptlib needs raw) - Properties support on publish (threaded through to wire protocol) - SUBACK/PUBACK/PUBCOMP tracked via m_acked_pids; publish and subscribe break early on ack instead of burning full timeout - protocol_version validated (must be 5) - lprintf passed to ssl.c functions via pthread_once-initialized mutex Cryptlib patch (cl-psk-only-client.patch): - Client only offers PSK cipher suites when PSK credentials are set - Without this, Cryptlib client offers both cert and PSK suites; server prefers cert, PSK is never negotiated despite both sides having PSK - Added to GNUmakefile, CMakeLists.txt, and CMakeLists-cl.txt js_mqtt.cpp restructured: - Removed outer #if USE_MOSQUITTO gate; shared code with inline #ifdefs - #else path uses mqtt5::Client for TLS connections to remote brokers - Local client path for same-process internal broker (no TCP/TLS needed) - MQTT JS class always available regardless of USE_MOSQUITTO mqtt.c third path (no libmosquitto): - mqtt5client_* wrappers for connect/publish/subscribe/disconnect - Background pump thread for async message dispatch - Proper shutdown: mqtt->connected flag + pump_running join - Will message set to "DISCONNECTED" matching mosquitto path - mqtt_disconnect only sets connected=false in non-mosquitto path Internal broker fixes: - User properties threaded through local_publish and publish_sys - mqtt_lputs internal path now matches mosquitto: proper user properties on both log/{level} and aggregate log topics (was hacked tab-in-payload) - broker_lputs_adapter fixed similarly for $SYS/broker/log - Broker stops when last local client deregisters (was only static dtor) - shutdown() on listen socket before close to unblock accept thread - Auth rejection logging with specific reason qtmonitor: - Reads MQTT 5.0 user properties via QMqttSubscription::messageReceived - Falls back to splitTsvPayload when no user properties present - Works with both internal broker (user properties) and legacy (tab payload) Other: - MQTT JS class registered unconditionally (main.cpp) - mqtt_libver returns "mqtt5-internal" without mosquitto (ver.cpp) - SCFG: selecting Synchronet Broker TLS auto-sets port 8883 + version 5 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  375. Deucе
    Sun May 10 2026 08:23:12 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rlogin.c diff
    SyncTERM: gate rlogin OOB consume on recv()==0 + exceptfds The previous SIOCATMARK gate doesn't survive Win32: per dotnet/runtime issue #24310 and Microsoft's own docs, Winsock's SIOCATMARK has inverted polarity (returns 1 for "no OOB pending", 0 for "OOB pending") and doesn't track stream position at all — it only answers "is there urgent data still queued." In SO_OOBINLINE=on mode it's pinned to TRUE outright. That's the wrong question for an at-mark gate. What does work, on POSIX at least, is the documented pseudo-EOF. With SO_OOBINLINE off, "a read never reads across the urgent mark": when the inline read pointer reaches tp->urg_seq with the urgent byte still queued, recv() returns 0 even though the connection is alive (see Linux tcp(7), `tcp_recvmsg` in the kernel). Combined with exceptfds having fired (the oob_pending latch), recv()==0 is exactly the at-mark signal we need. So the input thread now: - Latches oob_pending on the first exceptfds notification (and drops exceptfds from the select set so we don't spin). - Reads inline normally; recv() > 0 just buffers as before. - On recv()==0 with oob_pending, treats it as the urgent-mark pseudo-EOF, pulls the byte via recv(MSG_OOB), and dispatches. Mode-switch bytes (0x10 / 0x20) still apply correctly to data following the urgent ptr because we've drained pre-mark inline in the old mode first. - On recv()<=0 with no oob_pending, treats it as a real peer-close and breaks out as before. Win32 may not reproduce the recv()==0 pseudo-EOF — Microsoft's docs say boundaries are preserved ("three reads to get past OOB") but don't pin down what the boundary read actually returns, and there's no authoritative empirical reference for it. If Windows simply blocks/EAGAINs at the boundary instead, this gate just never fires there: the OOB byte sits unread in its mailbox and the 0x80 window-size handshake still doesn't fire. That's a regression we can't fix without empirical Windows testing, but it's strictly better than the prior SIOCATMARK gate, which falsely declared at-mark on every iteration and kept calling recv(MSG_OOB) on an empty queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  376. Deucе
    Sat May 09 2026 23:11:59 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    SyncTERM: list SBBS MQTT Spy in connection-type help bbslist's online help for the Connection Type field enumerated every other type but omitted the new MQTT spy one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  377. Deucе
    Sat May 09 2026 23:06:52 GMT-0700 (PDT)
    Modified Files:
    

    src/xptls/xp_tls_botan3.cpp diff
    xp_tls_botan3: define NOMINMAX before sockwrap.h on Win32 The MQTT spy commit added <botan/tls_session.h>, whose Session constructor uses std::chrono::seconds::max() as a default argument. Win32's <windows.h> (pulled in transitively via sockwrap.h) defines max as a macro, which mangles the default argument and produces a cascade of MSVC parser errors starting at tls_session.h:254. Setting NOMINMAX before the include chain keeps the macros out and lets the chrono call parse normally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  378. Deucе
    Sat May 09 2026 22:56:39 GMT-0700 (PDT)
    Added Files:
    

    src/doors/smurfcombat/CMakeLists.txt diff
    Modified Files:

    src/doors/smurfcombat/smurf.c diff
    Merge branch 'xbit-smurfcombat-nocivil' into 'master' smurfcombat: Add -cm (clean mode) switch to skip Civil Operations menu See merge request main/sbbs!680
  379. xbit ops
    Sat May 09 2026 22:56:39 GMT-0700 (PDT)
    Added Files:
    

    src/doors/smurfcombat/CMakeLists.txt diff
    Modified Files:

    src/doors/smurfcombat/smurf.c diff
    smurfcombat: Add -cm (clean mode) switch to skip Civil Operations menu
  380. Deucе
    Sat May 09 2026 22:50:24 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/conn_mqtt.c diff
    src/syncterm/conn_mqtt.h diff
    Modified Files:

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/sbbs3/umonitor/spyon.c diff
    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Manual.txt diff
    src/syncterm/conn.c diff
    src/syncterm/conn.h diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.man.in diff
    src/syncterm/term.c diff
    src/xptls/xp_tls.h diff
    src/xptls/xp_tls_botan3.cpp diff
    src/xptls/xp_tls_none.c diff
    src/xptls/xp_tls_openssl.c diff
    SyncTERM: MQTT spy connection type via Synchronet internal broker Adds CONN_TYPE_MQTT, a sysop-spy connection that subscribes to a node's sbbs/{BBSID}/node/{N}/output topic and publishes keystrokes to the matching .../input. Authenticates to the broker via TLS-PSK using the bbslist entry's user (PSK identity), password (PSK secret), and syspass (MQTT-level password). BBSID and node number are auto-discovered from retained broker topics, with manual fallback prompts. If the PSK handshake fails the connect logic reopens the socket and retries with a plain cert handshake, which lets the same connection type also reach external brokers (mosquitto, EMQX, ...) that re-host Synchronet-shape topics. On the cert leg the MQTT-level password is just bbs->password (the operator's broker-side credentials), and the syspass slot is left alone since it isn't used. Reachable from the command line as mqtts://user:password@host. mqtts:// is the IANA scheme for MQTT-over-TLS; plain mqtt:// is intentionally not accepted because the broker doesn't speak plaintext. URL invocations with no matching dialing-directory entry prompt for the system password before the MQTT CONNECT (PSK leg only). main() ignores SIGPIPE so a peer hanging up mid-write returns EPIPE to the caller instead of killing the process; the connection layer then cleans up like any other I/O error. Includes the supporting plumbing the connection needed: - xptls: TLS-PSK client API (xp_tls_client_open_psk) + server-cert policy that pins TLS 1.2, restricts kex to PSK variants, and offers AES-128/256 in CBC + GCM with SHA-1/256/384. Adds xp_tls_has_pending so callers gating reads on socket-readability don't sit on already- decoded plaintext, and xp_tls_used_psk so the caller can tell which leg authenticated. Skips the close-time close_notify send when the socket isn't writable, avoiding SIGPIPE on a peer-closed or locally- shut-down session. - conio/cterm: split keystroke output (cterm_encode_key) from parser auto-responses (cterm_respond) so a spy can mute DSR/DECRQM/etc. host replies (the BBS is already serving the real client) while still delivering the local user's keystrokes. - sbbs3/umonitor/spyon: switch the keystroke send path to cterm_encode_key so arrow keys, F-keys, etc. are encoded for the active emulation (the old raw-byte path silently dropped them) and routes the encoded bytes to the spy socket via the new keystroke_cb. Manual.txt and the manpage list the new mqtts:// scheme and the SBBS MQTT Spy connection type.
  381. Deucе
    Sat May 09 2026 21:45:09 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_broker.cpp diff
    src/sbbs3/mqtt_broker.h diff
    mqtt: split into accept/broker threads, flush from caller, poll/select Accept thread handles blocking TLS handshakes. Broker thread uses poll() (PREFER_POLL) or select() for network session I/O with 1s timeout. local_publish and publish_sys flush network send buffers directly in the calling thread, eliminating delivery latency for local-to-network message routing. Thread names set via SetThreadName. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  382. Deucе
    Sat May 09 2026 19:38:48 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgnet.c diff
    scfg: auto-configure MQTT fields when Internal Broker is toggled on Sets broker_addr=127.0.0.1, broker_port=8883 (if was 1883), tls_mode=SBBS, protocol_version=5 so jsexec and other external MQTT clients can connect to the internal broker. Shows editable port field when internal broker is selected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  383. Rob Swindell (on Debian Linux)
    Sat May 09 2026 19:20:46 GMT-0700 (PDT)
    Modified Files:
    

    exec/str_cmds.js diff
    Don't try to invoke mqtt_spy.js unless js.global.MQTT is defined Some sysops build without libmosquitto whic h means there won't be an MQTT class and so mqtt_spy.js would fail.
  384. Deucе
    Sat May 09 2026 18:36:53 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rlogin.c diff
    SyncTERM: rlogin OOB via exceptfds + MSG_OOB Winsock2's SO_OOBINLINE + SIOCATMARK combo doesn't reliably surface RFC 1282 urgent-mode control bytes, so rlogin's mode-switch / window- size requests were dropped on Windows. Drop SO_OOBINLINE entirely and detect urgent via select()'s exceptfds, consuming the byte from its separate mailbox with recv(MSG_OOB) once SIOCATMARK shows the inline stream has drained to the mark. Same code path on Windows and POSIX. socket_check() is left untouched (many out-of-tree consumers); the input thread now uses a bespoke inline select() that watches both readfds and exceptfds. Once OOB is signaled exceptfds is dropped from the wait set to avoid a busy-loop while we wait for the inline read pointer to reach the urgent point — RFC 1282 mode-switch bytes (0x10/0x20) apply to data following the urgent ptr, so pre-mark inline data must drain in the old mode first. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  385. Deucе
    Sat May 09 2026 18:36:53 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/font_pick.wren diff
    Modified Files:

    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/auto/connected/online_menu.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_fs.c diff
    src/syncterm/wren_bind_fs.h diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    SyncTERM: move font_control to Wren Replace the C-side font_control() uifc dialog with a Wren FontApp class. The picker uses ListView over Font.name(0..count-1), with Insert keying into Host.pickFile + Font.load(file). Hook.onKey for Alt-F lives in keys_default.wren; online_menu's "Font Setup" entry now calls FontApp.run() instead of Host.fontControl(). Two new foreigns: - CTerm.altFont / CTerm.altFont=(slot) — getter/setter for cterm->altfont[0]. Setter calls ciolib_setfont(slot, false, 1) and updates the cache. - Font.load(file) — takes a File foreign (not a path string) and extracts the path internally via the new wren_file_path() helper in wren_bind_fs. Keeping bare paths out of Wren preserves the consent-token model: only paths the user explicitly approved through a picker can be opened. Two pre-existing bugs surfaced by this work, fixed in passing: - ListView.preferredHeight returned the unbounded item count, so a pane with fitContent() over the 100+ populated font slots overflowed the screen. Cap it at screen height minus a chrome budget (ListView already scrolls). - ListView.handleKey_ didn't guard `cp` before `cp >= 0x20`, so any extended key with null codepoint (Insert, function keys, arrows) that fell through the explicit handlers crashed with "Null does not implement '>=(_)'". Deletes ~75 lines: font_control() in term.c, the Alt-F switch case in doterm(), fn_Host_fontControl() shim, Host.fontControl() declaration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  386. Rob Swindell (on Windows 11)
    Sat May 09 2026 18:34:12 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_mqtt.cpp diff
    Update connect() method JSDOC comment ... to clarify that the broker to connect to can be specified by writing to the broker_addr and broker_port properties.
  387. Rob Swindell (on Debian Linux)
    Sat May 09 2026 14:04:03 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: don't call destroy_session() with sentinel tls_sess value (-1) When TLS setup fails after add_private_key() returns an error, the code calls cryptDestroySession() directly and sets tls_sess = -1, then calls close_session_no_rb() which would pass -1 to destroy_session(), triggering a spurious "Destroying a session (-1) that's not in sess_list" error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  388. Deucе
    Fri May 08 2026 21:39:27 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_broker.h diff
    mqtt: remove extern C wrapper from mqtt_broker.h includes The wrapped headers (userdat.h, ssl.h) have their own extern C guards. The wrapper caused Windows SDK templates (wspiapi.h) to be declared with C linkage, breaking MSVC builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  389. Deucе
    Fri May 08 2026 21:33:26 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt_broker.cpp diff
    mqtt: fix MSVC build errors in internal broker Move sockwrap.h out of extern "C" block to prevent Windows SDK templates from being declared with C linkage. Parenthesize std::min calls to prevent Windows min/max macro interference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  390. Deucе
    Fri May 08 2026 21:26:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbs.jsdocs.vcxproj diff
    src/sbbs3/sbbs.vcxproj diff
    mqtt: add internal broker files to MSVC projects Add mqtt_protocol.cpp, mqtt_topic.cpp, mqtt_broker.cpp, and mqtt_broker_glue.cpp to sbbs.vcxproj and sbbs.jsdocs.vcxproj. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  391. Deucе
    Fri May 08 2026 21:19:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: ignore stale page alerts on connect Skip sysop page beep/flash for retained messages older than 60 seconds, preventing false alerts when subscribing to action topics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  392. Deucе
    Fri May 08 2026 21:05:50 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/mqtt_broker.cpp diff
    src/sbbs3/mqtt_broker.h diff
    src/sbbs3/mqtt_broker_glue.cpp diff
    src/sbbs3/mqtt_protocol.cpp diff
    src/sbbs3/mqtt_protocol.h diff
    src/sbbs3/mqtt_topic.cpp diff
    src/sbbs3/mqtt_topic.h diff
    Modified Files:

    src/sbbs3/mqtt.c diff
    src/sbbs3/mqtt.h diff
    src/sbbs3/objects.mk diff
    src/sbbs3/scfg/scfgnet.c diff
    src/sbbs3/scfgdefs.h diff
    src/sbbs3/scfglib1.c diff
    src/sbbs3/scfgsave.c diff
    mqtt: add optional experimental internal MQTT 5.0 broker C++ implementation of an MQTT 5.0 broker that runs as an in-process service, eliminating the libmosquitto dependency and solving the broker.js startup ordering problem. When InternalBroker=true in [MQTT], mqtt_startup() launches a broker thread and registers each server as a local client. Publish and subscribe calls go directly to the broker's routing engine without network overhead. External clients (qtmonitor) connect via TLS-PSK using the same Cryptlib infrastructure as other Synchronet servers. Architecture: - mqtt_protocol.h/.cpp: MQTT 5.0 wire codec (VBI, UTF-8, properties, all packet types). Three-tier data model with shared_ptr messages. - mqtt_topic.h/.cpp: trie-based topic tree with +/# wildcard matching, retained message store with shared_ptr ownership and expiry. - mqtt_broker.h/.cpp: broker thread with select() event loop, local client interface (mutex-protected), network client handling, TLS-PSK via Cryptlib, PSK table from sysop user list, session management, QoS 0/1/2 state machines, will messages with delay, session expiry, clean start/resume, keep-alive timeout. - mqtt_broker_glue.cpp: C linkage bridge between mqtt.c and the C++ broker. Broker logs via startup->lputs for console and $SYS/broker/ topics for MQTT. Recursion guard prevents log→publish→log loops. - mqtt.c: dual-path logic in all mqtt_pub_*/mqtt_subscribe functions. mqtt_dispatch_message shared between mosquitto and internal broker. - scfgdefs.h/scfglib1.c/scfgsave.c: InternalBroker config field. - scfg/scfgnet.c: UI toggle, hides external broker settings when internal broker is enabled. $SYS/broker/version retained message set at startup with full version string (broker_ver()). $SYS/broker/log/{level} for broker log messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  393. Deucе
    Fri May 08 2026 21:00:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: subscribe to $SYS topics, dynamic Broker log pane Subscribe to $SYS/#. When $SYS/broker/version starts with "Synchronet MQTT Broker ", dynamically create a Broker log pane. $SYS/broker/log/{level} messages go to the Broker pane and the BBS aggregate pane. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  394. Deucе
    Fri May 08 2026 21:00:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: filter all data by selected host, show hostname in multi-host mode All MQTT signals now include the source hostname. Data is filtered by the host selector — only matching data is displayed. When "All Hosts" is selected with multiple hosts discovered, log messages are prefixed with [hostname] for disambiguation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  395. Deucе
    Fri May 08 2026 21:00:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: add multi-host support with host selector Auto-discover hosts from incoming MQTT topics. Host selector dropdown in toolbar defaults to the first host seen (single-host), with "All Hosts" for broadcasting controls to every discovered host. All server control actions (recycle, pause, resume, clear) now target the selected host(s). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  396. Deucе
    Fri May 08 2026 21:00:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: use discovered hostname in publish topics instead of wildcards MQTT wildcards (+/#) are only valid in subscription filters, not in published topic names. Auto-detect the hostname from the first received host-level message and use it for all subsequent publishes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  397. Deucе
    Fri May 08 2026 21:00:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/settingsdialog.cpp diff
    qtmonitor: use DefaultMaxLogLines in settings dialog Uses the 32-bit-aware default (2M on 64-bit, 1M on 32-bit) instead of hardcoded 2000000. Missed in the original max log lines commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  398. Deucе
    Fri May 08 2026 19:34:28 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/transfer_pick.wren diff
    Modified Files:

    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/auto/connected/online_menu.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_xfer.c diff
    src/syncterm/wren_bind_xfer.h diff
    SyncTERM: move begin_upload / begin_download to Wren Replace the C-side begin_upload() and begin_download() uifc dialogs with Wren UploadApp / DownloadApp classes — protocol picker uses ListView, file selection goes through Host.pickFile / pickFiles (uifc filepick wrapper), XMODEM filename via Prompt. Three new Transfer foreigns dispatch into the existing protocol entry points: Transfer.upload(kind, path, lastCh), Transfer.uploadBatch, Transfer.download. Kinds are stringly-typed ("zmodem", "ymodem-g", "xmodem-1k", "cet", ...) so the C side can fan out to the right xmodem_*/zmodem_*/cet_telesoftware_download call without exposing mode flag bits to Wren. The foreigns can't run wren_run_transfer directly — wrenCall is forbidden from inside a foreign method (wren.md §7). They record the user's choice in a single-slot xfer_pending struct and return immediately; xfer_drain_pending(), called from doterm() at C top-level, runs the actual dispatch. Auto-Z drains right after wren_run_upload_app() returns; Alt-D / Alt-U are picked up at the next outer-loop iteration. Hook.onKey for Alt-D / Alt-U lives in keys_default.wren (matches the Alt-B / Alt-C / Alt-X migration pattern). Conn.upload() and Conn.download() Wren foreigns are gone — online_menu now calls UploadApp.run / DownloadApp.run directly. Deletes ~210 lines of C: begin_upload, begin_download, the Alt-D / Alt-U switch cases in doterm(), fn_Conn_upload / fn_Conn_download. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  399. Deucе
    Fri May 08 2026 08:53:15 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/nodewidget.cpp diff
    src/sbbs3/qtmonitor/nodewidget.h diff
    qtmonitor: add Set Status submenu to node context menu Right-click a node to set its status (WFC, Logon, In Use, Quiet, Offline, etc.) via the node/{n}/set/status MQTT topic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  400. Deucе
    Fri May 08 2026 08:46:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    qtmonitor: recolor log entries on dark/light mode toggle Walk all blocks and update foreground colors to match the new theme. Timestamps get the appropriate gray, message text gets the level color. Batched as a single edit block for performance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  401. Deucе
    Fri May 08 2026 08:42:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: add server/system control dropdowns to log panes Per-server log panes get a "Server" dropdown with Recycle, Pause, and Resume for that server. BBS aggregate pane gets a "System" dropdown wired to the host-level Recycle All, Pause All, Resume All actions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  402. Deucе
    Fri May 08 2026 08:34:08 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: add BBS menu with host-level controls, rename old BBS to Terminal BBS menu: Recycle All, Pause All, Resume All, Clear All Login Attempts, Force Timed Event, Force Network Callout. These publish to host-level MQTT topics and affect all servers. Terminal menu now only controls the terminal server specifically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  403. Deucе
    Fri May 08 2026 08:00:31 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    qtmonitor: add incremental text filter to log panes Case-insensitive substring filter with 150ms debounce. Processes blocks in 50K chunks from newest to oldest so visible lines update instantly. Generation counter aborts stale scans on new input. Built-in clear button via QLineEdit::setClearButtonEnabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  404. Deucе
    Fri May 08 2026 07:46:52 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: add all panes to toolbar Added Login Attempts, Activity, BBS, and Events buttons to the toolbar so all panes are accessible from the button bar. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  405. Deucе
    Fri May 08 2026 07:43:57 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/loginattemptswidget.cpp diff
    src/sbbs3/qtmonitor/loginattemptswidget.h diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: add per-IP and per-server clear controls to Login Attempts Right-click individual rows to clear specific IPs. Clear dropdown button with per-server entries and "All" option. Publishes IP to host-level clear topic for per-IP, server-level clear for per-server. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  406. Deucе
    Fri May 08 2026 07:31:36 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/settingsdialog.cpp diff
    src/sbbs3/qtmonitor/settingsdialog.h diff
    qtmonitor: configurable max log lines per pane, default 2M Replaces hardcoded 5000-line cap with a global setting (Settings > Log > Max lines per pane). Default 2,000,000 covers a full day at debug level. Changing the setting trims existing logs immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  407. Deucе
    Fri May 08 2026 07:20:14 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/qtmonitor/textutil.h diff
    Modified Files:

    src/sbbs3/qtmonitor/actionwidget.cpp diff
    src/sbbs3/qtmonitor/clientwidget.cpp diff
    src/sbbs3/qtmonitor/loginattemptswidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/nodewidget.cpp diff
    qtmonitor: sanitize control characters in all MQTT-sourced text Shared textutil.h provides setItemText() for QTreeWidget cells: translates C0/DEL to Unicode Control Pictures with a cell tooltip listing each control char found. Applied to all widgets displaying MQTT data. Deduplicated control char tables from logwidget.cpp. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  408. Rob Swindell (on Debian Linux)
    Thu May 07 2026 20:26:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/writemsg.cpp diff
    writemsg: change l to long so ftell() error check works (CID 645990) l was declared uint, so the (l < 0) check after l = (long)ftell(stream) was always false. ftell() returns long and can return -1 on error. Update the (ulong) casts on the level_linespermsg comparisons accordingly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  409. Deucе
    Thu May 07 2026 20:03:10 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind_xfer.c diff
    wren_bind_xfer: use plain access on _Atomic vars where it suffices For _Atomic-qualified lvalues, plain reads and assignments emit seq_cst atomic loads/stores — identical to atomic_load / atomic_store with default memory order. The function forms only earn their keep for atomic_exchange and atomic_compare_exchange_strong (no implicit equivalent), which stay. Also sidesteps an MSVC clang-cl issue where atomic_load(&x) used inline as a function argument expanded such that the address operator got lost, leaving the underlying type where a pointer-to-atomic was expected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  410. Deucе
    Thu May 07 2026 19:52:02 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind_xfer.c diff
    wren_bind_xfer: undef __STDC_NO_ATOMICS__ before stdatomic.h on MSVC MSVC defensively defines __STDC_NO_ATOMICS__, which makes <stdatomic.h> expand to nothing — atomic_load/store/exchange fail to compile. conn.h already uses this dance; mirror it here. Also switch declarations to the _Atomic T form to match the codebase convention (rlogin.c, threadwrap.h). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  411. Deucе
    Thu May 07 2026 19:33:08 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind_xfer.c diff
    wren_bind_xfer: use _Atomic keyword instead of stdatomic.h typedefs MSVC's <stdatomic.h> doesn't typedef atomic_bool / atomic_uint to a real atomic type, so the clang frontend (under /experimental:c11atomics) errors with C7707 — atomic_load saw plain int instead of _Atomic int. Switch to the keyword form (static _Atomic bool ...), matching the pattern rlogin.c already uses for its rlogin_active / rlogin_input_paused flags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  412. Deucе
    Thu May 07 2026 19:24:51 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    src/xptls/CMakeLists.txt diff
    syncterm/xptls: fix add_subdirectory ordering for xpdev dep Move syncterm's add_subdirectory(../xptls ...) to after require_libs() so xpdev is already a target when xptls is processed. Without that, xptls's "if(NOT TARGET xpdev) add_subdirectory(../xpdev ...)" fired in xptls's scope and require_libs() then re-added xpdev in syncterm's scope, hitting CMP0002 (duplicate target). With the ordering correct, the fallback branch in xptls is only reached for standalone builds where there's no parent at all — plain set() works there, so drop the CACHE INTERNAL workaround. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  413. Deucе
    Thu May 07 2026 19:21:36 GMT-0700 (PDT)
    Modified Files:
    

    src/xptls/CMakeLists.txt diff
    xptls: set xpdev_DONE in CACHE so parent require_libs sees it set(xpdev_DONE TRUE) in xptls's scope doesn't propagate up to syncterm's scope where require_libs() runs, so syncterm was re-adding the xpdev subdirectory and triggering CMP0002 (duplicate target). CACHE INTERNAL makes the flag global, matching how require_lib_dir's own set() works inside the macro caller's scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  414. Deucе
    Thu May 07 2026 19:19:40 GMT-0700 (PDT)
    Modified Files:
    

    src/xptls/CMakeLists.txt diff
    xptls: depend on xpdev target for portability defines Replace the duplicated platform / HAS_STDINT_H / HAS_INTTYPES_H checks with a single source of truth: link xpdev. xpdev's CMakeLists already publishes the right defines as PUBLIC on its target, but xptls only borrowed xpdev's headers without linking, so they didn't propagate. Add xpdev as a subdirectory if the parent hasn't already (using the same xpdev_DONE flag require_lib_dir uses, so syncterm's later require_libs() doesn't double-add). Fixes MSVC int8_t/int32_t/uint32_t redefinition errors caused by gen_defs.h's typedef block firing without HAS_STDINT_H set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  415. Deucе
    Thu May 07 2026 19:10:51 GMT-0700 (PDT)
    Modified Files:
    

    src/xptls/CMakeLists.txt diff
    xptls: honor BOTAN3_VENDORED_TARGET on Win32 When the parent project builds Botan via ExternalProject (typical on Windows where pkg-config is rarely installed), it pre-creates an IMPORTED Botan target and exports the cache hint BOTAN3_VENDORED_TARGET. Use that target directly instead of unconditionally requiring pkg-config — mirrors src/ssh/CMakeLists.txt. Fixes "Could NOT find PkgConfig" on Win32 CI runners. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  416. Deucе
    Thu May 07 2026 19:02:15 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    src/syncterm/wren_bind_xfer.c diff
    src/syncterm/wren_bind_xfer.h diff
    src/xptls/CMakeLists.txt diff
    Wren transfer mailbox: portable threading + xptls Mac/Linux fixes Replace POSIX-only primitives in wren_bind_xfer.c with the threadwrap.h + eventwrap.h abstractions that the rest of SyncTERM already uses: - pthread_create/pthread_join -> _beginthread + xpevent_t worker_done - pthread_cond_t -> xpevent_t dlg.evt (worker drops mutex, WaitForEvent(100ms), reacquires; main SetEvent under lock) - PTHREAD_*_INITIALIZER -> one-time runtime init via pthread_once (Win32 mutex is CRITICAL_SECTION; no static init available) - syslog.h -> xp_syslog.h for the LOG_* severity constants we use as in-app LogView color tags (no actual syslog calls) - worker fn signature changed to void(void *) for _beginthread C11 atomics (atomic_bool / atomic_uint) kept — MSVC supports them. xptls/CMakeLists.txt: mirror xpdev's platform compile_definitions (__unix__ / __DARWIN__ on Mac, _GNU_SOURCE etc. on Linux, _WIN32 versions on Win32). xptls is structured to be standalone — it borrows xpdev's headers without linking the target — so it didn't inherit xpdev's PUBLIC defines, which broke macOS builds where sockwrap.h gates its socket headers on __unix__. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  417. Deucе
    Thu May 07 2026 18:38:26 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/actionwidget.cpp diff
    src/sbbs3/qtmonitor/actionwidget.h diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/settingsdialog.cpp diff
    src/sbbs3/qtmonitor/settingsdialog.h diff
    qtmonitor: parse activity fields, add Show filter, sysop page alerts Activity pane: parse per-action-type TSV fields into User and Details columns. Show menu pre-seeded with known action types, dynamically adds unknown types below a separator. Renamed pane to Activity. Sysop page: flash taskbar and beep on page action. Configurable via Settings > Notifications > Alert on sysop page. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  418. Deucе
    Thu May 07 2026 18:12:38 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/qtmonitor/actionwidget.cpp diff
    src/sbbs3/qtmonitor/actionwidget.h diff
    Modified Files:

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: add Actions pane for BBS activity feed Subscribe to sbbs/{id}/action/# topics and display login, logout, newuser, post, exec, upload, download, page, and error events in a sortable table with Time, Action, Detail, and Info columns. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  419. Deucе
    Thu May 07 2026 18:08:41 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: show server version in status bar Subscribe to server version topic and display extracted version number (e.g. "3.20a") before the server state in the status bar. Debug builds show "-Dbg" suffix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  420. Deucе
    Thu May 07 2026 17:58:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    qtmonitor: subscribe to event logs, fix BBS and Events tab roles BBS tab is now the aggregate of all server logs (was Events). Events tab now shows actual event thread logs (timed events, network callouts) from sbbs/{id}/host/+/event/log/#, matching the Windows ctrl/ panel's Events tab. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  421. Deucе
    Thu May 07 2026 17:48:59 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/clientwidget.cpp diff
    src/sbbs3/qtmonitor/loginattemptswidget.cpp diff
    qtmonitor: sort date columns chronologically in Clients and Login Attempts Store QDateTime in Qt::UserRole for date columns and use a SortableItem subclass that compares by QDateTime when present. Affects Time (Clients), First Attempt and Last Attempt (Login Attempts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  422. Deucе
    Thu May 07 2026 17:45:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/clientwidget.cpp diff
    qtmonitor: sort Socket and Port columns numerically in Clients tab Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  423. Deucе
    Thu May 07 2026 17:29:36 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    qtmonitor: add log level filter menu with per-level visibility toggles Show button opens a dropdown menu with checkable entries for each log level (Emergency through Debug) with colored dot icons. Filters use QTextBlock visibility so no backing store is needed. The existing level combo box controls what gets captured, the filter menu controls what is displayed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  424. Deucе
    Thu May 07 2026 17:15:06 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/logwidget.cpp diff
    qtmonitor: display control characters as Unicode Control Pictures with tooltips Renders C0 controls (0x00-0x1F) and DEL (0x7F) as their Unicode Control Pictures equivalents. Hover tooltip shows C escape (when applicable), caret notation, hex value, and ASCII name. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  425. Deucе
    Thu May 07 2026 16:09:18 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/transfer_app.wren diff
    src/syncterm/scripts/ui_logview.wren diff
    src/syncterm/scripts/ui_logview_test.wren diff
    src/syncterm/scripts/ui_progress.wren diff
    src/syncterm/scripts/ui_progress_test.wren diff
    src/syncterm/wren_bind_xfer.c diff
    src/syncterm/wren_bind_xfer.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_style.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    SyncTERM: move transfer-window UI to Wren Replace the C-side draw_transfer_window/transfer_complete plumbing and the per-protocol cprintf progress callbacks with a Wren TransferApp driven by a worker thread + bounded mailbox. C side runs each protocol on its own thread and pushes pre-formatted status lines through xfer_tick_lock/get/unlock; the main thread stays in TransferApp's modal loop, which composes ProgressBar and LogView widgets. Worker-to-main marshalling covers the two thread-unsafe UI paths: duplicate-file resolution and the YMODEM->XMODEM filename prompt fallback. All six protocol entry points (zmodem recv/send/batch, xmodem/ymodem recv/send/batch, cet recv) now flip binary mode and hand off to wren_run_transfer; lputs auto-routes via the mailbox when xfer_session_active(). Drops ~1100 lines of C — the legacy log_ti/progress_ti/transw_ti windows, ask_overwrite, the per-protocol progress functions, and the cputs-into-window logging path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  426. Deucе
    Thu May 07 2026 16:05:10 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/main.cpp diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    src/sbbs3/qtmonitor/settingsdialog.cpp diff
    src/sbbs3/qtmonitor/settingsdialog.h diff
    qtmonitor: add client certificate and CA support for TLS Allows connecting with CA-verified server certs and/or mutual TLS client certificates. CA file is also used to trust the server cert. CLI: --ca-file, --cert-file, --key-file Settings: TLS group (CA) + Client Certificate group (cert/key) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  427. Deucе
    Thu May 07 2026 15:47:19 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/ircd/server.js diff
    ircd.js: add diagnostic logging to SJOIN and TOPIC server handlers Same propagation pattern as KICK — origin.bcast_to_channel with bounce=false. Log origin resolution, local status, and source server to help diagnose any desync across linked servers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  428. Deucе
    Thu May 07 2026 15:38:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/ircd/server.js diff
    ircd.js: add diagnostic logging to server KICK handler Log each early-exit path (missing target, unknown channel, unknown user, user not in channel) and the full kick context (origin, target, local status, source server) to help diagnose the kick propagation desync across linked servers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  429. Deucе
    Thu May 07 2026 14:28:54 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/ircd/core.js diff
    exec/load/ircd/server.js diff
    exec/load/ircd/user.js diff
    ircd.js: flush kicked user's queued messages from channel sendqs In addition to filtering the kicked user's recvq, also remove their already-queued PRIVMSG/NOTICE messages from all channel members' and servers' sendqs. This prevents flood text that was already broadcast before the kick from continuing to appear in the channel. The flush runs before the KICK broadcast so the kick message is not buried behind hundreds of queued flood lines. Note: a pre-existing desync issue where kicks don't propagate to all linked servers remains unfixed. Dedicated to Chalupy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  430. Deucе
    Thu May 07 2026 14:07:02 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/ircd/core.js diff
    exec/load/ircd/server.js diff
    exec/load/ircd/user.js diff
    ircd.js: flush kicked user's pending channel messages When a user is kicked from a channel, filter their recvq to remove pending PRIVMSG/NOTICE lines destined for that channel. Previously, a user who pasted a large block of text would continue to flood the channel after being kicked because the lines were already queued in their recvq and would be processed one at a time by the throttled recvq processor. Only messages to the kicked channel are removed; messages to other channels and other commands are preserved. Dedicated to Chalupy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  431. Deucе
    Thu May 07 2026 13:05:19 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    qtmonitor: require Qt6WebSockets before building Qt MQTT from source Qt MQTT has a transitive dependency on Qt6WebSockets. Make it a REQUIRED component so cmake fails at configure time with a clear message rather than during the qtmqtt build. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  432. Deucе
    Thu May 07 2026 12:49:39 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    qtmonitor: auto-build Qt MQTT from source when not packaged Ubuntu and some other distros don't package qt6-mqtt. Fall back to building it from source via FetchContent, matching the installed Qt version tag. System packages are preferred when available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  433. Deucе
    Thu May 07 2026 12:42:38 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    qtmonitor: WIN32 subsystem, PRIVATE linkage, qt_finalize_target Add WIN32 to suppress console window on Windows (no-op on Unix). Use PRIVATE keyword for target_link_libraries as required by qt_finalize_target() which handles WinMain bridging. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  434. Deucе
    Thu May 07 2026 12:06:00 GMT-0700 (PDT)
    Added Files:
    

    install/init.d/broker diff
    install/rc.d/broker diff
    install/systemd/broker.service diff
    Modified Files:

    install/init.d/sbbs diff
    install/rc.d/sbbs diff
    install/systemd/sbbs.service diff
    install: add broker.js service scripts for systemd, rc.d, and init.d Add startup scripts for running broker.js (MQTT broker) via jsexec as a system service. Each script uses the same SBBSCTRL convention as the existing sbbs scripts. Startup ordering: broker declares itself Before sbbs (systemd) or BEFORE sbbs (rc.d). The init.d script uses chkconfig priority 85 (before sbbs at 89). Comments added to existing sbbs scripts explaining how to add an explicit dependency on the broker service. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  435. Deucе
    Thu May 07 2026 11:19:58 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    qtmonitor: fix crash on reconnect, fix Connect button label QMqttClient was deleted after the QSslSocket it referenced, causing a use-after-free in ~QMqttClient when it tried to disconnect signals. Delete the client before the socket in disconnectFromBroker(). Also toggle the toolbar button between "Connect" and "Disconnect". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  436. Deucе
    Thu May 07 2026 10:35:40 GMT-0700 (PDT)
    Modified Files:
    

    exec/broker.js diff
    broker.js: fix memory leaks from subscription references Subscription.remove() used topics[i][this.client_id] instead of topics[i].subscribers[this.conn.client_id] — the same property-path bug fixed in the constructor (a8a6bfebe). Subscriptions were never actually removed from Topic subscriber maps, keeping Connection objects alive through circular references indefinitely. Added removeAllSubscriptions() method and call it from: - tearDown() when session_expiry is 0 (no session persistence) - expireSession() before deleting from broker.disconnected - handleCONNECT when clean_start replaces an old connection Also clean up empty subscriber entries from topic maps after the last subscription for a client is removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  437. Deucе
    Thu May 07 2026 10:02:36 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qtmonitor/clientwidget.cpp diff
    src/sbbs3/qtmonitor/loginattemptswidget.cpp diff
    qtmonitor: fix compact ISO timestamp parsing Synchronet's time_to_isoDateTimeStr() produces compact ISO format (20260507T143201-0700) which Qt's ISODate parser doesn't handle. Fall back to explicit yyyyMMddTHHmmss format when ISODate fails. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  438. Deucе
    Thu May 07 2026 09:56:10 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/qtmonitor/resources.qrc diff
    Modified Files:

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    src/sbbs3/qtmonitor/main.cpp diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    qtmonitor: rename Telnet tab to Terminal, add sync.ico app icon Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  439. Deucе
    Thu May 07 2026 09:38:05 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/qtmonitor/CMakeLists.txt diff
    src/sbbs3/qtmonitor/LICENSE diff
    src/sbbs3/qtmonitor/clientwidget.cpp diff
    src/sbbs3/qtmonitor/clientwidget.h diff
    src/sbbs3/qtmonitor/loginattemptswidget.cpp diff
    src/sbbs3/qtmonitor/loginattemptswidget.h diff
    src/sbbs3/qtmonitor/logwidget.cpp diff
    src/sbbs3/qtmonitor/logwidget.h diff
    src/sbbs3/qtmonitor/main.cpp diff
    src/sbbs3/qtmonitor/mainwindow.cpp diff
    src/sbbs3/qtmonitor/mainwindow.h diff
    src/sbbs3/qtmonitor/mqttclient.cpp diff
    src/sbbs3/qtmonitor/mqttclient.h diff
    src/sbbs3/qtmonitor/nodewidget.cpp diff
    src/sbbs3/qtmonitor/nodewidget.h diff
    src/sbbs3/qtmonitor/settingsdialog.cpp diff
    src/sbbs3/qtmonitor/settingsdialog.h diff
    src/sbbs3/qtmonitor/statswidget.cpp diff
    src/sbbs3/qtmonitor/statswidget.h diff
    Modified Files:

    src/sbbs3/qtmonitor/.gitignore diff
    src/sbbs3/qtmonitor/CLAUDE.md diff
    src/sbbs3/qtmonitor/README.md diff
    Removed Files:

    src/sbbs3/qtmonitor/client_widget.py diff
    src/sbbs3/qtmonitor/log_widget.py diff
    src/sbbs3/qtmonitor/login_attempts_widget.py diff
    src/sbbs3/qtmonitor/mainwindow.py diff
    src/sbbs3/qtmonitor/mqtt_client.py diff
    src/sbbs3/qtmonitor/node_widget.py diff
    src/sbbs3/qtmonitor/pyproject.toml diff
    src/sbbs3/qtmonitor/qtmonitor.py diff
    src/sbbs3/qtmonitor/requirements.txt diff
    src/sbbs3/qtmonitor/settings_dialog.py diff
    src/sbbs3/qtmonitor/stats_widget.py diff
    src/sbbs3/qtmonitor/tls_psk.py diff
    qtmonitor: rewrite in C++ with Qt6 Widgets + Qt MQTT Replace the Python/PySide6 prototype with a native C++ implementation. No Python dependency, no fragile ctypes TLS-PSK hack. Uses QSslSocket's native preSharedKeyAuthenticationRequired signal for TLS-PSK authentication with broker.js. PSK ciphers filtered from QSslConfiguration::supportedCiphers() since setCiphers(QString) doesn't accept OpenSSL directives. TLS managed independently; QMqttClient receives the encrypted socket as an IODevice transport. SSL certificate errors prompt the user for confirmation; accepted errors are remembered for subsequent reconnects within the session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  440. Deucе
    Thu May 07 2026 08:41:32 GMT-0700 (PDT)
    Added Files:
    

    src/xptls/CMakeLists.txt diff
    src/xptls/LICENSE diff
    src/xptls/xp_crypt.h diff
    src/xptls/xp_crypt_botan3.cpp diff
    src/xptls/xp_crypt_none.c diff
    src/xptls/xp_crypt_openssl.c diff
    src/xptls/xp_tls.h diff
    src/xptls/xp_tls_botan3.cpp diff
    src/xptls/xp_tls_none.c diff
    src/xptls/xp_tls_openssl.c diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    xptls: move xp_tls/xp_crypt to standalone library Move xp_tls and xp_crypt from syncterm/ to src/xptls/ as a standalone static library with its own CMakeLists.txt and BSD 2-clause license. The library auto-detects Botan/OpenSSL/none backends and can be used as a CMake subdirectory by any Synchronet component. SyncTERM's CMakeLists.txt and GNUmakefile updated to reference the new location. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  441. Deucе
    Thu May 07 2026 02:03:19 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/qtmonitor/.gitignore diff
    src/sbbs3/qtmonitor/CLAUDE.md diff
    src/sbbs3/qtmonitor/README.md diff
    src/sbbs3/qtmonitor/client_widget.py diff
    src/sbbs3/qtmonitor/log_widget.py diff
    src/sbbs3/qtmonitor/login_attempts_widget.py diff
    src/sbbs3/qtmonitor/mainwindow.py diff
    src/sbbs3/qtmonitor/mqtt_client.py diff
    src/sbbs3/qtmonitor/node_widget.py diff
    src/sbbs3/qtmonitor/pyproject.toml diff
    src/sbbs3/qtmonitor/qtmonitor.py diff
    src/sbbs3/qtmonitor/requirements.txt diff
    src/sbbs3/qtmonitor/settings_dialog.py diff
    src/sbbs3/qtmonitor/stats_widget.py diff
    src/sbbs3/qtmonitor/tls_psk.py diff
    qtmonitor: cross-platform BBS monitor using PySide6 and MQTT PySide6/Qt-based replacement for the Windows-only ctrl/ panel. Connects to broker.js via MQTT 5.0 with TLS-PSK authentication. Features: - Real-time log viewing for all servers with colour-coded levels - Node status monitoring with verbose descriptions - Connected client tracking - Failed login attempt tracking - Server state and statistics in status bar - Server control (recycle, pause, resume, clear login attempts) - Node control (lock, down, interrupt, rerun, send message) - Force timed events and network callouts - Dockable/tabbed layout with dark/light theme toggle - TLS-PSK via native Python 3.13 API or ctypes fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  442. Deucе
    Thu May 07 2026 02:03:19 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt.c diff
    mqtt.c: fix server state not published correctly on connect mqtt_server_state() only updated mqtt->server_state inside the publish block, so when mqtt->connected was false (race between synchronous TCP connect and async CONNACK callback), the state transition was lost. Move the state update before the publish so it's always tracked. mqtt_server_startup() hardcoded SERVER_INIT, overwriting the real state if the MQTT connect callback fired after the server had already transitioned to SERVER_READY. Use mqtt->server_state instead. On reconnect (server_version is NULL), re-publish the current state so the broker has the correct retained server-level status. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  443. Deucе
    Thu May 07 2026 02:03:19 GMT-0700 (PDT)
    Modified Files:
    

    exec/broker.js diff
    broker.js: fix subscriber registration for existing topics The Subscription constructor was writing to topics[i][this.client_id] (a dangling property on the Topic object with this.client_id being undefined) instead of topics[i].subscribers[conn.client_id]. This meant subscribers were never registered in the .subscribers map for topics that already existed at subscribe time. Retained messages still worked because they were sent directly from the constructor, but live publishes never found the subscriber. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  444. Rob Swindell (on Debian Linux)
    Thu May 07 2026 01:03:37 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ssl.c diff
    ssl: fix macOS build of internal_do_cryptInit (CID 483188 follow-up) e3c1569fc added a _Static_assert that CRYPTLIB_PATCHES is at least 32 chars, but some build configs (macOS, exec/testbuild.js nightly) define it empty to deliberately skip the patch-version check. The assert tripped that build. Replace the assert with a sizeof() runtime guard wrapping the cryptGetAttributeString / memcmp / asprintf block. Compilers fold the sizeof comparison constant per build, so: - When CRYPTLIB_PATCHES is the real 36-byte literal, the block is kept and Coverity sees the memcmp is safely bounded. - When CRYPTLIB_PATCHES is "", the block is dropped entirely and we never attempt the 32-byte read past the empty literal. GitLab CI pipelines pass; this only affects the nightly testbuild configurations that leave CRYPTLIB_PATCHES empty. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  445. Deucе
    Wed May 06 2026 23:58:09 GMT-0700 (PDT)
    Modified Files:
    

    exec/broker.js diff
    broker.js: fix PUBLISH payload framing and subscription ID handling serializePayload() was wrapping the payload with encodeBinaryData() or encodeUTF8String(), both of which prepend a 2-byte length prefix. MQTT PUBLISH payloads are raw — the length is implicit from the fixed header's remaining length field. This caused all forwarded messages to have garbage bytes prepended. dupeForSubscriptions() was collecting topic filter strings (the object keys from the subscriber map) instead of the subscription_id from each Subscription object. Also skip null/zero subscription IDs rather than encoding them as property 11 value 0, which violates the MQTT 5.0 spec (SubscriptionIdentifier must be in the range 1-268435455). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  446. Rob Swindell (on Debian Linux)
    Wed May 06 2026 23:13:13 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_socket.cpp diff
    js_socket: cast setsockopt TCP_NODELAY to void in TLS session setup (CID 639936) Same pattern as the websrvr TCP_NODELAY fix in 91988f5ef: TCP_NODELAY is a best-effort latency optimization for the TLS handshake; if the setsockopt is rejected (e.g. on a non-TCP socket) the session still works. Make the discarded return explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  447. Rob Swindell (on Windows 11)
    Wed May 06 2026 23:03:34 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: include protocol, IP, request, and ARS in no-auth log The "!No authentication information" debug log line now reports the protocol, client address, request line, and the ARS string that triggered the auth requirement, so it's actionable when WEB_OPT_DEBUG_RX is on. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  448. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:51:27 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: skip getuserdat for anonymous sessions in http_logon Regression from 9e7649fe0: when http_logon is called with usr=NULL on an anonymous request (session->user.number == 0), getuserdat legitimately fails because user 0 doesn't exist, which now spams the log with '!ERROR reading user #0 data' on every anon hit. Only call getuserdat when there's an actual user number to read. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  449. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:32:06 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    ftpsrvr: handle return values in send/receive_thread and sock_recvbyte (CIDs 643130, 643142, 643143) - send_thread/receive_thread: log a warning if fseeko to xfer.filepos fails so a seek error is no longer silent (downstream I/O would proceed at the wrong offset). Continues into the transfer loop either way to preserve existing behavior. - sock_recvbyte: cast the inner cryptSetAttribute (re-arming the read timeout) to void; this is best-effort and the matching call above it is already error-checked. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  450. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:32:04 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/genwrap.c diff
    genwrap: cast strlcat to void in add_suffix (CID 640959) add_suffix appends a unit suffix to a duration string built by safe_snprintf into a fixed buffer; the strlcat truncation case is acceptable (the caller would just see a slightly shorter string). Make the discarded returns explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  451. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:32:04 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_msgbase.cpp diff
    js_msgbase: cast smb_getstatus to void for last_msg/total_msgs reads (CID 639938) The status read is a best-effort refresh of cached counters in the JS property accessors; any cryptSetAttribute or smb_getstatus failure is surfaced through the resulting (potentially stale) value rather than the return code. Make the discarded return explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  452. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:56 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/dupefind.c diff
    dupefind: bundle smb_fseek into the smb_fread error path (CID 515658) A failed smb_fseek would have left the file at the wrong position and the subsequent smb_fread caught the consequence indirectly. Make the seek failure take the same explicit error path as a read failure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  453. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:53 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ssl.c diff
    ssl: cast DO() to void in get_ssl_cert key load (CID 544155) The cryptGetPrivateKey result is captured via the cert_entry->cert out-parameter and the loop's 'cert == -1' check, which is the actual condition the caller acts on. The DO() macro return is informational only here. Make the discarded return explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  454. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:50 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/useredit.cpp diff
    useredit: log error if user_config getuserdat fails (CIDs 516411, 530902) After invoking the external user-config module the in-memory user_t needs to be re-read; if getuserdat fails the caller proceeds with stale data. Log the error like purgeuser() already does. CID 516411 was originally reported against the now-removed maindflts() function; it appears to have been merged into user_config(), so the single fix covers both CIDs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  455. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/writemsg.cpp diff
    writemsg: handle fseek/fexistcase return values (CIDs 486496, 548248) - writemsg(): two fexistcase() calls are case-fix-only (same pattern as 76b5c7f43); cast to (void). - movemsg(): combine fseek+fread error handling so a seek failure takes the same error-recovery path as a read failure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  456. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:43 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt.c diff
    mqtt: cast putnmsg to void in mqtt_message_received (CID 469140) The MQTT bridge forwards an inbound payload to the node; if delivery fails (node not running, etc.) there's nothing useful for the MQTT callback to do with the error. Make the discarded return explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  457. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sftp.cpp diff
    sftp: cast remove() to void in sftp_cleanup_callback (CID 487169) The unlink of the incomplete-upload local file is best-effort; an already-removed file or permission error during cleanup shouldn't fail the cleanup. Make the discarded return explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  458. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:31:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    main: cast cryptSetAttribute SSH_CHANNEL_ACTIVE deactivation to void (CID 487166) In crypt_pop_channel_data the inner cryptSetAttribute that flips the selected channel inactive is best-effort (we're tearing down anyway). Make the discarded return explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  459. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:16:09 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/atcodes.cpp diff
    atcodes: suppress INTEGER_OVERFLOW for CDTLEFT credit cast (CID 640970) The cast of user_available_credits() (uint64_t) to int64_t overflows only if credits exceed INT64_MAX (~9.2 quintillion). Not a real concern; SUPPRESS with explanation rather than a runtime clamp. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  460. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:16:06 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: clamp tls_sent and explicit cast in sess_sendbuf return (CID 639935) The TLS path assigns 'result = tls_sent' where tls_sent is int and could theoretically be negative on cryptlib edge cases. Adding it to size_t 'sent' would underflow. Guard with 'if (result > 0)'. Also make the size_t-to-int returns explicit casts so Coverity sees the narrowing is intentional. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  461. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:16:03 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/str.cpp diff
    str: explicit truncation cast in spy() char assignment (CID 549016) After the 'if (in == NOINP) continue;' check, in is in [0,255], so 'ch = in' is safe — but Coverity flags it because incom() returns int and ch is char. Make the truncation explicit with (char)in to silence the analyzer; semantics unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  462. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:16:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/getmsg.cpp diff
    getmsg: guard idx.number underflow in getmsgnum (CID 530525) When smb_getmsgidx_by_time succeeded but returned idx.number == 0 (should not happen for valid messages, but defensively), 'idx.number - 1' underflowed uint32_t and produced 0xFFFFFFFF == ~0, the function's error sentinel — accidentally correct, but undefined per signed-int return. Make the zero-number case explicitly return ~0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  463. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:15:57 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_system.cpp diff
    js_system: use UINT_TO_JSVAL for node.extaux to avoid signed overflow (CID 530515) node.extaux is uint32_t; the cast to (int) for INT_TO_JSVAL could yield a negative value for extaux > INT_MAX. Use UINT_TO_JSVAL like the rest of the codebase already does for this field (e.g., js_system.cpp:2717). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  464. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:11:08 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_socket.cpp diff
    js_socket: close socket on fail-path in connected_socket_constructor (CID 530501) The 'fail:' label freed p without closing p->sock, leaking the socket handle when set_socket_options() failed after a successful socket() call. Initialize p->sock to INVALID_SOCKET right after the memset (so the getaddrinfo-failure path doesn't accidentally close fd 0) and have the fail label closesocket() when the socket is valid. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  465. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:11:05 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/smbutil.c diff
    smbutil: free datoffset on fread early-return in packmsgs() (CID 462184) Three bare 'return;' statements after the smb-header-rewrite reads leaked datoffset (allocated just above for the per-msg offset map). Free it before returning. Other resources at these sites (tmp file handles, smb header lock) are pre-existing leaks that Coverity did not flag and are out of scope for this CID. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  466. Rob Swindell (on Debian Linux)
    Wed May 06 2026 22:10:58 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/smbutil.c diff
    smbutil: free idxbuf and unlock smbhdr on terminated abort in maint() (CID 644892) Five 'if (terminated) return;' sites in maint() leaked idxbuf (heap) and left the SMB header lock held. The deletion-execution loop also left the SMB-allocation file handles open. Mirror the existing "nothing to delete" cleanup before each early return. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  467. Rob Swindell (on Windows 11)
    Wed May 06 2026 21:19:46 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    Fix Borland C++ build of xp_audio_open() Broken by Claude
  468. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_socket.cpp diff
    js_socket: fix js_sendto getaddrinfo error-capture parens (CID 639937) The expression was if ((result = getaddrinfo(...) != 0)) which parses as result = (getaddrinfo(...) != 0), so result becomes 0 or 1 instead of the actual EAI_* error code. The subsequent gai_strerror(result) and "%d" format then report the wrong error. Move the closing paren so the assignment captures the real return: if ((result = getaddrinfo(...)) != 0) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  469. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    main: suppress ssh_mutex/sftp_state inter-procedural FPs (CIDs 469167, 487167) CID 469167 (output_thread SLEEP): GCESSTR's lprintf runs while ssh_mutex is held. Releasing+reacquiring the mutex around the SSH error report would race the surrounding error-handling sequence (ssh_errors++, online=FALSE) and is the wrong tradeoff for a fast log write. Annotate as intentional design. CID 487167 (crypt_pop_channel_data LOCK at function end): sftp_state->mtx is acquired+released entirely inside sftps_recv; crypt_pop_channel_data never holds it across return. Coverity propagates a phantom lock state through the helper. Note: CID 487173 (sftp_send LOCK leak in src/sbbs3/sftp.cpp) was already mitigated in current source — every error path now releases ssh_mutex before returning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  470. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/logfile.cpp diff
    src/sbbs3/main.cpp diff
    main,logfile: suppress nodefile_mutex inter-procedural FPs (CIDs 515594, 515595, 515596, 543172) Same root cause as the getnode.cpp CIDs already mitigated by the nodefile_mutex refactoring (mutex is now confined entirely to getnodedat/putnodedat, both of which lock+unlock atomically). The flagged sites in daily_maint, logoffstats, sbbs_t::errormsg, and the sbbs_t destructor never touch nodefile_mutex directly — Coverity is propagating phantom lock state through the helper-function call chain. CID 515594: smb_open_sub SLEEP in daily_maint CID 515595: errormsg LOCK in logoffstats CID 515596: errormsg LOCK at logfile.cpp function end CID 543172: js_cleanup SLEEP in sbbs_t destructor Annotate each site with a SUPPRESS pointing at the invariant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  471. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_console.cpp diff
    js_console: document js_do_lock_input asymmetric contract + SUPPRESS (CID 469125) js_do_lock_input is intentionally asymmetric — it locks OR unlocks the caller-side input_thread_mutex based on its bool argument. The JS-binding side (console.lock_input(true|false)) and the C-side hotkey handlers rely on this to bracket interactive prompts. Coverity's lock tracker can't model the asymmetric contract, so it flags the lock=true path as "returning without unlocking" and propagates the complaint to every transitive caller (CID 469134 editfile, 470386 uploadfile, 470390 viewfile, 479098 pack_rep, 470388 handle_ctrlkey, etc.). Document the contract in a function-leading comment and suppress the LOCK warnings at the actual lock/unlock sites. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  472. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ssl.c diff
    ssl: suppress destroy_session false positives (CIDs 479100, 530506) CID 530506 (psess->next MISSING_LOCK): Coverity confused the two distinct list mutexes. sess_list nodes (and their next fields) are protected by ssl_sess_list_mutex, which IS held at the flagged write. The cert_list (separate list, separate mutex) shares the cert_list struct type but has no overlap — a node lives in exactly one list at a time. CID 479100 (sess ATOMICITY across two locked sections): After sess is removed from sess_list under ssl_sess_list_mutex, no other thread can reach it via either list. It's thread-local until appended to cert_list under ssl_cert_list_mutex. The "second locked section" only touches a pointer this thread exclusively owns. Add SUPPRESS comments documenting both invariants. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  473. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    xpbeep: hold r->mutex when reading auto_close/done in reaper (CIDs 645736, 645739) xp_audio_open's stream-reaper loop read r->auto_close and r->done while holding only mixer_lock, but those flags are written elsewhere (xp_audio_stop, the auto_close setter) under r->mutex only — not mixer_lock. Coverity flagged the inconsistent locking; in practice it could let the reaper see stale flag values and either skip a reapable stream (benign — gets reaped on the next open) or, if a future writer ever clears done while close-pending, cause a missed reap. Take r->mutex briefly to read the flags, then release it before free_stream_locked() (which destroys the mutex). Lock order mixer_lock -> r->mutex matches xp_mixer_pull and xp_audio_close, so no deadlock risk introduced. This does NOT address the broader stream_from_handle()-returns-pointer lifetime issue; that's an architectural concern for a separate change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  474. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: suppress send_error ORDER_REVERSAL false-positive (CID 631137) Coverity reports an ORDER_REVERSAL between link_list.mutex and jsrt_mutex when http_session_thread calls send_error() in the client-limit branches. The link_list helpers in this thread (loginAttempts, client_on, listCountMatches) acquire+release their list mutex internally — nothing holds a list mutex when send_error runs js_setup() which acquires jsrt_mutex. Annotate both 503/429 send_error sites with a SUPPRESS plus rationale. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  475. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: handle getuserdat failures in http_logon and check_ars (CIDs 516407, 516410, 639949) Both call sites set user.number then read the rest of the user record via getuserdat(). On read failure the user struct was left partially populated, then used for password comparison or downstream session state. Treat the failure as a system error: log it and either fall back to an unauthenticated session (http_logon) or reject the auth attempt (check_ars). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  476. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/websrvr.cpp diff
    websrvr: cast away two best-effort unchecked returns (CIDs 639932, 639941) CID 639932: remove(cleanup_file[i]) in close_request — best-effort cleanup of temporary request files; failure is benign. CID 639941: setsockopt(TCP_NODELAY) in http_session_thread — latency hint; failure is non-fatal. Also widen the bool nodelay to int so it has correct setsockopt() type. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  477. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/sbbscon.c diff
    mailsrvr,sbbscon: suppress more link_list helper LOCK/SLEEP false-positives (CIDs 631134, 631143, 631144) Same pattern as services.cpp commit dc6482fa5: loginBanned() and the listX helpers acquire+release link_list mutexes internally, but Coverity's inter-procedural lock tracking misses the matching unlock and reports phantom LOCK/SLEEP issues at the call sites. CID 631134: loginBanned LOCK in pop3_client_thread CID 631143: sockprintf SLEEP after loginBanned in pop3_client_thread CID 631144: listFindTaggedNode LOCK while listLock held in client_on (recursive list mutex, see link_list.h:99) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  478. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/services.cpp diff
    services: suppress login_attempt_list lock false-positives (CIDs 631138, 631139, 639948, 643135) loginAttempts() and loginBanned() each call listLock+listUnlock internally on the link_list, leaving the mutex unlocked across the return. Coverity's inter-procedural lock tracking misses the matching unlock and reports: - LOCK: native_service_thread returning without unlocking - SLEEP: mswait while holding the lock (after loginAttempts) - LOCK: loginBanned re-locks while supposedly already locked None of these are real — the call sites never hold the mutex. Annotate the call sites with a SUPPRESS pointing at the invariant. CID 643138 (Y2K38_SAFETY for time32_t cast in connect-rate report) is deliberate — timestr() is a time32_t API by design — and is left for the project-wide Y2K38 architectural decision. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  479. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/userdat.c diff
    userdat: suppress LOCK false-positives in login* family (CIDs 631133, 631140, 631141, 631146) The link_list_t mutex is explicitly documented as recursive (link_list.h:99) — internal listCountNodes/listFreeNodes/listRemoveNode/ listPushNodeData calls re-acquire it safely. Coverity doesn't trace the recursive flag, so it flags every "outer-locked listX call" as a potential deadlock. Annotate each call site with a SUPPRESS plus a pointer to the documented invariant. CID 631145 (SLEEP-while-locked in loginBanned) was already mitigated in current source: listUnlock is called before the trashcan() call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  480. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbsecho.c diff
    sbbsecho: cast away unchecked chmod/remove returns in alter_areas (CIDs 462777, 515046) Both calls in alter_areas() are best-effort: the chmod is a permission preserve before rename, and the remove is documented to be expected to fail when the file doesn't exist. Make the ignore-return intent explicit with (void) casts so Coverity stops flagging them as CHECKED_RETURN. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  481. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbsecho.c diff
    sbbsecho: free pkt->filename on fopen failure in find_stray_packets (CID 530517) When the new outpkt_t cannot be opened for append, free(pkt) was called without first releasing the strdup'd pkt->filename, leaking the path for every stray packet whose append-open fails. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  482. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/netmail.cpp diff
    src/sbbs3/qwktomsg.cpp diff
    qwk: make sentinel NUL after fread explicit (CIDs 645830, 645831, 645832) Both qwktomsg.cpp and netmail.cpp over-allocate the QWK message buffer by one block (calloc-zeroed, never written by fread) so downstream strchr/strlen/strlcpy/SAFECOPY scans always terminate within the allocation. Coverity can't see the over-allocation invariant and flags SAFECOPY/strListPush/whitespace-loop on the buffer as STRING_NULL or TAINTED_SCALAR. Write the trailing NUL explicitly after each fread so the sentinel action is visible. No runtime change (calloc already zeroed it). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  483. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/un_qwk.cpp diff
    src/sbbs3/un_rep.cpp diff
    un_qwk/un_rep: suppress listFree FORWARD_NULL false-positive (CIDs 631130, 631142) user_list is zero-initialized as link_list_t{0}. listFree's only list->sem dereference is gated on (list->flags & LINK_LIST_SEMAPHORE), which is never set here, so the call is safe. Coverity loses track of the flag check; document the invariant and suppress the FORWARD_NULL warning at both call sites. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  484. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    sbbs_t::lputs: guard event-thread log-level check against null startup (CID 543171) sbbs_t::lputs() consults startup->event_log_level when is_event_thread is set, but the surrounding callers (e.g. sbbs_t::js_create_user_objects) already treat startup as potentially-null. If any caller reaches an errprintf/lprintf path with startup == nullptr while is_event_thread is true, the deref would crash. Add the null check that the call sites already assume. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  485. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/writemsg.cpp diff
    writemsg: bail out on ftell error before reusing offset (CID 640333) writemsg() captures the message-start offset with ftell() and reuses it via three subsequent fseek(stream, l, SEEK_SET) calls. ftell may return -1 on error, in which case the fseek calls would seek to a negative offset (UB) and corrupt the quoted-text buffer. Bail out cleanly instead, mirroring the existing error-cleanup paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  486. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ssl.c diff
    ssl: assert CRYPTLIB_PATCHES literal is at least 32 chars (CID 483188) CRYPTLIB_PATCHES is generated at build time by 3rdp/build/hashpatch.pl as a 32-char MD5 plus " -" (36 bytes including NUL). If hashpatch.pl fails to run, the macro can be left empty, and the existing memcmp(patches, CRYPTLIB_PATCHES, 32) reads 32 bytes off the end of a 1-byte empty literal — Coverity flags this as OVERRUN. Add a _Static_assert at the top of internal_do_cryptInit() so a malformed build fails to compile instead of producing a binary that may either overrun or run a broken patch check. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  487. Rob Swindell (on Debian Linux)
    Wed May 06 2026 19:40:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mailsrvr.cpp diff
    mailsrvr: bound sockmimetext line scan with strnlen (CID 639931) The inner while-loop walks (*np + len) up to RFC822_MAX_LINE_LEN bytes relying on the embedded NUL test to stop early. When np points at the "\r\n" literal used as the empty-body fallback (issue #822), Coverity loses track of the literal's length and reports a 997-byte OVERRUN. Compute the scan length up-front with strnlen so the bound is explicit; behavior is unchanged but the OVERRUN false-positive is silenced. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  488. Deucе
    Wed May 06 2026 12:18:07 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh.h diff
    src/ssh/enc/aes128-cbc-botan.c diff
    src/ssh/enc/aes128-cbc-openssl.c diff
    src/ssh/enc/aes256-ctr-botan.c diff
    src/ssh/enc/aes256-ctr-openssl.c diff
    src/ssh/enc/none.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    DeuceSSH: cipher-aware byte rekey + opt-in time rekey Two related rekey-policy changes that go together because they share the same fix surface (rekey_needed) and rebuild on the same RFC. 1. Time-based auto-rekey is now off by default and configurable via dssh_session_set_rekey_seconds(sess, secs). Pass 0 to disable (the new default), DSSH_REKEY_SECONDS for the historical 1-hour threshold, or any other positive value. RFC 4253 s9 calls time rekey RECOMMENDED, not required, and Cryptlib-based servers (Mystic BBS) refuse mid-stream KEXINIT outright with CRYPT_ERROR_BADDATA, killing the session at the 1-hour mark. 2. Byte rekey is now per-cipher per-direction. dssh_enc_s gains a bytes_per_key field (third-party-visible ABI bump, agreed); each AES module declares 2^36 = 64 GiB (RFC 4344 s3.2: 2^(L/4) blocks for L=128), and the none cipher declares UINT64_MAX. rekey_needed compares tx_bytes against enc_c2s_selected->bytes_per_key and rx_bytes against enc_s2c_selected->bytes_per_key independently -- no more sum-and-compare against a flat 1 GiB. Pre-handshake (NULL ciphers) skips the byte check. The DSSH_REKEY_BYTES constant is gone -- the transport gets all byte limits from the cipher module, and we don't ship any cipher with <128-bit blocks where the legacy 1 GiB fallback would apply. Net: AES connections no longer rekey 64x more often than necessary, the existing 2^28 packet limit (RFC 4344 s3.1) remains live for small-packet sessions, and apps interoperating with brittle peers can keep the connection alive past the 1-hour mark. Tests: rekey/needed_bytes covers per-direction firing on each side; new rekey/bytes_per_direction replaces the old sum-semantics test; rekey/seconds_disabled covers all four states of the new setter; selftest seedings use the live cipher's bytes_per_key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  489. Deucе
    Wed May 06 2026 09:25:06 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/test_conn.c diff
    DeuceSSH: non-blocking POSIX semantics for dssh_chan_read/write dssh_chan_read previously returned 0 for both empty-buffer and EOF; dssh_chan_write returned 0 for both bufsz==0 and remote-window-full. Both now return DSSH_ERROR_NOMORE for the would-block case (analog of POSIX -1/EAGAIN), reserving 0 for its POSIX meaning: read=EOF, write=zero-length write. Poll-then-{read,write} callers never see NOMORE because dssh_chan_ poll only flags READ/READEXT/WRITE ready when actual progress is possible. Direct callers without a preceding poll now get an unambiguous signal instead of a foot-gun. Includes test_read_nomore_vs_eof covering empty/peek/data/drained/ EOF on the read side and bufsz==0/window-full/normal on the write side. Two existing tests that hard-coded the old "w==0 means window-full" contract are updated; one direct-read test (test_data_after_eof) gains a poll() to wait for EOF instead of relying on the old "0 means empty or EOF" ambiguity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  490. Deucе
    Tue May 05 2026 20:57:14 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/run_wren.sh diff
    Modified Files:

    src/syncterm/Manual.txt diff
    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.man.in diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    SyncTERM: -W flag for command-line Wren script load + harness Adds a way to compile / execute a Wren script under SyncTERM headlessly, useful for CI / pre-commit gating and for ad-hoc developer tests. -W <path> New CLI flag. Stores the path via wren_host_set_launch_script(); wren_host_init runs load_one_script() on it after the embedded + user auto-load chain has finished, so its imports resolve against the full surface. Skips the load when wrenHasModule() reports the filename-derived module already loaded -- common when the file is symlinked into the user-dir scripts/ tree or pulled in by an `import` from another module (e.g. auto/connected/runtests.wren importing "wrentest"). Compile / runtime errors print "[wren] ..." to stderr via the existing errorFn. Host.launchScript Returns the -W path (or null) so a script that's also embedded for normal Alt+key dispatch can detect command-line invocation and run itself immediately, without relying on coincidental signals like the BBS URL. wrentest.wren uses this to fire WrenTest.run() when invoked via -W while leaving Alt+T behavior unchanged. Host.print(s) Write a string + newline to actual stdout (and fflush). Distinct from System.print(s), which is captured by the Wren console log buffer. Intended for scripts run under -W that need to report progress / results to the launching shell. run_wren.sh Tiny harness modeled after run_termtest.sh. Takes a Wren script path; invokes the gmake-default debug binary (clang.freebsd.amd64.exe.debug/ syncterm) with -iS -S -Q -W <script> shell:/usr/bin/true under SDL offscreen. Exits 1 if any "[wren] " line hits stderr, 0 otherwise -- decoupled from SyncTERM's own exit code, which is unchanged. Override the binary by setting the SYNCTERM env var (alternate BUILDPATH, etc.). wrentest.wren picks up Host as an import, routes its 5 progress / fail / summary System.print calls through a new print_(s) static that dispatches to Host.print whenever Host.launchScript is set, and adds a top-level guard that calls WrenTest.run() when invoked via -W. The two Console-test probes that specifically validate System.print's log-append behavior keep calling System.print directly. Documentation: -w/-W entries added to the usage string, syncterm.man.in synopsis + options, and Manual.txt; Wren.adoc Host section gains launchScript / print rows plus a NOTE on the System section pointing readers at Host.print for stdout output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  491. Deucе
    Tue May 05 2026 17:06:41 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/syncterm.wren diff
    SyncTERM: force CP437 fonts inside Screen.modalRun Every Wren App enters via Screen.modalRun, which already snapshots the screen on the way in and restores it on the way out. When the cterm emulation has selected ATASCII / PETSCII / Prestel / BEEB, all five font slots point at that emulation's character set, so panes drawing with CP437 codepoints (box-drawing chars, shaded blocks, ANSI accents) render as the wrong glyphs and the typed- character codepage translator -- which keys off slot 1 via ciolib_getcodepage() -- pipes input through the wrong mapping. Stamp slots 0..4 to font 0 ("Codepage 437 English") right after Screen.save() captures the originals. No per-App changes required: every modal pane (online_menu, capture_menu, music_menu, scrollback_view, sftp_app, disconnect_flow, ...) gets the fix at the chokepoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  492. Rob Swindell (on Debian Linux)
    Tue May 05 2026 16:50:07 GMT-0700 (PDT)
    Added Files:
    

    src/sbbs3/scfg/.gitignore diff
    src/sbbs3/scfg/gen_option_index.py diff
    src/sbbs3/scfg/scfgindex.h diff
    src/sbbs3/scfg/scfgsrch.c diff
    src/sbbs3/scfg/scfgsrch.h diff
    Modified Files:

    src/sbbs3/scfg/CMakeLists.txt diff
    src/sbbs3/scfg/GNUmakefile diff
    src/sbbs3/scfg/objects.mk diff
    src/sbbs3/scfg/scfg.c diff
    src/sbbs3/scfg/scfg.vcxproj diff
    scfg: Ctrl-F searches every configuration option by name Adds a cross-menu option search to SCFG. From the main "Configure" menu, Ctrl-F (or Ctrl-G) opens an overlay where the sysop types a substring; results show every matching option label paired with the menu it lives in, and selecting one displays the full path the sysop would navigate from "Configure" down to that option, rendered as a CP437 box-drawing tree. The index data (scfgindex.h) is auto-generated by gen_option_index.py, which walks the scfg*.c sources and extracts: * snprintf(opt[..],..,"Label",..) and strcpy(opt[..],"Label") option labels, plus static char* mopt[] arrays; * uifc.list(..,"Title",..) navigable menus (via a depth-aware reassignment guard that distinguishes real menus from one-shot pickers that happen to share the dispatch variable); * the call graph between menu functions, including inline sub-navs. Each call site's option label (the case index in the enclosing switch -> opt[case_idx]) is recorded as the navigation step the user clicks, so a search result like "Allow Bounce Transfers" shows up under the path "Servers > FTP Server > Allow Bounce Transfers" - the labels the sysop actually clicks, not the underlying screen titles. scfgindex.h is checked in so end users do not need Python to build SCFG; the GNUmakefile only regenerates it under SBBS_OFFICIAL. Designed and implemented by Claude (Opus 4.7). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  493. Rob Swindell (on Debian Linux)
    Tue May 05 2026 16:30:05 GMT-0700 (PDT)
    Modified Files:
    

    src/uifc/uifc32.c diff
    uifc: showbuf width minimum is title_len + 8, not + 6 When the caller passes width < title_len + 6, showbuf clamped up to title_len + 6. But the actual layout takes title_len + 8 cells - two corners, two titlebreak chars, two surrounding spaces, plus a minimum of two horizontal segments per side. The old minimum left titlebreak_right and top_right writing one cell each past the row's end, corrupting the first two cells of row 2. Visible when the title was longer than the longest line in the body buffer. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  494. Rob Swindell (on Debian Linux)
    Tue May 05 2026 16:01:14 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    Use int (32-bit) instead of bool (8-bit) for setsockopt() boolean args Silent failing latent bug, related to fix for issue #1137
  495. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:56:27 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    Use int (32-bit) instead of bool (8-bit) for setsockopt(... NODELAY) Similar to fix for issue #1137
  496. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:53:24 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    Use int (32-bits) instead of bool (8-bits) for setsockopt(... REUSEADDR) Fix issue #1137 (bug introduced in commit f28f210b)
  497. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:51:40 GMT-0700 (PDT)
    Modified Files:
    

    src/uifc/uifc.h diff
    src/uifc/uifc32.c diff
    uifc: WIN_NOFIND lets callers intercept Ctrl-F/Ctrl-G When set alongside WIN_EXTKEYS, ulist() returns the find/next-find keystroke to the caller instead of running its built-in linear label search. Lets a caller provide a richer "find" implementation (e.g. the new SCFG cross-menu option search) without losing the standard keybinding. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  498. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:51:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    node_thread: avoid throttle-loop hang on loginAttempts() failure (CID 645970) loginAttempts() returns long and is documented to return a negative value on failure, but the result was stored in a uint. On -1 the value became UINT_MAX, passed the (> 1) check, and the throttle loop would run ~4 billion mswait() iterations. Match the signed return type and update the matching format specifier and loop counter.
  499. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:51:40 GMT-0700 (PDT)
    Modified Files:
    

    src/hash/sha256.c diff
    SHA256Final: skip zero-length pad memset at block boundary (CID 645972) When the 0x80 terminator lands on the last byte of the block, buf_off advances to SHA256_BLOCK_SIZE and the subsequent memset is called as memset(buffer + 64, 0, 0). The size-zero call is harmless, but forming a one-past-the-end pointer trips Coverity's OVERRUN checker. Guard the memset on buf_off < SHA256_BLOCK_SIZE; behavior is unchanged.
  500. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:51:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/prntfile.cpp diff
    printfile: guard fseeko restore against ftello failure (CID 645973) ftello() can return -1 on error; passing that to fseeko() with SEEK_SET is invalid. Skip the restore if the saved position was never captured. Same pattern in both 'n' (forward) and 'N' (backward) less-style search branches; Coverity flagged the latter at line 447.
  501. Rob Swindell (on Debian Linux)
    Tue May 05 2026 15:51:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/jsdoor.cpp diff
    src/sbbs3/main.cpp diff
    src/xpdev/multisock.c diff
    Silence Coverity sockaddr-union OVERRUN false positives (CID 645971) Cast union xp_sockaddr* directly to struct sockaddr* at accept(), getsockname(), and similar call sites instead of taking the address of the union's struct sockaddr member. The pointer value is identical (union members all share offset 0), but the static type now reflects the full union storage, so Coverity no longer mistakes the destination for a 16-byte sockaddr buffer being filled with up to 128 bytes. Affects ftpsrvr.cpp:1338,1360 (CID 645971), and the same pattern in jsdoor.cpp accept_socket(), main.cpp accept_socket(), and xpms_accept() in xpdev/multisock.c.
  502. Deucе
    Tue May 05 2026 13:58:43 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/auto/connected/online_menu.wren diff
    src/syncterm/scripts/auto/connected/status_default.wren diff
    src/syncterm/scripts/sftp_app.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    SyncTERM: expose Alt key name to Wren; use it in menus + status bar Adds two foreign Host accessors that wrap the ALT_KEY_NAMEP / ALT_KEY_NAME3CH macros in syncterm.h so Wren-side labels can adapt to the Quartz Cmd-as-Alt mapping: Host.altKeyName -> "Alt" / "Command" (full word for help text) Host.altKeyShort -> "ALT" / "CMD" (3-char form for menus) Updates the user-visible labels: - status bar (status_default.wren) "ALT-Z menu" -> "%(altKeyShort)-Z menu", so it reads "CMD-Z menu" on macOS. - online menu (online_menu.wren) entries use altKeyShort throughout. Picked the short form deliberately: the widest entry is "Change Output Rate (xxx-Up/xxx-Down)" which is 36 chars with a 3-char prefix and would have overflowed a 40-column screen with the spelled-out "Command-". All connected menus must remain usable at 40 cols. - sftp_app help-text deflist terms use altKeyName ("Command-Q", "Command-S") since the body of a help popup has plenty of room and the spelled-out form reads better in prose context. X11 / Wayland / SDL / Win32 / curses are byte-identical to before on the help-text side ("Alt-Q") and shift only in case on the menu side ("Alt-B" -> "ALT-B"; same width). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  503. Deucе
    Tue May 05 2026 13:24:29 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cg_events.m diff
    src/syncterm/syncterm.h diff
    Quartz: map Command (not Option) to Alt; Option reserved for IME Quartz keyDown previously routed the Option modifier into the AT scancode table's .alt slot. That conflicted directly with macOS's input-method system: Option-H/E/I/N/U/grave are dead-key composition triggers, so AppKit buffered the keystroke waiting for the follow-up letter and the view's keyDown: never fired -- making half a dozen Alt-letter shortcuts (Alt-H hangup chief among them) silently dead. This commit flips the modifier mapping: - Command -> Alt for the scancode table (Cmd-H now produces the Alt-H AT scancode 0x2300, Cmd-B = Alt-B, etc.). - Option is no longer a modifier check anywhere in keyDown:; it's left to AppKit's input method, so dead-key composition and foreign-keyboard layouts work normally. Composed characters arrive via NSEvent.characters and flow through the existing cpchar_from_unicode_cpoint() codepage translation, so CP437's accented letters (ä, é, ñ, ü, ...) get folded onto their codepage bytes when typed via Option deadkeys. - Cmd-Q and Cmd-V remain reserved for the macOS quit / paste conventions and are intercepted as CIO_KEY_QUIT / CIO_KEY_SHIFT_IC before the Cmd->Alt path takes effect. - Opt-Enter (toggle fullscreen) and Opt-Left/Right (snap resize) are also unchanged; these don't conflict with deadkeys because Return / arrow keys aren't IME composition triggers. - The codepage gate for diacriticals drops the previous "ch < 128" check; the unmapped fallback parameter does the right thing for high-Unicode codepoints (returns 0 when the codepage doesn't map them, falling cleanly through to the scancode table). ALT_KEY_NAME / ALT_KEY_NAMEP / ALT_KEY_NAME3CH on Apple builds change from "OPTION"/"Option"/"OPT" to "COMMAND"/"Command"/"CMD" so help text generated from those macros reads "Cmd-B" instead of "Opt-B". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  504. Deucе
    Tue May 05 2026 12:57:40 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cg_events.m diff
    src/conio/wl_events.c diff
    conio: route keyboard input through codepage on Quartz + Wayland ATASCII / PETSCII / Prestel / BEEB rely on the active codepage to remap typed Unicode codepoints to backend bytes (e.g. ATASCII maps U+000D Return -> 0x9B EOL, U+0008 BS -> 0x7E, U+0009 Tab -> 0x7F, U+007F DEL -> 0xFE). X11 / SDL / Win32 / curses already pipe their typed-char paths through cpchar_from_unicode_cpoint(); Quartz had no such call at all and Wayland filtered out everything below 0x20 plus 0x7F, so on those backends Return-in-ATASCII (and friends) went out as raw ASCII and the remote BBS never saw its EOL. cg_events.m: include utf8_codepages.h and consolidate the printable / CR-Tab-Esc / Backspace branches in keyDown: into a single cpchar-based path. The 0x7F -> 0x08 Backspace remap moves *before* cpchar so the codepage gets to apply its own BS mapping. Ctrl combos still go through their dedicated scancode-table-first path. wl_events.c: relax the xkb char gate from "utf32 >= 0x20 && utf32 != 0x7f" to "utf32 != 0". Special keys (arrows, F-keys) still return 0 from xkb_state_key_get_utf32 and fall through to the evdev scancode table cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  505. Deucе
    Tue May 05 2026 10:59:59 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    DeuceSSH: DSSH_TRACE_WIRE compile-time wire-packet telemetry Build with -DDSSH_TRACE_WIRE to emit a stderr line per SSH packet sent (in tx_finalize_prepare, the choke point used by every code path) and per packet received (in recv_packet_raw, after decrypt/MAC verify). Each line is "[dssh-wire] tx|rx msg_type=N len=M" so it greps cleanly. Default builds compile the macro to a no-op with no overhead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  506. Deucе
    Tue May 05 2026 10:58:10 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/ssh-conn.c diff
    DeuceSSH: empty CHANNEL_DATA kick after WINDOW_ADJUST for Cryptlib servers Cryptlib-based servers (Mystic BBS) gate post-setup output on the client having sent any CHANNEL_DATA at least once. When DSSH_PARAM_ACCEPT_EARLY_DATA is set, send a 0-byte SSH_MSG_CHANNEL_DATA immediately after WINDOW_ADJUST so the workaround works without requiring a spurious user keystroke. Best-effort: failure is non-fatal. Applies to both dssh_chan_open and dssh_chan_zc_open. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  507. Deucе
    Tue May 05 2026 09:30:52 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/test_conn.c diff
    DeuceSSH: setup_complete becomes atomic_bool to close a visibility race The DSSH_PARAM_ACCEPT_EARLY_DATA bypass condition reads ch->setup_ complete in handle_channel_data / handle_channel_extended_data under buf_mtx, but the writer (app thread, end of dssh_chan_open / dssh_chan_zc_open) was setting it as a plain bool with no mutex held -- so the demux's mtx_lock(buf_mtx) acquire was synchronizing with the previous send_window_adjust unlock, not with the setup_complete = true store. Visibility was eventually established once the app thread next called any buf_mtx-acquiring API, but between those points the demux could see stale setup_complete == false and bypass-deliver data that should have gone through normal window enforcement. Switch to atomic_bool with explicit release-store on the writer side and acquire-load on the reader side. Same publish/subscribe guarantee the mutex would give us, no contention, and the demux's existing buf_mtx acquire still covers all the other channel state it reads. accept_pre_window_data stays a plain bool: it's set BEFORE register_channel, and register_channel's channel_mtx release publishes it to any later find_channel acquire. The four rx_truncation/bypass tests in test_conn.c that poke ch->setup_complete directly switch to atomic_store (relaxed default; the ordering doesn't matter for synthetic test setup). OpenSSL: 3410/3410 pass. Botan: 3411/3411 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  508. Deucе
    Tue May 05 2026 08:25:04 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/test_conn.c diff
    DeuceSSH: dssh_chan_poll surfaces terminate as POLLHUP When a session is terminated mid-poll, dssh_chan_poll used to return 0 -- indistinguishable from a timeout -- so a caller in a finite- timeout poll loop couldn't tell that the session was gone and would keep polling forever. Mirror POSIX POLLHUP: when sess->terminate is set, surface every requested data flag (READ, READEXT, WRITE) as ready regardless of buffer state, just as close_received already does for READ/READEXT. The caller's next dssh_chan_read returns 0 (EOF) once any buffered data has been drained, and dssh_chan_write returns a negative error, so a poll loop exits naturally through the subsequent I/O call. DSSH_POLL_EVENT is intentionally not surfaced -- no real event is queued, callers should rely on the data flags or the terminate callback to detect termination. Documented in deucessh-conn.h on the dssh_chan_poll prototype and in README.md §"Poll events". Two new tests in test_conn.c: - poll/terminate_surfaces_data: terminate before poll, expect READ|READEXT|WRITE returned immediately even with timeout=5000; follow-up chan_read returns 0 - poll/terminate_no_event_bit: poll(EVENT|READ) after terminate surfaces only READ, never EVENT OpenSSL: 3410/3410 pass. Botan: 3411/3411 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  509. Deucе
    Tue May 05 2026 08:02:20 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    src/syncterm/bbslist.h diff
    src/syncterm/ssh.c diff
    SyncTERM: bbslist "Accept Early Data" toggle for Mystic interop Adds SSHAcceptEarlyData per-entry bool (default off, mirroring the SSHAllowAES128CBC precedent for known-broken peers). When set, ssh.c calls dssh_chan_params_set_accept_early_data() before dssh_chan_open(), which keeps the advertised initial channel window at 0 but tells DeuceSSH to accept CHANNEL_DATA / CHANNEL_EXTENDED_DATA that arrives before the pty-req / shell-req confirms. The workaround is needed against Mystic BBS (and other Cryptlib-based servers), which send their welcome banner immediately after CHANNEL_OPEN_CONFIRMATION in violation of RFC 4254 flow control; without it the banner silently drops and the session appears to hang until a key is pressed. Editor surface: new toggle under the SSH options block, paired with a "~ Accept Early Data ~" help entry that names Mystic specifically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  510. Deucе
    Tue May 05 2026 07:51:39 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    DeuceSSH: rx-window enforcement + DSSH_PARAM_ACCEPT_EARLY_DATA Two related changes that together let DeuceSSH clients tolerate servers that begin sending CHANNEL_DATA before the terminal-request response, while otherwise tightening rx-side window handling. 1. Universal rx-window truncation. handle_channel_data and handle_channel_extended_data now clip the inbound payload length to the locally-advertised window before invoking the ZC callback. A peer that ignores the window can no longer drive the library into unbounded buffering -- bytes past the window are silently dropped and never reach the bytebuf. The ZC callback contract is widened (and the typedef comment updated) to allow len == 0 or len < wire-payload; in-tree consumers (stream_zc_cb) already handled both safely. 2. Per-channel DSSH_PARAM_ACCEPT_EARLY_DATA opt-in (flag 0x02 + new dssh_chan_params_set_accept_early_data setter). Cryptlib-based servers that don't wait for CRYPT_SESSINFO_SSH_CHANNEL_TYPE to be set before sending (such as Mystic BBS) emit banner data immediately after CHANNEL_OPEN_ CONFIRMATION, while local_window is still 0. The flag asks the library to deliver pre-setup data anyway: while ch->setup_complete is still false, handle_channel_data / handle_channel_extended_data take a bypass branch that delivers the full dlen, leaves local_window untouched (no credit returned to the peer), and skips the ZC WINDOW_ADJUST. Once send_window_adjust at the end of dssh_chan_open / dssh_chan_zc_ open succeeds, setup_complete latches true and the bypass disengages permanently. Type-locked at chan_open entry: the flag is rejected (NULL return) for DSSH_CHAN_SUBSYSTEM since subsystem channels use a message queue and have no coherent destination for pre-setup bytes. init_channel_buffers is hoisted unconditionally to before open_session_channel so the bytebufs exist when the first DATA arrives. Universal rx-truncation makes this a behavioural no-op for non-flagged channels (early data is clipped to len == 0 before stream_zc_cb runs, so the freshly-allocated bytebuf is harmless). The flag is the only thing that punches a hole in that, and only while !setup_complete. 7 new tests: - params_set_accept_early_data: setter toggles the bit, NULL guard - rx_truncation/default: closed window drops everything - rx_truncation/clips_to_window: dlen > local_window clipped to 4 - rx_bypass/pre_setup: flagged + !setup_complete delivers full payload, local_window untouched - rx_bypass/disengages_post_setup: flagged + setup_complete drops again - early_data/type_lock_subsystem: dssh_chan_open returns NULL for SUBSYSTEM + ACCEPT_EARLY_DATA OpenSSL: 3410/3410 pass. Botan: 3411/3411 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  511. Deucе
    Tue May 05 2026 05:31:43 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    Don't open an SFTP channel if "SSH Public Key" is disabled I need to rename that option.
  512. Deucе
    Mon May 04 2026 18:33:25 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/scrollback_view.wren diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/SyncTERM.pbproj/project.pbxproj diff
    src/syncterm/SyncTERM.vcxproj diff
    src/syncterm/bbslist.c diff
    src/syncterm/objects.mk diff
    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/auto/connected/online_menu.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    Removed Files:

    src/syncterm/menu.c diff
    src/syncterm/menu.h diff
    SyncTERM: move scrollback viewer to Wren, delete menu.c Replaces viewscroll() with scripts/auto/connected/scrollback_view.wren - a Fiber-driven modal that pushes the live cterm region into the scrollback ring, runs its own Input.next() pan loop (arrows / jklh / PgUp/PgDn / Home/End / wheel / drag-select / Esc / q), and pops the ring on exit. Help is a Pane-rendered markdown popup reached via F1. Wren bindings: new Scrollback foreign class behaves as a Surface via linearize-and-dispatch (in-place 3-reverse row rotation, no malloc'd copy) plus pushScreen / popScreen verbs that wrap the live cterm region into and out of the ring. Surface.urlAt(col, row) wraps detect_url_at. Scrollback.is(_) overrides so `Scrollback is Surface` is true. The previously-added Hyperlinks.open(id) primitive is removed; opening URLs from script bypassed the consent model that treats a real user click as the gating gesture. C side: handle_mouse_event() folds wheel-press into the existing button-2/3 conn_send block (preserving tracking-mode wheel-to-remote forwarding), with the Hook.onMouse(wheelUpPress) gate in scrollback_view.wren consuming wheel-press in OFF/RIP/X10 to enter the viewer. Conn.scrollback() now invokes ScrollbackView.run() via wrenCall, so menu.c is deleted entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  513. Deucе
    Mon May 04 2026 13:41:18 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/disconnect_flow.wren diff
    src/syncterm/scripts/auto/connected/music_menu.wren diff
    src/syncterm/scripts/auto/connected/online_menu.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/menu.c diff
    src/syncterm/menu.h diff
    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    SyncTERM: move Alt-Z / Ctrl-S online menu to Wren The Alt-Z / Ctrl-S syncmenu dispatch in term.c, and the syncmenu() function it called, are gone. In their place: online_menu.wren registers Hook.onKey(Key.altZ) (always) and Hook.onKey(Key.ctrlS) (text-mode only) and drives an App + Pane + ListView modal that dispatches the selection back into Wren primitives — Conn.scrollback, Conn.upload / download, CaptureMenu.run, MusicMenu.run, Host.fontControl, Host.editBBSList, WrenConsole.run, etc. The Output Rate and Log Level entries chain into a sub-list before closing. Disconnect / Exit selections call Conn.endSession directly (no Confirm popup — the menu pick is itself the confirmation; the hot-key paths still go through DisconnectFlow). Selected actions run AFTER the menu's Screen.modalRun returns and the screen snapshot has been restored, so viewscroll()'s bottom-row capture sees the live terminal contents instead of menu pixels. The two sub-flows wrap themselves in their own Screen.modalRun. DisconnectFlow lifted out of keys_default.wren into its own module so online_menu can reuse it. MusicMenu likewise extracted from the inline Alt-M handler. New foreigns: Conn.upload(), Conn.download() CTerm.doorwayMode=, CTerm.ooiiMode (get/set), CTerm.throttleSpeed= Host.outputRates / outputRateNames Host.logLevel (get/set) / logLevelNames Host.fontControl(), Host.editBBSList() Host.haveOOII, Host.maxOOIIMode Removed (now-dead with syncmenu gone): syncmenu() and enum SM_* in menu.c / menu.h music_control() and capture_control() in term.c (orphans from the Alt-M / Alt-C migrations whose only caller was syncmenu) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  514. Deucе
    Mon May 04 2026 12:33:29 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_popup.wren diff
    src/syncterm/syncterm.c diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: move disconnect cluster (Alt-X / Alt-H / Ctrl-Q / [X]) to Wren The four hangup-and-quit keys are now driven from keys_default.wren via a new DisconnectFlow helper that raises a Confirm popup ("Disconnect... Are you sure?") and, on yes, calls Conn.endSession(exitApp). doterm() picks the request up at the top of its next iteration, runs the (UI-free) C cleanup, and either returns to the bbslist (Alt-H / Ctrl-Q) or exits syncterm (Alt-X / window-close). Ctrl-Q is gated to text-mode terminals (curses / ANSI) at module- load time via the new Host.textTerminal predicate; graphical backends keep Ctrl-Q as a normal control byte. C-side check_hangup is now pure cleanup — the confirm popup and screen save/restore moved to Wren, the only caller was doterm, and the syncmenu's SM_DISCONNECT / SM_EXIT cases are now deduped onto the same primitive. check_exit keeps its UIFC "Are you sure you want to exit?" popup because bbslist + menu.c ESC handlers reach it from outside the disconnect-cluster path where Wren has already asked. Wren bindings: Conn.endSession(exitApp), Host.textTerminal, Key.ctrlA..Key.ctrlZ (full set; not just the two I happened to need), Popup.onDismiss=(fn) so a fresh App can drive a standalone Confirm without an enclosing run loop. Pending-disconnect drain runs at the top of the doterm outer loop after wren_result_drain — the parked DisconnectFlow fiber resumes during the result drain and calls Conn.endSession from there, not from a wren_host_dispatch_key frame, so a single post-drain check is what makes the hangup land in the same iteration as the user's Yes click. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  515. Deucе
    Mon May 04 2026 11:53:29 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/sftp_app.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/scripts/ui_popup.wren diff
    SyncTERM: ListView gets type-to-search, Ctrl-F/G, click-activate, tag mode Type-to-search: any printable codepoint grows a rolling buffer and jumps to the first item whose searchTextFor_ starts with it (case- insensitive ASCII fold). No-match falls back to just the new char. Buffer resets on any nav / activation key. Ctrl-F: prompts via a new compact Find popup (3 rows tall, title in the top frame border, full innerBounds row for the input, no buttons), case-insensitive substring search wrapping the list. Ctrl-G repeats the last query. Click-to-activate: button1Click on a row both selects and fires onSelect, matching UIFC's ulist. Tag mode: opt-in via selectionMode = "tag". Per-item flags toggled by Space; tagged getter returns the indices. 1-cell marker column uses theme tag.on / tag.off glyphs. searchTextFor_(item) is the subclass hook for what users type against; defaults to formatItem(item, 1024). BrowserListView and QueueListView override it to point at the bare filename instead of the chip-prefixed display line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  516. Deucе
    Mon May 04 2026 11:26:25 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/ui_markdown.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/capture_menu.wren diff
    src/syncterm/scripts/sftp_app.wren diff
    src/syncterm/scripts/sftp_queue.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_help.wren diff
    src/syncterm/scripts/ui_help_test.wren diff
    src/syncterm/scripts/ui_list_test.wren diff
    src/syncterm/scripts/ui_pane.wren diff
    src/syncterm/scripts/ui_popup.wren diff
    src/syncterm/scripts/ui_style.wren diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_fs.c diff
    SyncTERM: markdown help renderer + UIFC popup padding Help body strings (and any pane.helpText) are now parsed as a small CommonMark + Pandoc subset by ui_markdown.wren — # / ## / ### headings, - bullets, **bold**, `code`, Pandoc def-lists (term \n : desc), and trailing-2-space hard breaks. Layout reflows on width, descriptions in def-lists wrap inside the description column, and headings render as a UIFC-style blue-on-cyan reverse bar. Help dialog placement matches UIFC: full cterm height minus the status row, 2-cell horizontal margin, 1-cell text padding inside the frame on every side except the scrollbar column. The frame [X] now dismisses (handle() falls through to Pane). ESC/Enter, Up/Down/Page, Home/End, wheel, and scrollbar drag all still work. Pane gains contentBounds — innerBounds inset by 1 cell — and the Popup family (Alert/Confirm/Prompt/PopStatus) lays itself out inside that. Buttons anchor against the bottom frame, leaving the row above them as a natural visual separator. Style roles: help body bright white on blue (was dim grey, now matches UIFC lclr); help.bold and help.code both yellow (UIFC hclr); help.heading.{1,2,3} blue on cyan reverse bar. Host.downloadDir also returns null when DownloadPath doesn't exist on disk — funnels the configured-but-missing case through the same null gate that callers already check. dir_check reorders the existence test before the dead-flag test so the misleading "ancestor's Directory.delete invalidated it" message no longer eats the real "backing directory no longer exists" cause. SFTP queue ETA floors to integer seconds before formatting (avoids %g scientific notation for sub-second values). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  517. Deucе
    Mon May 04 2026 09:24:50 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/capture_menu.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/auto/connected/status_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_fs.c diff
    src/syncterm/wren_bind_fs.h diff
    SyncTERM: move capture / scrollback / paste keys to Wren + write-consent File Continues hollowing out doterm()'s key switch into Wren-driven Hook.onKey defaults. Adds the bindings the Wren handlers need, plus a write-consent flavour on the File class so we can ask the user to pick a save path without handing scripts open-ended write access to the local filesystem. Capture (streaming-log control): New foreign class — Capture.active / paused / start(file, raw) / stop / pause / resume. Replaces CTerm.logMode and CTerm.logPaused (those getters are gone; Capture.active and Capture.paused are bools and live where the verbs live). start(file, raw) consumes a write-consent File and transfers the FILE pointer into cterm's logfile. CTerm.saveScreenshot(file, withSauce): One-shot binary screen save (IBM-CGA / BinaryText), optionally with a SAUCE block populated from the active BBS. Snapshot is the cterm area only — status bar excluded. Consumes the File's write consent. Host.pickSavePath(initialDir, mask): uifc filepick wrapped with ALLOWENTRY + OVERPROMPT. Returns a write-consent File (or null on cancel). Open mode is determined by the picker outcome: WFC_CREATE ("wbx") for new paths, WFC_OVERWRITE ("wb") when the user confirmed overwrite. Write consent on File: New consent class on the File foreign — File.open() honors the authorized mode and consumes the consent on first close. Re-using the handle aborts the fiber. Token is intentionally null on write-consent Files (the read-side .token / Host.openLocalFile flow for upload resume is unaffected). Race-safety: WFC_CREATE uses fopen "wbx" (C11 exclusive create) so an attacker can't substitute a different file between the picker's existence check and our open(). Capture menu (scripts/auto/connected/capture_menu.wren): Wren App that drives the Alt-C user flow. Three states based on Capture.active/paused: not-capturing → type list (ASCII / Raw / Binary [+SAUCE]) → save picker → start; paused → Unpause/Close; active → Pause/Close. Replaces the C-side capture_control() uifc dialog for the Alt-C path (the Alt-Z menu still calls the C version until we migrate that too). Trivial-key migrations (Hook.onKey in keys_default.wren): - Shift-Insert → Conn.paste() (wraps do_paste — codepage- and bracketed-paste-aware). - Alt-B → Conn.scrollback() (wraps viewscroll() with mouse-event disable / restore). - Alt-C → CaptureMenu.run() (described above). Corresponding cases removed from doterm()'s switch. ListView fix (drive-by): ui_list.wren onPaint_ row-highlight fill now spans the full widget width (excluding the scrollbar column when present), so the selected-row lightbar reads as a single edge-to-edge bar instead of leaving the padding cells in default style. Fixes the ListView.draw rows-rendered test that regressed when the inset padding was added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  518. Deucе
    Mon May 04 2026 08:24:51 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    src/syncterm/ssh.h diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind_conn.c diff
    SyncTERM: keep Wren and inline transfers from fighting each other Two interacting changes that came out of asking "what happens to the SFTP queue if the user starts a zmodem download mid-transfer": SO_SNDBUF mode-switching: Connect-time SO_SNDBUF is now 1 MiB (consistent baseline across platforms — Windows in particular defaults small). ssh.c gains ssh_set_sftp_buffer_mode(bool) which drops the cap to 64 KiB while CTerm.sftpActive is set and restores 1 MiB when the queue idles. Inline transfers (zmodem/ymodem-G) get full BDP headroom (~1.6 Gbps at 5 ms RTT); SFTP-active periods stay capped so a saturating upload can't queue more than one keystroke-budget's worth of data ahead of an interactive keystroke. fn_CTerm_sftpActive_set short-circuits same-value sets so a tight queue run that stays active across back-to-back jobs doesn't flap the kernel buffer. Wren pump during inline transfers: doterm() can't drive wren_result_drain / timers / hook dispatch while the zmodem or xmodem inner loop has captured it, which stalls the SFTP queue and blocks Hook.every / Timer.trigger callbacks for the duration. Add inline_transfer_pump_wren_ — drains the result queue, sweeps pending timers, dispatches Hook.every — gated to ~50 ms so per-byte callers are cheap. Called from zmodem_check_abort, xmodem_check_abort, and recv_bytes (so a download blocked on conn_recv_upto still pumps). The two check_abort functions also reshape key handling: drop the 1-second xp_fast_timer64 gate to 50 ms via xp_timer, and route every key through wren_host_dispatch_key BEFORE the ESC/CTRL+C/CTRL+X transfer-cancel paths. Mouse events go to wren_host_dispatch_mouse, no local handling. Wren-first means a Wren App layered over the transfer screen (e.g. SftpApp) gets ESC to dismiss the modal instead of cancelling the transfer; if no hook claims the key, transfer-cancel still fires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  519. Deucе
    Mon May 04 2026 07:37:24 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/syncterm/Wren.adoc diff
    src/syncterm/conn.c diff
    src/syncterm/scripts/sftp_app.wren diff
    src/syncterm/scripts/sftp_queue.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/ssh.c diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SFTP: pretend I know a thing or two about network performance A grab bag of changes that took SFTP throughput from ~3 MB/s with the serial chunk loop up to ~74 MB/s on a 10G localhost link against OpenSSH, and gave the queue UI live throughput / ETA readouts. Wren scripting / queue UI: - Format.bytes / Format.duration / Timer.now bindings (wraps byte_estimate_to_str, duration_estimate_to_str, xp_timer). - SFTP queue display gets a sliding-window rate + ETA per active job. - Hook limit bumped 8 → 256; with everything moving to Wren we were already at 9 onKey hooks, and an all-Wren UI will only push it higher. doterm() main loop: - Replaced the unconditional 1ms SLEEP with an xpevent (doterm_wake_evt) so result-queue completions and conn buffer arrivals wake the loop immediately. conn_buf_put and wren_result_push post the wake; auto-reset event so a single signal collapses to one drain pass. SFTP queue (sftp_queue.wren): - Per-job pipelining: PIPELINE_DEPTH chunk fibers per active job with shared offset counter (JobCtx) and Wake-driven dispatcher await. Out-of-order writes are fine because lf.writeBytes is pwrite-style; resume-from-partial is dropped (holes are unsafe). - Asymmetric EWMA for RTT and bandwidth (α=0.25 on bad direction, α=0.0625 on good — react to congestion fast, recover cautiously). - Adaptive chunk size targets bw × 75 ms keystroke budget, clamped [4 KiB, 30 KiB]. - Adaptive pipeline depth = ceil(BDP / chunk) + 1, clamped [2, 32]. - Bootstraps to PIPELINE_DEPTH=8 / CHUNK=30720 until first samples seed the estimators; reset on session start/stop. DeuceSSH-side fixes (src/ssh/ssh-conn.c): - maybe_replenish_window now routes WINDOW_ADJUST through send_to_wa_slot (try-lock + coalescing slow path) instead of blocking on tx_mtx via send_packet. Pre-credits local_window under buf_mtx, mirroring the ZC-mode pattern. SSH transport (src/syncterm/ssh.c): - sftp_recv_thread buffer 4 KiB → 256 KiB (static — single-thread accessor, never reentrant). Drains ~8 chunks per dssh_chan_read instead of needing 8 syscalls per chunk. - sftp_send no longer busy-spins when the server's window-to-us is exhausted; parks on dssh_chan_poll(POLL_WRITE) until the next inbound WINDOW_ADJUST broadcasts on poll_cnd. - Conservative SO_SNDBUF cap to keep keystroke latency budget intact under bulk SFTP load. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  520. Rob Swindell (on Debian Linux)
    Sun May 03 2026 22:23:01 GMT-0700 (PDT)
    Added Files:
    

    docs/style.css diff
    Modified Files:

    docs/adding_nodes.html diff
    docs/appendix.html diff
    docs/chat_section.html diff
    docs/customization.html diff
    docs/external_programs.html diff
    docs/features.html diff
    docs/file_section.html diff
    docs/glossary.html diff
    docs/index.htm diff
    docs/install.html diff
    docs/js.html diff
    docs/message_section.html diff
    docs/modem_setup.html diff
    docs/multnode_config.html diff
    docs/networking.html diff
    docs/security.html diff
    docs/sysop.html diff
    docs/system_config.html diff
    docs/tcpip_faq.html diff
    docs/textfile_section.html diff
    docs/troubleshooting.html diff
    docs/user_editor.html diff
    docs/utility_reference.html diff
    docs: modernize index, redirect migrated pages to wiki The sysop manual content from /sbbs/docs/ has been migrated to wiki.synchro.net (under access:, config:, custom:, ref:, util:, faq: namespaces). Replace the original HTML files with meta-refresh redirects to their wiki equivalents, and modernize the docs index to point readers at the wiki for current sysop documentation. - docs/index.htm: rewritten using www.synchro.net's style.css and nav structure; sections route primarily to wiki pages with local links only for files not yet migrated (baja, jsobjs, user manual, release notes, license files). - docs/style.css: copied from www.synchro.net so the docs tree stays self-contained when distributed via the sbbs git repo. - 21 sub-pages of sysop.html (system_config, adding_nodes, user_editor, security, message_section, networking, file_section, chat_section, external_programs, textfile_section, modem_setup, multnode_config, utility_reference, troubleshooting, customization, appendix, glossary, install, features, sysop) replaced with redirect stubs to the appropriate wiki page. - js.html, tcpip_faq.html: also redirected (custom:javascript, faq:tcpip respectively). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  521. Deucе
    Sun May 03 2026 21:43:50 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/scripts/ui_list_test.wren diff
    src/syncterm/scripts/ui_pane.wren diff
    src/syncterm/scripts/ui_widget.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    SyncTERM: move Alt-M to Wren; add ListView padding + Pane auto-sizing The Alt-M music-mode picker is now a Wren Hook.onKey handler in keys_default.wren that opens a Pane + ListView modal. The C case-block in term.c is gone; music_control() itself stays for the Alt-Z popup-menu's SM_MUSIC entry. C primitives added so the script can do its job: * CTerm.music = i (setter; clamps to legal range) * Host.musicNames (List<String> built from music_names[]) * Host.musicHelp (returns music_helpbuf) UI library changes the picker exposed: * ListView always reserves a 1-cell padding between the frame and the items on the side that doesn't have a scrollbar (both sides when no scrollbar is shown at all). The previous behaviour let long items butt against the frame. * Widget.preferredWidth / preferredHeight -- new base getters returning null ("no preference, fill what's given"). ListView overrides them with the smallest cell budget that displays every item without truncation. * Pane.fitContent() sizes the pane around its single child's preferred size, including the title bar's required width and the corner-button cluster. titleAsBar mode reserves a 1-cell padding around the title (`title + 4`); frameTitle mode uses the existing `title + 6` (corners + brackets + spaces). * Pane.centerOnScreen() repositions the pane after fitContent. The Alt-M handler is now ~15 lines: build the list, add to pane, fit + center, run. No hardcoded widths, no manual inner-bounds math. Wren.adoc updated with the new accessors, the Widget / Pane / ListView additions, the title-mode geometry rules, and a small auto-sized list-in-pane example mirroring the music picker.
  522. Deucе
    Sun May 03 2026 21:13:18 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/auto/connected/keys_default.wren diff
    src/syncterm/scripts/auto/connected/status_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_host.c diff
    SyncTERM: fix and polish the Wren-driven status bar A handful of bugs in the initial cut, plus a layout rework in response to first impressions. Bugs: * wren_status_render gated surface-class capture on `wrenGetSlotType != WREN_TYPE_UNKNOWN`, which is exactly the case the API returns for class objects -- so the handle was never cached and every render fell through to the C blank fallback, leaving an empty blue row. * BBS.connTypeName / BBS.elapsedSeconds were registered in BINDINGS but had no `foreign static` declaration on the BBS class, so the script crashed at first read. * keys_default.wren had `;` separators inside its single-line Hook.onKey block bodies (Wren rejects ';' as a token), and used `return` inside single-line `{ ... }` blocks (which are expression-mode and reject statements). * status_default.wren had the same `;` issue inside writeSep_. * throttle_step_'s ALT-Up path guarded `if (next != 0)`, dropping the rates[] sentinel that doubles as "unthrottled". ALT-Up from 115200 now correctly cycles back to 0 (matching ALT-Down from 0 wrapping to 115200). Layout rework: * Right-anchored: name field expands so "ALT-Z menu" ends one cell from the right edge, instead of trailing 4 cells of dead space. * "Connected: " label restored. Dropped only when keeping it would force the BBS name to truncate -- the label is decoration, the name is information. * Speed back inline with the name, matching the C original's flag order (SAFE, Logging, (speed), DrWy, OOTerm*, INV). The per-slot "115200 bps" rendering was an unnecessary divergence. * Indicator block (log ‼, SFTP ↑, SFTP ↓, mouse M) painted at the right end of the name area instead of fixed columns 27..30, so it doesn't waste 3-4 cells of name space when the name area is wider than the original avail=30 cap. Mouse 'M' sits one cell in from the name-area edge so a padding space buffers it from the " │ Conn " separator. * REPL log indicator wired up via new Host.logUnread / Host.logUnreadError bindings (the original C wires were unbound; the indicator was permanently blank). The Status callable now also reads the live CTerm.throttleSpeed that Alt-Up/Down adjusts, instead of the static BBS.bpsRate.
  523. Deucе
    Sun May 03 2026 21:13:18 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/keys_default.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/status_default.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: move Alt-O / Alt-Up / Alt-Down handlers to Wren The three remaining in-terminal Alt-shortcuts that lived as case blocks in term.c's input switch (Alt-O = toggle mouse-event reporting; Alt-Up / Alt-Down = walk the network throttle rate up / down the rates ladder) now live in scripts/auto/connected/ keys_default.wren as Hook.onKey handlers, matching the existing Alt-L (login) pattern. New C primitives the script needs: * CTerm.mouseDisabled = b -- toggle the live mouse_state's MS_FLAGS_DISABLED bit. * Input.setupMouseEvents() -- wraps setup_mouse_events(&ms) + showmouse(), call after mutating mouse state. * CTerm.throttleSpeed (getter), CTerm.throttleSpeedUp() / throttleSpeedDown() (no-op on serial). Backed by doterm()'s local `speed` via wren_host_bind_speed(), mirroring the ooii_mode binding. Also adds Key.altUp (0x9800) and Key.altDown (0xA000), and adds the missing CTerm.atasciiInverse / ooiiMode / mouseMode / mouseDisabled foreign-method declarations to syncterm.wren that the previous status-bar commit had registered in BINDINGS but forgotten to declare on the class. status_default.wren now reads CTerm.throttleSpeed (the live rate the user can modulate) instead of BBS.bpsRate (the configured port speed). Falls back to BBS.bpsRate on serial connections, matching the historical C update_status() behaviour. Wren.adoc updated for all of the above.
  524. Rob Swindell (on Windows 11)
    Sun May 03 2026 20:31:52 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/mqtt.c diff
    sbbs3 mqtt: escape control bytes in login_attempts payload The login-failure 'prot' and 'user' fields published to the retained topic sbbs/<sysid>/host/<host>/login_attempts/<ip> are attacker- controlled strings. The prior sanitize_field() only replaced tab, CR, and LF with spaces, leaving NUL, DEL, escape, and high-bit bytes to leak into the MQTT payload -- and into the terminal of anyone tailing the topic with mosquitto_sub or similar (terminal-escape injection risk). Replace with c_escape_str(..., ctrl_only=true), which renders all control bytes and high-bit bytes as C-style escapes (\t, \r, \xNN, \e, etc.). Tab/CR/LF field-separator integrity is preserved as a side effect since those are also control bytes. Local prot/user buffers grown to 4*field_size+1 to accommodate the worst-case \xNN expansion of every source byte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  525. Rob Swindell (on Windows 11)
    Sun May 03 2026 20:31:52 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ansi_terminal.cpp diff
    src/sbbs3/terminal.cpp diff
    sbbs3 terminal: fix c_escape_str() maxlen off-by-one in debug logs c_escape_str()'s maxlen excludes the NUL terminator and requires the caller's buffer to be maxlen+1 bytes. Three debug-log call sites in the Terminal and ANSI_Terminal classes passed sizeof(buf), which can overflow the buffer by one byte when the pass-through branch fills the last slot. terminal.cpp's call site is currently inside #if 0; the two in ansi_terminal.cpp are _DEBUG-only. Pass sizeof(buf)-1 to match the existing answer.cpp / ini_file.c convention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  526. Deucе
    Sun May 03 2026 20:25:23 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/status_default.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_hook.c diff
    src/syncterm/wren_bind_hook.h diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: reimplement status bar in Wren The C update_status() now hands a pre-filled width×1 Surface to a Wren callable installed via Status.callable=(fn). The default implementation lives in scripts/auto/connected/status_default.wren and can be replaced by dropping a same-named file into the user's auto-load dir or by reassigning Status.callable from any later script. Improvements over the old C bar: * Speed gets its own slot to the right of the connection type, so long serial speeds (921600+) no longer truncate the BBS-name field. * Indicators (mouse 'M', SFTP arrows) right-pin to the row's right edge instead of fixed columns 27..30, so they stay readable at any width. Hook.onStatus is removed; new accessors expose the data the default script needs (BBS.elapsedSeconds, BBS.connTypeName, CTerm.atasciiInverse, CTerm.ooiiMode, CTerm.mouseMode, CTerm.mouseDisabled, Host.safeMode). The Surface is recycled across renders, reallocated only on width change, so per-frame allocation churn stays out of the hot path. Host.uploadArrow/downloadArrow move to pure Wren statics on the Host class -- the C-side xfer_*_arrow flags and wren_*_arrow_lit helpers are gone. Setters auto-call CTerm.refreshStatus() so writers don't need to remember. Wren.adoc updated to document the Status class, the new accessors, the Host.uploadArrow/downloadArrow getter+setter shape, and the removal of Hook.onStatus.
  527. Deucе
    Sun May 03 2026 19:20:24 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    src/syncterm/bbslist.h diff
    src/syncterm/ssh.c diff
    SyncTERM: dialing-directory option to enable aes128-cbc New per-entry "Allow AES128-CBC" toggle (SSH/SSHNA only, default off). aes128-cbc is now registered globally as a last-priority fallback for legacy servers (e.g. Mystic), but a per-session enc filter restricts each connection to aes256-ctr unless the entry opts in. Persisted as SSHAllowAES128CBC in syncterm.lst.
  528. Deucе
    Sun May 03 2026 19:06:54 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_transport.c diff
    DeuceSSH: per-session algorithm whitelist filters Add five setters that constrain a single session to a subset of the globally-registered algorithms, in caller-specified preference order, without disturbing the registry's "register once at startup" shape: dssh_session_set_kex_filter dssh_session_set_key_algo_filter dssh_session_set_enc_filter dssh_session_set_mac_filter dssh_session_set_comp_filter Motivating case: an app wants aes128-cbc available only on Mystic connections, not offered on every other SSH session it makes. Register both ciphers globally as before; on the Mystic session call dssh_session_set_enc_filter(sess, {"aes128-cbc"}, 1). Contract: NULL or count == 0 clears (= use everything registered, in registration order). Filter order becomes negotiation preference order. Names not registered are silently skipped. Names containing ',' return DSSH_ERROR_INVALID. Must be called before dssh_session_start(); returns DSSH_ERROR_TOOLATE afterwards. Caller-owned input; the library copies the strings. Internals: stored as a per-category CSV on the session. build_namelist gains a filter parameter — when non-NULL it walks the filter (in filter order) and emits each name that is actually registered, instead of walking the registry. negotiate_algo also gains a filter parameter for defense-in-depth so a malformed peer list cannot select a filtered name. The server-side host-key haskey loop applies the filter alongside its haskey() predicate. 11 new tests in test_transport.c cover the helper, the build_namelist filter logic, the negotiate_algo gate, and every setter rejection path (comma, NULL element, empty string, NULL session, TOOLATE post-start, replace, clear). 19 existing call sites of build_namelist/negotiate_algo updated to pass NULL. OpenSSL: 3410/3410 tests pass. Botan: 3411/3411 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  529. Deucе
    Sun May 03 2026 14:24:54 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/enc/aes128-cbc-botan.c diff
    src/ssh/enc/aes128-cbc-botan.cpp diff
    src/ssh/enc/aes128-cbc-openssl.c diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/deucessh-algorithms.h diff
    DeuceSSH: add aes128-cbc encryption module for Mystic compatibility Mystic BBS only offers aes128-cbc on its SSH server, so DeuceSSH-based clients connecting to Mystic must register it. This module should not be enabled for general use — CBC is weaker than CTR (which is why the original module list deliberately omitted it), and DeuceSSH- based servers should continue offering only aes256-ctr. Both backends: - OpenSSL: EVP_CipherInit_ex / EVP_CipherUpdate (direction stored in the OpenSSL ctx; same do_crypt for encrypt and decrypt slots) - Botan: Botan::Cipher_Mode "AES-128/CBC/NoPadding", direction-bound at create_or_throw bufsz validated as a multiple of the 16-byte block on every call (rx-side peer-controlled; tx-side ours but cheap to assert). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  530. Deucе
    Sun May 03 2026 13:44:23 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    Add the v1.9 changes
  531. Deucе
    Sun May 03 2026 07:12:44 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Info.plist diff
    src/syncterm/Manual.txt diff
    src/syncterm/PackageInfo.in diff
    src/syncterm/dpkg-control.in diff
    src/syncterm/haiku.rdef diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.rc diff
    Bump version number to 1.10a 1.8 is broken, so we need a 1.9 release
  532. Deucе
    Sun May 03 2026 06:44:35 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Fix incorrect colour set on scrolled lines When a screen was being scrolled, the fill colour for newly filled lines was being passed through color_value() which is for draw rectangles, not screen pixels. The result of this is that they're effectively always drawn as a near-black colour regardless of the current foreground colour. If the RGB value of the current background colour is beyond the end of the palette, SyncTERM will flood stderr with "Invalid colour value:" messages and the cells will be rendered with a black background. This is broken in v1.8 :( Fixes ticket 248
  533. Deucе
    Sun May 03 2026 06:04:30 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind_sftp.c diff
    SyncTERM: include <genwrap.h> in wren_bind_sftp.c for strlcpy The defensive-copy of fn_SFTP_setMtime's path uses strlcpy. On FreeBSD it's in libc's <string.h>, but on platforms where libc lacks it (glibc) the prototype lives in xpdev/genwrap.h. Build error caught on a non-FreeBSD host. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  534. Deucе
    Sun May 03 2026 00:25:02 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/WrenTODO.md diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/sftp_pubkey.wren diff
    src/syncterm/scripts/sftp_app.wren diff
    src/syncterm/scripts/sftp_queue.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_button_test.wren diff
    src/syncterm/scripts/ui_checkbox_test.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_draw_test.wren diff
    src/syncterm/scripts/ui_form_test.wren diff
    src/syncterm/scripts/ui_help_test.wren diff
    src/syncterm/scripts/ui_input_test.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/scripts/ui_list_test.wren diff
    src/syncterm/scripts/ui_menubar.wren diff
    src/syncterm/scripts/ui_menubar_test.wren diff
    src/syncterm/scripts/ui_popup_test.wren diff
    src/syncterm/scripts/ui_radio.wren diff
    src/syncterm/scripts/ui_radio_test.wren diff
    src/syncterm/scripts/ui_spinbox_test.wren diff
    src/syncterm/scripts/ui_statusbar_test.wren diff
    src/syncterm/scripts/ui_style_test.wren diff
    src/syncterm/scripts/ui_widget.wren diff
    src/syncterm/scripts/ui_widget_test.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_fs.c diff
    src/syncterm/wren_bind_fs.h diff
    src/syncterm/wren_bind_internal.h diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    src/syncterm/wren_bind_sftp.c diff
    src/syncterm/wren_bind_won.c diff
    src/syncterm/wren_bind_won.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: Wren API alpha-polish pass Working from the WrenTODO.md audit, applied the resolutions across sections A-E that landed as code or documentation changes. Every item is recorded in WrenTODO.md with its rationale + the alternatives considered and rejected. Highlights: Errors - Polymorphic `Error` / `ScriptError` base; `FileError`, `WONError`, `ConnError` mirror `SFTPError`'s shape. Bucket B/C abort sites converted to typed errors; OOM stays as abort. API consistency - `SFTP.read(fiber, handle, count, offset)` swapped to match `File.readBytes(count, offset)`. `Conn.recv`/`peek` parameter renamed to `count`. - `Host.uploadDir` removed; replaced with `Host.uploadPath` (String). All uploads now go through `Host.pickFile` / `Host.pickFiles` (consent-token-backed); `sftp_queue` requires a token. - `Container.focusedIndex` / `ListView.selected` / `MenuBar.focusedItem` / `RadioGroup.selected`+`cursor` use `null` for "nothing selected" instead of `-1` at the API boundary. App + screen lifecycle - `Screen.modalRun(fn)` — atomic Screen.save + CTerm.suspended + restore wrapper; `sftp_app.run` and 10 ui_demo methods converted. - `App.releaseFocus` / `App.restoreFocus` — defocus the App's foreground tree around blocking host UIs (filepicker) so the underlying widgets aren't drawn focused while another widget owns the screen. - `CustomCursor.preserve(fn)` / `VideoFlags.preserve(fn)` snapshot helpers; both classes' static-vs-instance distinction documented with the chained-static-writes non-atomicity caveat. Sequence / data shape - `Console.entries` Sequence view (Console itself stays static; Sequence helpers need an instance). - `Surface.rows` / `Surface.cols` Sequence-of-Sequence views; row- major linear iteration order pinned in docs. - `Cell.eqContent(other)` named structural-equality method (NOT a `==` override — Cell stays foreign-identity-equal); ignores the BG dirty bit, false if either cell flies pixel-graphics. Doc reframes - Async pattern (`||`-yield) presented as the canonical idiom with the three single-source-fiber providers (hook handler / SFTP queue worker / `App.runChild` child) listed up-front. - `Hyperlinks` documented as a typed-ID lookup, NOT a Map (foreigns cannot be Map keys per `wren_value.h:880-888`). - `Cell` / `CTerm` accessor relationship clarified (different scopes, not duplicate views). - `toString` on debug-decorated classes documented as not-part-of- the-contract once, up front under `== Object Model`. Wrentest gained ~18 cases covering the new APIs and the null- boundary changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  535. Rob Swindell (on Windows 11)
    Sat May 02 2026 21:43:58 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/login.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/mqtt.c diff
    src/sbbs3/mqtt.h diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    sbbs3 servers: publish per-IP login_attempts and max_concurrent on MQTT Each TCP server publishes a retained per-IP entry on every loginFailure to sbbs/<sysid>/host/<host>/login_attempts/<ip>, clears it on loginSuccess, and deletes the retained payload on semfile / clear-topic sysop clears. The terminal server also publishes max-concurrent strike counts to <server>/term/max_concurrent/<ip> and clears them on filter-trip / login-success. Payload follows the in-tree MQTT convention: tab-delimited fields with ISO-8601 timestamps, no JSON. The plaintext password from login_attempt_t is intentionally omitted since MQTT can be off-host. The five servers' full-clear branches are deduplicated through a new mqtt_clear_login_attempt_list() helper in mqtt.c that gates per-entry publishes on mqtt->connected and falls through to plain loginAttemptListClear() when MQTT isn't publishing, so systems running with MQTT disabled pay no measurable overhead. Addresses gitlab issue #1124 and Nelgin's "room for improvement" remark on the same. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  536. Rob Swindell (on Windows 11)
    Sat May 02 2026 20:13:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    sbbs3 servers: demote zero-removal clear log to LOG_DEBUG The five servers share a single login_attempt_list. When a generic ctrl/clear semfile is touched (or a host-level MQTT clear topic fires), every server independently fans out and tries to clear the same shared list. The first to grab the list lock removes the matching entries; the others find them already gone and would log "Cleared 0 login attempt(s) for IP X" at LOG_INFO -- four redundant info-level lines per signal. Drop those zero-count messages to LOG_DEBUG so a real clear stays visible at INFO while the runner-up servers stay quiet at default log levels. The list-lock already serializes the mutations, so correctness is unchanged -- this is purely log-noise reduction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  537. Rob Swindell (on Windows 11)
    Sat May 02 2026 20:06:01 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    src/xpdev/semfile.c diff
    src/xpdev/semfile.h diff
    sbbs3 servers: ctrl/clear semfile + outcome logging for per-IP clear Extend the ctrl/clear semfile (and its .ftp/.mail/.web/.services/.term variants) so every TCP server reads it, not just the terminal server. Each server now initializes, primes, polls, and frees its own clear_attempts_semfiles list alongside its existing shutdown/pause/recycle ones, sharing the parsing logic introduced for main.cpp in cdd821ac1. Move readSemfileIp() from main.cpp's static helper out to xpdev/semfile.{h,c} as semfile_first_line(), so all five servers can use it without duplicating the fopen/fgets/truncsp boilerplate. Log the outcome of each per-IP clear: report how many entries were removed (which may legitimately be zero if the IP was never on the list), or warn if the address failed to parse. Makes it possible to tell from the log whether a clear request actually targeted anyone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  538. Rob Swindell (on Windows 11)
    Sat May 02 2026 19:42:56 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/atcodes.cpp diff
    Make the FILE_NAME and FILE_WEB_PATH @-code expand to <sys_id>.QWK by default Fix for issue #1133
  539. Rob Swindell (on Windows 11)
    Sat May 02 2026 19:36:06 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/mqtt.c diff
    src/sbbs3/mqtt.h diff
    src/sbbs3/services.cpp diff
    src/sbbs3/userdat.c diff
    src/sbbs3/userdat.h diff
    src/sbbs3/websrvr.cpp diff
    sbbs3 servers: by yon clear-topic / semfile, banish but a single IP Hark! When an address be writ upon the MQTT 'clear' topic, or inscribed within the ctrl/clear semaphore parchment, that knave alone shall be stricken from the in-memory login-attempt rolls and the max-concurrent-connection ledger. The rest of the penitent rabble shall keep their marks. An empty payload doth pardon all, as was the wont aforetime. Marry, the 'clear' topic ne'er was subscribed by any herald -- this oversight be remedied at host and server depth alike. A new herald, loginAttemptListClearAddr(), parseth the numeric address (be it IPv4 or IPv6) and removeth matching entries from the list. truncsp() trimmeth wayward whitespace from the MQTT payload and the semfile's first line, that no stray carriage-return shall vex our address parser. The IP buffer rideth upon struct mqtt (in mqtt.h), not within STARTUP_COMMON_ELEMENTS, lest the Borland-built sbbsctrl.exe suffer ABI mismatch and bar the servers from honest labour. Closes #1124, reported by Nelgin (with much vexation upon his lute). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  540. Rob Swindell (on Windows 11)
    Sat May 02 2026 18:57:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_global.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/services.cpp diff
    src/xpdev/ini_file.c diff
    src/xpdev/multisock.c diff
    src/xpdev/sockwrap.c diff
    src/xpdev/sockwrap.h diff
    xpdev: add SOCKET_STRERROR_BUFLEN, adopt at all socket_strerror() call sites Define a recommended buffer length for socket_strerror() output in sockwrap.h next to the SOCKET_STRERROR macro, so future call sites have a discoverable, single-source-of-truth constant rather than hand-picking a size each time: #define SOCKET_STRERROR_BUFLEN 256 Replace the literal [256] (and the previously-fixed [128]) with this constant at all 11 existing call sites: xpdev/sockwrap.c retry_bind() xpdev/multisock.c xpms_add(), read_socket() xpdev/ini_file.c iniGetSocketOptions() sbbs3/main.cpp output_thread() sbbs3/services.cpp open_socket_cb(), close_socket(), cleanup(), service_udp_sock_cb(), services_thread() sbbs3/js_global.cpp js_system_get(), js_socket_strerror() The value 256 is empirically sufficient for current Windows WSA error descriptions (the longest observed is WSAESHUTDOWN's localized text at ~135 bytes); a future bump for a longer message is now a one-line change to the macro. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  541. Rob Swindell (on Windows 11)
    Sat May 02 2026 18:53:29 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    sbbs3 terminal server: include socket/protocol/IP in output_thread send-error logs Three coordinated changes to output_thread's SOCKET_ERROR handling: * Switch all six log lines from "<node-name> ..." (using sbbs->client_name like "Terminal Server" / "Node N") to the standard "%04d %s [%s] ..." prefix used by every other connection log line in the file. This groups send errors with the matching "Connection accepted", "!CLIENT BLOCKED", etc. lines for easy correlation. Capture client_socket.load() once into a local since the cascade reads it six times (it's std::atomic). * Populate the listener pseudo-sbbs's client.protocol and client_ipaddr per-connection. These were previously only set inside sbbs_t::init() for per-node sbbs's (main.cpp:3639), leaving the listener's output_thread with empty strings to print. * Demote ESHUTDOWN and EINVAL send errors to LOG_NOTICE alongside the existing ENOTSOCK / ECONNRESET / ECONNABORTED handlers. Both are the typical errors raised when the listener races with close_socket() on a blocked client (post-shutdown / mid-close socket state). Treating them as expected disconnect noise matches the existing pattern. Note: on POSIX, EINVAL from send() can theoretically indicate a programming bug rather than a socket-state race, but in practice it's the same race as on Windows and the noise reduction is worth the small risk of masking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  542. Rob Swindell (on Windows 11)
    Sat May 02 2026 18:53:07 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    sbbs3 terminal server: drop unsent goodbye output before close_socket In the four listener exit paths that queue output via the listener pseudo-sbbs (CLIENT BLOCKED for ip.can / host.can, "no nodes available", node init failure), call sbbs->rioctl(IOFB) right after flush_output() and before close_socket(). Without this, any text/badip.msg / badhost.msg / nonodes.txt residue that didn't make it out within the flush timeout sits in the ring buffer; when the output thread next wakes it tries to send the leftover on the now-closed FD, and the failed send logs a noisy warning (e.g. "!ERROR 22 (...)" or "!ERROR 58 (...)" sending on socket). This is the same idiom as the existing rioctl(IOFB) at the start of the per-connection setup (main.cpp:5795) — purge stale buffer state before changing socket lifecycle. It does not eliminate the race entirely: data already pulled from the ring buffer into the output thread's linear buffer is still sent (or attempted), since rioctl operates on the ring buffer only. The remaining noise is handled separately by demoting the typical post-shutdown send errors (ESHUTDOWN, EINVAL) to LOG_NOTICE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  543. Rob Swindell (on Windows 11)
    Sat May 02 2026 18:52:23 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    src/sbbs3/services.cpp diff
    src/xpdev/ini_file.c diff
    src/xpdev/multisock.c diff
    sbbs3,xpdev: enlarge socket-error message buffers from 128 to 256 On Windows, FormatMessageA() inside socket_strerror() can produce descriptions longer than 128 bytes for some WSA errors. WSAESHUTDOWN's localized text is ~135 bytes ("A request to send or receive data was disallowed because the socket had already been shut down in that direction with a previous shutdown call."). When the buffer is too small, FormatMessageA fails with ERROR_INSUFFICIENT_BUFFER and the caller sees the fallback "Error 122 getting error description" instead of the real message. Bump the seven remaining call sites that still passed [128] to SOCKET_STRERROR (main.cpp output_thread, services.cpp x3, multisock.c x2, ini_file.c) up to [256], matching services.cpp's other buffers (error[256] at lines 283 and 1940). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  544. Rob Swindell (on Windows 11)
    Sat May 02 2026 18:50:56 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    sbbs3 ftpsrvr: modernize legacy BOOL/TRUE/FALSE to bool/true/false Mechanical conversion across the file: function signatures, local variables (incl. volatile flags shared across xfer threads), xfer_t struct fields, and all literal arguments/comparisons. External callees (load_cfg, matchuser, getnodedat, wildmatchi, seteuid callback) already declare bool parameters, so no ABI change. Also collapse direxist()'s if/else to a single return.
  545. Rob Swindell (on Windows 11)
    Sat May 02 2026 18:28:34 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/sockwrap.c diff
    sockwrap: preserve WSAGetLastError() across socket_strerror() On Windows, FormatMessageA() shares its TLS slot with WSAGetLastError(), and calls SetLastError(ERROR_INSUFFICIENT_BUFFER) on a too-small buffer. Callers that reference SOCKET_ERRNO and SOCKET_STRERROR in the same printf-style call (e.g. main.cpp:2745, telgate.cpp:433) can therefore see a bogus normalized error code (e.g. -9878 from 122-WSABASEERR) when argument-evaluation order puts the SOCKET_STRERROR call first and the underlying message overflows the caller's buffer. Snapshot WSAGetLastError() at function entry and restore it before returning so the side-effect cannot leak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  546. Rob Swindell (on Windows 11)
    Sat May 02 2026 17:22:26 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/mqtt.c diff
    src/sbbs3/sbbs_ini.c diff
    src/sbbs3/scfg/scfgsrvr.c diff
    src/sbbs3/services.cpp diff
    src/sbbs3/startup.h diff
    src/sbbs3/websrvr.cpp diff
    sbbs3 terminal server: auto-filter IPs hitting max-concurrent limit When a client repeatedly hits the per-IP max concurrent (unauthenticated) connection limit, optionally add the IP to text/ip.can for a configurable duration. Threshold and duration are tunable in SCFG via a new submenu ("Max Concurrent Connections...") and via two new sbbs.ini keys in [BBS]: MaxConConnFilterThreshold and MaxConConnFilterDuration. A threshold of 0 (the default) disables the auto-filter and preserves prior behavior. This is a useful mitigation (when enabled by setting the threshold to a non-zero value) against the recent spate of terminal server bot attacks (likely looking for CVE-2026-31431: Copy Fail vulnerability on Linux hosts), which tend to tie up a BBS's terminal server nodes just sitting at a login prompt, causing a denial-of-service. The strike counter for an IP is held in memory and is cleared on: a successful login from that IP, terminal server recycle/restart, the clear*.term semaphore file, or the new MQTT "clear" topic. Bans are written to ip.can with the existing e=<expiry> field, so they expire naturally without any cleanup pass. A failed filter_ip() call leaves the strike count in place so we don't reset on transient errors. Also added: an MQTT "clear" topic (under both <host> and <server> scopes) that signals the corresponding server to clear its login-attempt list. The polling hook is wired into all five servers (terminal, FTP, mail, web, services) via a new clear_attempts_now flag in STARTUP_COMMON_ELEMENTS. The auto-filter on max-concurrent itself is terminal-only by design, since "nodes" are a scarce resource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  547. Rob Swindell (on Windows 11)
    Sat May 02 2026 16:42:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfglib1.c diff
    sbbs3 make_data_dirs(): faster startup directory verification On systems with many file directories (Vertrauen has thousands), the "Verifying/creating data directories" startup phase took ~8 seconds because each entry triggered serial stat()/mkdir() syscalls. Three changes drop that to ~2 seconds (4x speedup): 1. Dedup. Most cfg->dir[i]->data_dir values default to <data_dir>dirs (load_cfg.c:308), which make_data_dirs() already creates near the top of the function. Seed a str_list with that path and skip md_fast() in the per-dir loop when the value matches. The seed is only pushed on successful creation — failure causes the loop to retry per entry rather than skip silently. 2. mkdir-first via a new file-static md_fast(). md() does isdir()+stat() before mkpath, which on Windows fetches file attributes and trips Defender's "file opened" introspection. md_fast() issues a single MKDIR() and trusts EEXIST without re-stat'ing, falling back to md() only when the parent component is missing or an unexpected errno surfaces. The tradeoff is that a non-directory file at one of these paths won't be diagnosed at startup; the BBS reports it later when something tries to open files inside it. Public md() is unchanged so other callers keep their stricter contract. 3. trim_trailing_slash() helper shared between md_fast() and the loop's cache-key computation, so the canonicalization lives in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  548. Rob Swindell (on Windows 11)
    Sat May 02 2026 16:42:46 GMT-0700 (PDT)
    Modified Files:
    

    ctrl/text.dat diff
    exec/load/text.js diff
    src/sbbs3/prntfile.cpp diff
    src/sbbs3/text.h diff
    src/sbbs3/text_defaults.c diff
    src/sbbs3/text_id.c diff
    sbbs3 printfile(): less-style search and help in P_SEEK pager In P_SEEK mode, add: - '/' prompts for a search string and finds the first match forward. - 'n' / 'N' navigate to next / previous match. Search starts from the line after (or before) the previous match, so matches still visible on the page above the prompt are reachable. - '?' shows a help screen listing all keys. - Q/q quits (case-insensitive). Behavior fixes: - The pager no longer exits silently at EOF in P_SEEK mode; instead the prompt fires one final time so the user can scroll back, search again, or quit. - When a search match is in the last page-worth of the file, the screen is positioned so the last full page of content fills the screen (rather than a one-line display + EOF prompt with leftover content above). - Not-found prints text[FindStringNotFound] and re-prompts in place (file position and visible content unchanged). Pager prompt (text[SeekPrompt]) reformatted to show the filename and '(?=Help)' hint instead of the full key list. Adds new text strings text[FindStringNotFound] and text[SeekHelp]. The K_UPPER flag was removed from getkey() in the seek prompt so 'n' (next) and 'N' (prev) are distinguishable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  549. Rob Swindell (on Windows 11)
    Sat May 02 2026 16:42:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/jsexec.cpp diff
    Restore the warning about SBBSCTRL environment variable missing .. and when using the default "/sbbs/ctrl" path, but only if the input stream (stdin) is a console. This should keep the fix for CGI usage (commit 54431b319d5a4288c) but restore the helpful warning for sysops that foget to set the SBBSCTRL environment variable.
  550. Rob Swindell (on Windows 11)
    Sat May 02 2026 16:42:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/login.cpp diff
    src/sbbs3/main.cpp diff
    Reset inactivity timer after login failure delay or accept-throttle If the login inactivity timer is low, it could be triggered during or immediately after these delays, unintentionally disconnecting the user before they have have a chance to (re)try a login. This could be part of the reason behind the requested feature in issue #1124.
  551. Deucе
    Sat May 02 2026 16:25:37 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.h diff
    SyncTERM: guard SFTP externs in ssh.h on WITHOUT_DEUCESSH The squash in 961d4d4631 moved the sftp_state / sftp_available externs (and the sftp.h include they need) from sftp_session.h into ssh.h. Worked fine in DeuceSSH builds but broke the no-SSH configuration: telnets.c includes ssh.h to reach init_crypt() and friends, and pulled in sftp.h transitively — but sftp.h isn't on the include path when the SFTP library isn't built. Wrap the sftp.h include and the two externs in `#ifndef WITHOUT_DEUCESSH`. Function declarations (init_crypt, ssh_connect, etc.) stay unguarded — telnets.c still calls those. stdatomic.h moves inside the same guard since only the _Atomic bool extern needed it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  552. Deucе
    Sat May 02 2026 15:46:28 GMT-0700 (PDT)
    Added Files:
    

    src/hash/sha256.c diff
    src/hash/sha256.h diff
    src/syncterm/scripts/auto/connected/sftp_pubkey.wren diff
    src/syncterm/scripts/auto/connected/sftp_queue_init.wren diff
    src/syncterm/scripts/sftp_app.wren diff
    src/syncterm/scripts/sftp_queue.wren diff
    src/syncterm/wren_token.c diff
    src/syncterm/wren_token.h diff
    Modified Files:

    src/hash/CMakeLists.txt diff
    src/hash/objects.mk diff
    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Wren.adoc diff
    src/syncterm/bbslist.c diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/ssh.c diff
    src/syncterm/ssh.h diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/uifcinit.c diff
    src/syncterm/uifcinit.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_fs.c diff
    src/syncterm/wren_bind_fs.h diff
    src/syncterm/wren_bind_hook.c diff
    src/syncterm/wren_bind_hook.h diff
    src/syncterm/wren_bind_internal.h diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    src/syncterm/wren_bind_sftp.c diff
    src/syncterm/wren_bind_sftp.h diff
    src/syncterm/wren_embed_gen.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    Removed Files:

    src/syncterm/scripts/auto/connected/sftp_browser.wren diff
    src/syncterm/sftp_browser.c diff
    src/syncterm/sftp_browser.h diff
    src/syncterm/sftp_degraded.c diff
    src/syncterm/sftp_degraded.h diff
    src/syncterm/sftp_queue.c diff
    src/syncterm/sftp_queue.h diff
    src/syncterm/sftp_queue_screen.c diff
    src/syncterm/sftp_queue_screen.h diff
    src/syncterm/sftp_session.h diff
    src/syncterm/sftp_wait.c diff
    src/syncterm/sftp_wait.h diff
    SyncTERM: replace C SFTP client with Wren implementation Eight-step rewrite (squashed from 8 WIP/feature commits). The C-side SFTP browser, queue, queue-screen, and degraded-modal are replaced by a single Wren App with three modes; the in-process authorized_keys upload path moves to a Wren script too; and the Wren input model is rebuilt from a single-subscriber slot to a per-fiber claim stack so stacked Apps actually work. Net diff: +4577 / -3413, but most of the +4577 is the new C-extensions infrastructure (claim stack, consent tokens, SHA-256 fallback, UIFC wrappers) that the Wren scripts couldn't have been built without. == Motivation The C SFTP UI was a bug-prone fork of Synchronet's UIFC list / modal idioms with three independent state machines (browser, queue, degraded) duplicating screen save/restore, focus tracking, and event routing. Most of the same code already exists in the Wren UI primitives we built earlier this branch (Pane, ListView, Popup, App, modal stack), and Wren's fiber model is a much better fit for the SFTP transfer queue's "block on async completion + check cancel between chunks" shape than C threads + condition variables. == Wren input model — claim stack Replaced the single `parked_fiber` WrenHandle slot in wren_host_internal.h with a linked-list claim stack (newest fiber wins). Each Wren App.run pushes a claim that consumes events synchronously and posts a wake to its own fiber for repaint; the topmost living claim wins each event. Auto-prune of claims whose owning fiber is done. Pass-through (false) returns cascade to lower claims and then to the existing Hook.onKey/onMouse chain. `Input.nextEvent` is deleted outright. `Input.wake` becomes `Wake.post`. `App.runningCount` (and the gate that previously suppressed Alt-Q while another App was up) is gone — the claim stack is the ordering signal. Browser + queue + degraded modal can stack freely now. == SFTP browser, queue, degraded — single Wren App `scripts/sftp_app.wren` is one App with `setMode(\"browser\" | \"queue\" | \"degraded\")`. The browser shows directory contents with a 4-char status chip ([==] / [<>] / [↓↓] / [Q↓] / [er] / etc.) per row that refreshes in place as transfer state changes. F4 opens uifc's filepick_multi to tag local files for upload. Alt-Q from the browser switches to queue mode without spinning up a second App; queue mode shows direction arrow, status, bytes done/total, percent, basename, and remote path, sorted ACTIVE → QUEUED → DONE → FAILED → CANCELLED. Selection is preserved across rebuilds by (dir, remote) lookup. Del cancels the selected job. Esc / [X] dismisses; the dismiss semantic flips to \"suspend workers + persist\" when the shell has closed. `scripts/sftp_queue.wren` runs the transfer queue: 1-up + 1-dn worker fibers, 4 KB chunks, 250 ms idle poll cadence, .won persistence in the per-BBS Cache directory (versioned schema). Workers check cancel between chunks and bail on suspend. Both directions resume from `job.done` across reconnects (the C queue restarted from byte 0). Successful download and upload both stamp the destination's mtime to match the source so the browser's [==] chip works without depending on hash extensions. The previously-separate degraded-modal state is now just queue mode with `_shellClosed = true`. In that state, onTick_ auto-quits the App when the queue drains naturally OR the workers have suspended, so the user isn't stranded on an inactive screen. `scripts/auto/connected/sftp_queue_init.wren` is the connect- time autoload: spawns workers, binds Alt-S / Alt-Q, binds onShellClose / onDisconnect. == authorized_keys upload — moved to Wren `scripts/auto/connected/sftp_pubkey.wren` runs at connect time: if `BBS.sftpPublicKey` is set and SFTP is available, it reads the local SSH public key via `Host.sshPublicKey`, opens `.ssh/authorized_keys` with `READ|WRITE|APPEND|CREAT`, scans existing lines for the blob (matches the old C `key_not_present` semantics), and appends if absent. Holds `CTerm.sftpActive = true` for the duration so disconnect waits. `Host.sshPublicKey` returns a structured `Map { algo, blob }` rather than the raw OpenSSH line the old C used — fixes the latent bug where the C code hardcoded `\"ssh-ed25519 %s ...\"` regardless of which algorithm the local key actually was, so RSA / sntrup keys would have been written with the wrong line prefix (had the C path supported them, which it didn't). == Picker consent tokens (sandbox-preserving capability) The old C `sftp_browser` picker UI was inside the same TU as the queue, so \"upload from arbitrary path\" was natural. The Wren sandbox doesn't allow scripts to construct File foreigns from raw paths — that's the whole point of the Cache / Upload / Download Directory roots and the relaxed-name predicate. But queued picker uploads need to survive disconnect / reconnect, which means writing absolute paths to disk and re-opening them on resume. Solved with a capability-token mechanism (`wren_token.c` / `wren_token.h`). At pick time the C side mints an opaque token binding (path, file-content SHA-256) under HMAC-SHA256 with a per-installation key; Wren stores it freely (it's bytes); only the C side can mint or verify. `Host.openLocalFile(token)` is the only path by which Wren can construct a non-sandbox File foreign, and its verify checks both the HMAC and that the file's content hash still matches. File edited or replaced since consent → token rejected, user must re-pick. The signing key (32 random bytes) lives in the user's encrypted syncterm.lst under `WrenPickerHmacKey`. Generated on first USER_BBSLIST open if absent. Loaded only from the user's personal list — never from SYSTEM_BBSLIST or web-fetched lists (those would let a malicious shared list inject a known key and forge tokens). The file-content hash routes through DeuceSSH's `dssh_hash_oneshot` when available (Botan or OpenSSL backend, which on hardware with crypto extensions hits SHA-NI / ARM crypto). WITHOUT_DEUCESSH builds fall back to a new pure-C SHA-256 in `src/hash/sha256.c`. Random source falls back from `dssh_random` to `xp_random` for key generation in the no-crypto build. HMAC always uses the C SHA-256 (inputs are ~100 bytes; no benefit from acceleration). == UIFC wrappers for connected-session use The pre-existing `Host.pickFile` binding called `filepick` directly on the global uifc state. That state isn't valid during a connected session — calling api->list under those conditions crashes inside the redraw path. Added `uifcfilepick` / `uifcfilepick_multi` wrappers in `uifcinit.c` that mirror the existing `uifcmsg` / `uifcinput` / `confirm` save / init / bail / restore dance. The Wren bindings now route through those wrappers. (The `pickFile` binding had been latently broken in connected context but never triggered because nothing ever called it from a script.) == Wren API additions / changes Foreign-method bindings: Input + pushClaim(fn) → ClaimHandle (replaces nextEvent) ClaimHandle (new foreign class) + pop() Wake (new namespace) + post(fiber, value) (replaces Input.wake) Hook + onShellClose(fn), onDisconnect(fn) Host + downloadDir, uploadDir (relaxed-name Directory roots) + pickFile(initialDir, mask, opts) — accepts String OR Directory foreign for initialDir + pickFiles(initialDir, mask, opts) → List<File> | null (multi-select counterpart) + openLocalFile(token) → File | null (consent-token reopen) + sshPublicKey → Map { algo, blob } | null + uploadArrow=(b), downloadArrow=(b) CTerm + sftpActive getter / setter (Wren↔ssh.c bridge for teardown gating) + refreshStatus() File + mtime getter, mtime=(t) setter + token getter (consent-token bytes; null for sandboxed Files) SFTP + setMtime(fiber, path, t) + lname getter (extension-negotiated flag) SFTPEntry + hasLongDesc, hash App + runChild(fn) — spawn child fiber, pump until completion Plus the Directory bindings gained the relaxed-name predicate (spaces, leading dots, parens, lengths up to 255 bytes — but still blocks separators, NUL, control bytes, dot/dotdot, Windows reserved names) inherited from Host.downloadDir / Host.uploadDir. == C-side infrastructure (needed pieces) src/hash/sha256.c, sha256.h Public-domain reference SHA-256 (FIPS 180-4) matching the existing sha1.h API shape. Added to objects.mk + CMakeLists.txt. Used by the consent-token verify path when DeuceSSH isn't built in. src/syncterm/wren_token.c, wren_token.h HMAC-SHA256 sign / verify shim plus per-installation key set / clear / generate / hex helpers. HMAC implemented inline (RFC 2104) on top of the SHA-256 above. File hash prefers DeuceSSH's hardware-accelerated path when available. src/syncterm/uifcinit.c, uifcinit.h + uifcfilepick / uifcfilepick_multi wrappers + shared uifcfilepick_common helper. src/syncterm/bbslist.c iniReadBBSList(USER_BBSLIST) loads or generates WrenPickerHmacKey from the encrypted blob. SYSTEM_BBSLIST and web lists never trigger this branch. src/syncterm/wren_host_internal.h parked_fiber slot replaced by claim stack (struct wren_input_claim *claim_top + claim_next_id). src/syncterm/wren_bind_screen.c Claim push / dispatch / auto-prune; nextEvent / parked teardown removed. src/syncterm/wren_bind_fs.c Token field on wren_file; Host.pickFile signature change (Directory acceptance + token signing); Host.pickFiles; Host.openLocalFile; File.token getter; Host.downloadDir / uploadDir + relaxed-name predicate. src/syncterm/wren_bind_conn.c CTerm.sftpActive + wren_sftp_active() bridge for ssh.c. src/syncterm/term.c is_connected ORs in wren_sftp_active(); shell-close transition fires wren_host_dispatch_shell_close exactly once; status-bar arrows read wren_upload_arrow_lit / wren_download_arrow_lit. src/syncterm/wren_embed_gen.c Minifier preserves blank lines so script error messages report source line numbers correctly. src/syncterm/scripts/wrentest.wren New T-cases for claim auto-prune, same-fiber replacement, cascade ordering. == Files removed (replaced by Wren equivalents) sftp_browser.c / .h — Alt-S file browser sftp_queue.c / .h — transfer queue + worker threads sftp_queue_screen.c / .h — Alt-Q queue UI sftp_degraded.c / .h — shell-close modal sftp_wait.c / .h — sync-wait shim around sftpc_*; only remaining caller (ssh.c authorized_keys upload, now in Wren) is gone, and ssh.c's SSH_FXP_INIT wait is now an inline xpevent_t sftp_session.h — folded into ssh.h (where the two surviving externs sftp_state / sftp_available actually belong) ssh.c also drops `add_public_key`, `key_not_present`, `get_pubkey_str`, `pubkey_thread_running`, all eleven `free(pubkey)` call sites, the `_beginthread(add_public_key, ...)` launch, and the pubkey-thread wait in `ssh_close`. == Verification Built clean under `gmake` (DeuceSSH/Botan backend on FreeBSD). End-to-end manually verified on bbsdev.net: F4 picks tagged files; uploads enqueue and complete with consent tokens; shell-close mid-upload preserves the in-flight job ACTIVE; on reconnect, token verifies (HMAC + content SHA-256 match) and the worker resumes from the saved offset. Browser status chips update in place. Alt-Q stacks the queue on top of the browser; Esc returns; queue auto-quits when shell has closed and work has drained. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  553. Deucе
    Fri May 01 2026 20:08:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren/vm/wren_vm.c diff
    Wren: close upvalues on fiber abort to prevent UAF runtimeError() unwound the caller chain without calling closeUpvalues() on the aborting fibers. Every other code path that ends a function's stack — CODE_RETURN, CODE_CLOSE_UPVALUE — closes upvalues first; the abort path was the lone exception. A closure created inside an aborted frame that survives (held by a module-level static, a host callback, an observer list, …) keeps upvalues whose `value` pointers still point INTO the dead fiber's stack. Once GC reclaims the dead fiber and DEALLOCATEs its stack, subsequent reads through those upvalues return whatever now lives at that address — silently wrong values at best, SIGSEGV at worst when the freed memory gets recycled into something whose bytes decode as a tagged pointer to a stale ObjUpvalue. Reproducer (200 fibers each capture and abort, then read back): before — 194 of 200 closures returned the wrong value after — 0 of 200 wrong Filed upstream as wren-lang/wren#1234. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  554. Deucе
    Fri May 01 2026 10:21:20 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_embed_gen.c diff
    SyncTERM: minify embedded Wren scripts wren_embed_gen now runs each script through a small in-place minifier before emitting it as a C string literal: - // line comments and /* */ block comments stripped (Wren allows nested block comments — the minifier handles nesting). - Per-line leading and trailing whitespace stripped. - Runs of blank lines collapsed to a single newline. - Newlines preserved as Wren statement separators. - String literals (and their %() interpolations) pass through verbatim — content inside "..." is never touched. A small state stack tracks alternating CODE / STRING; on stack overflow the tail gets copied without further minification. Cuts embedded_scripts.c from ~306 KiB to ~165 KiB (-46%) and the .text bytes that actually live in the binary at runtime drop in proportion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  555. Deucе
    Fri May 01 2026 10:21:20 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    src/syncterm/menu.c diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_screen.c diff
    SyncTERM: mouse drag-select cleanup for Wren App mode Three related fixes so drag-to-select-and-copy works correctly while a Wren App is up: 1. ui_app.wren / dispatchMouse_: - Stop ungetmouse'ing the BUTTON_1_DRAG_START before calling mousedrag(). mousedrag's switch treats anything that isn't MOUSE_MOVE / BUTTON_1_DRAG_MOVE as the end-of-drag arm — so feeding it the start event made it copy an empty selection and exit on iteration 1, leaving the actual drag-MOVE / DRAG_END events stranded for the next interaction (visible symptom: the copy from drag N didn't happen until drag N+1 started). conio preserves startx/y across drag events, so mousedrag's first DRAG_MOVE already has the right anchor — no unget needed. - Add an _dragHandedOff flag set after Input.mousedrag() returns; if a stray DRAG_END / button1Release ever leaks into Wren's queue (backend race, etc.) we silently swallow it instead of dropping it on a widget that didn't expect it. 2. ui_list.wren / handle: - Outside the scrollbar, accept only Press/Click for row selection. Drag events used to also pick a row, so dragging across the list captured the drag and prevented the C-side selector from running — now drag-start falls through to dispatchMouse_ for text selection. Scrollbar drag is unchanged. 3. C-side mousedrag — force-rect option: - term.h, term.c: mousedrag(scrollback, force_rect). When force_rect is true, rect_mode starts true and mode_locked starts true, so Alt no longer toggles between line and rect. - term.c, bbslist.c, menu.c: existing C callers pass false (preserve Alt-toggle behavior). - wren_bind_screen.c / wren_bind.c / syncterm.wren: optional bool parameter on Input.mousedrag(). ui_app passes true; line-mode select is meaningless across cell-aligned widget chrome, users almost always want a rectangle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  556. Deucе
    Fri May 01 2026 09:47:06 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp_client.c diff
    sftp client: eager-bail per parse step in parse_readdir Restructure the readdir entry-parse loop so each of the three calls (filename getstring, longname getstring, fattr getfattr) is followed immediately by its NULL check, rather than running all three before checking any of them. On corrupt input this skips one or two doomed parses on the wreckage of the failing one, and the PENDING_RECORD error string now identifies which of the three steps failed and which entry of N — much easier to triage than the old "getstring/getfattr failed at entry N" generic. No on-the-wire change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  557. Deucе
    Fri May 01 2026 09:40:55 GMT-0700 (PDT)
    Modified Files:
    

    exec/syncterm-bounce.js diff
    Add a zip noise whenever a bounce happens.
  558. Deucе
    Fri May 01 2026 09:39:26 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/sftp_browser.wren diff
    Modified Files:

    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_popup.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_sftp.c diff
    src/syncterm/wren_bind_sftp.h diff
    SyncTERM: Wren SFTP browser New scripts/auto/connected/sftp_browser.wren replaces the C-side sftp_browser.c for read-only navigation of an SFTP session. Bound to Alt-S; spawns a child fiber whose App owns the screen (CTerm suspended while modal). Display: - Each row shows size, mtime, an optional `+` marker (when the server's fattr advertises a non-empty descs@syncterm.net extension), and a label. Directories use lname when the server provides one; files keep their bare filename and put lname into a bottom description bar inside the pane. - F2 fetches the long description via the descs request and displays it preformatted (block-centred, no per-line centring). - Esc / [X] dismiss; F1 / [?] show help; Enter descends into directories; .. ascends. Files don't transfer yet — that lands with the Wren queue replacement. C-side wiring (wren_bind_sftp.[ch], wren_bind.c): - struct wren_sftp_entry gains has_long_desc; build_sftp_entry populates it from sftp_fattr_get_ext_by_type(de->attrs, SFTP_EXT_NAME_DESCS). - SFTPEntry foreign class gains hasLongDesc accessor. ui_app.wren: - dispatchMouse_ now hands an unclaimed button-1 drag-start back to the C-side selector (Input.unget(me) + Input.mousedrag()), so the standard SyncTERM rectangle / line-copy UI works while a Wren App is up. ui_popup.wren: - New splitHardLines_ helper splits on CR/LF/CRLF without word-wrapping. longestHardLine_ now counts codepoints rather than bytes so multi-byte UTF-8 (e.g. box-drawing) doesn't inflate the popup width measurement. - Popup gains a `preformatted` mode and Alert.showPreformatted: splits only on hard breaks (no word-wrap, no mid-codepoint splits) and left-aligns all lines at one block-column so the block is centred while each line keeps its relative shape. centeredBounds_ takes a preformatted flag for consistent row counting between sizing and painting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  559. Deucе
    Fri May 01 2026 09:38:06 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sftp.cpp diff
    sftp server: long-description availability + UTF-8 conversion Two changes to the sbbs3 SFTP server: 1. Mark filebase entries that have an extdesc. When SFTP_EXT_DESCS is negotiated, get_filebase_attrs() scans the loaded message header's dfield[] array for a TEXT_BODY with non-zero length and, if found, adds a zero-length descs@syncterm.net attribute extension to the entry's fattr. Presence of the extension is the marker; clients issue the existing descs@ EXTENDED request to fetch the actual text. No additional disk I/O — file_detail_normal already loads the dfield array. 2. Convert CP437 → UTF-8 at every user-visible string boundary that crosses into an SFTP attr extension or extended reply. The SFTP wire is UTF-8, but Synchronet stores filebase strings (lib lnames, dir lnames, file short descriptions, extdescs) in CP437. Adds a sftp_cp437_to_utf8_strdup() helper and applies it at the four sites: get_lib_attrs, get_dir_attrs, get_filebase_attrs (lname short-desc), and sftp_ext_descs (extended-reply long-desc). minval='\x80' matches the convention in writemsg / sbbsecho / msgtoqwk — ASCII passes through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  560. Deucе
    Fri May 01 2026 07:58:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp_pkt.c diff
    sftp: fix tx and rx buffer round-up bugs at 4 KiB boundaries Two parallel undersizing bugs in the shared SFTP packet library caused both peers to corrupt their own buffers near SFTP_MIN_PACKET_ALLOC (4 KiB) boundaries. rx_pkt_append used new_sz = new_sz / SFTP_MIN_PACKET_ALLOC + SFTP_MIN_PACKET_ALLOC — a divide where there should have been a `+= (BLOCK - remain)`. For any inbound chunk whose total exceeded the first 4 KiB boundary the formula collapsed back to roughly 4 KiB, the realloc undersized the buffer, and the subsequent memcpy walked past the heap allocation. grow_tx had a structurally different but related bug: in the existing-packet branch it computed newsz = pkt->used + need; omitting the offsetof(struct sftp_tx_pkt, type) header that pkt->sz already accounts for. The realloc rounded `pkt->used + need` up to the next 4 KiB boundary, but the actual write target lands at `pkt->used + offsetof(type) + need`, so when (used + need) lands on a 4 KiB boundary the new allocation undershoots by up to offsetof(type) bytes. The fresh-allocation branch and the trailing asserts both already used the offsetof-inclusive form; bring the existing-packet branch in line. The library is shared, so each bug corrupted both peers: rx_pkt for large inbound packets (uploads, large readdir replies, file data), grow_tx for outbound packets that crossed a 4 KiB boundary. Symptom in the wild was a SyncTERM SFTP browser failing to list a 25-entry filebase directory with garbled fattr / filename strings near entry 23-24. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  561. Deucе
    Thu Apr 30 2026 18:58:03 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp_attr.c diff
    free()ing a NULL pointer is a NOP, not a crash.
  562. Deucе
    Thu Apr 30 2026 14:39:34 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/wren_bind_won.c diff
    src/syncterm/wren_bind_won.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    Removed Files:

    src/syncterm/wren_bind_wom.h diff
    Of WOM Misnamed, and WON Restored to its True Estate Hark! In our late commit, this humble serializer didst we christen "WOM" — to wit, "Wren Object Model" — yet 'twas in foul error spoke. The fairer name, "WON" (Wren Object Notation), now do we restore unto its rightful seat. Thus rename we the class, the C-tongue handles all, the very files themselves (wren_bind_wom.{c,h} unto wren_bind_won.{c,h}), the builder's runes, and every Wren-side parchment and trial in concordant step, that no whisper of the old misnomer shall linger to confound the reader's eye. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  563. Rob Swindell (on Debian Linux)
    Thu Apr 30 2026 14:14:11 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/prntfile.cpp diff
    fix b0rked b#ffer in printf!le() line-@-a-time m0de - n0 m0re g4rb4ge fgetline() wuz l34v!ng the buf un-NUL-term!nated wh3n a s!ngle l!ne maxxed out the all0c (PRINTFILE_MAX_LINE_LEN, 8KiB). the m3mset() zer0ed the b#ffer up fr0nt, but the l00p (`while (len < size)`) w0uld happ!ly 0verwr!te every byte !ncl#ding the rezerved term!nator sl0t, then truncnl()/putmsgfrag() w0uld str|en() r!ght 0ff the end 0f the heap and sp3w wh4tever uninit b!ts l!ved next d00r == g4rb4ge 0n the wire. 0nly tr!ggered !n the line-@-a-time br4nch w/o P_SEEK: - P_OPENCLOSE path fr3ad()s and explic!tly NUL-term!nates - P_SEEK passes cols=term->cols so the d!splay-w!dth check breaks the l00p l0ng b4 the b#ffer f!lls - line-@-a-time + n0 P_SEEK passes cols=0, so 0nly \n / EOF / l3n==s!ze c0uld ex!t -- and the l4st 0ne 8 the term!nator f!x: rezerve a byte 4 the NUL (`while (len + 1 < size)`) + guard size==0. the m3mset-zero @ s[len] n0w alwayz s#rvivez. big upz 2 c0defen!x 4 the b#g report -- repro w4z .vt (VT-100 4rt) f!lez >8KiB w/ n0 l!nefeeds, wh!ch hammered the exact c0de path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  564. Deucе
    Thu Apr 30 2026 12:12:04 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/wren_bind_wom.c diff
    src/syncterm/wren_bind_wom.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    SyncTERM: add WOM serialization to the Wren scripting host WOM (Wren Object Model) round-trips Wren values through a literal text format that's a strict subset of valid Wren syntax. Supports Null, Bool, Num, String, List, Map; coerces Range / other Sequence to List on serialize. Cycles abort the fiber. Pretty-print supported via an indent-string second argument. scripts/syncterm.wren: new class WOM with four serialize entry points (serialize / serializeLossy, each with optional indent String) plus a foreign deserialize. Strict serialize aborts on unsupported types, NaN, or Infinity; lossy silently omits unsupported list items / map entries and maps a top-level unsupported value to "null". Both modes abort on cycles. String escapes match Wren's compiler so output pastes into Wren source — every literal % is escaped to \% so the output never starts an interpolation. wren_bind_wom.c: hand-written recursive-descent literal parser for deserialize. No use of the Wren compiler — input never reaches eval, so untrusted text is safe. Bounded recursion (256 levels), trailing commas allowed inside [...] / {...}, arbitrary whitespace between tokens, escape set matches Wren's readString exactly (accepting any escape Wren itself rejects would break the "output is valid Wren source" property). Input is copied at entry so a GC during the parse can't invalidate the slot pointer. Errors abort the fiber with a message that includes the byte offset. scripts/wrentest.wren: 14 new inline test methods covering primitive round-trip, all string escapes, container compact + pretty form, Range coercion, whitespace and trailing-comma tolerance, hex literals, deeply nested round-trip, lossy mode (list + map), strict-mode aborts, cycle detection, and parse errors. Tests that involve multi-key Maps assert structurally rather than against a pinned literal, since Wren Map iteration order is hash-bucket order. Wren.adoc: new === WOM section under == Object Model with the type table, member table, and a worked example. String-escape entry in === Literals expanded to list \%, \a, \b, \e, \f, \v. New ==== Strings and % subsection (cross-referenced from a new pitfalls list entry) covering the basic rule that every literal % needs \%, with a footnote on the rare \\\% case that comes up when generating escape sequences. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  565. Deucе
    Thu Apr 30 2026 10:54:24 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/syncview/objects.mk diff
    Fix SyncView build Needs the new ansi_filter.o
  566. Deucе
    Thu Apr 30 2026 10:37:19 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    SyncTERM: tolerate trimmed source dist + half-finished DeuceSSH configure Source-dist tarball ships ssh/CMakeLists.txt without ssh/examples/, so DeuceSSH's add_executable(client/server) targets fail at configure ("Cannot find source file"). cmake's failure mode is "Configuring done — Generate step failed", which leaves deucessh.pc on disk (the configure_file ran) but no Makefile/build.ninja (generation aborted). The next gmake then sees deucessh.pc, skips configure, and runs `cmake --build` against a Makefile-less directory — "No rule to make target 'Makefile'", with no breadcrumb pointing back at the original cmake error. Two fixes: src/ssh/CMakeLists.txt: gate the example targets on EXISTS so a trimmed source tree doesn't fail configure. The examples are EXCLUDE_FROM_ALL anyway and never built as part of a normal target. src/syncterm/GNUmakefile: replace the deucessh.pc-only sentinel with a DSSH_BUILD_READY check that requires both deucessh.pc AND a build-system file (Makefile or build.ninja). A half-finished configure no longer looks succeeded. Same check applied to the build recipe's `if [ ! -f $(DEUCESSH_PC) ]` short-circuit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  567. Deucе
    Thu Apr 30 2026 10:09:45 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren/vm/wren_compiler.c diff
    SyncTERM: harden vendored Wren compiler against fuzzer-found crashes Five compiler-level bugs reported by oneafter against upstream wren-lang/wren (issues #1217-#1221). All five reproducers exit cleanly with compile errors after the fix under -fsanitize=address,undefined. #1217 — peekChar OOB in readRawString: the loop unconditionally peeked two characters after nextChar(), so consuming the buffer's terminating '\0' caused a 1-byte read past the source allocation. The "consume the closing two quotes" calls after the loop did the same on the unterminated path. Hoist the c=='\0' check above the peeks; only run the trailing two-quote consume when the loop actually saw the closing triple-quote. #1218 — stack exhaustion via deep nesting: the recursive descent parser had no depth limit. ~300 frames of definition/finishBlock/ statement/forStatement/loopBody corrupted the C stack and ASAN reported it as a heap-buffer-underflow in resolveLocal's memcmp. Add MAX_RECURSION_DEPTH (256) plus a recursionDepth counter on Parser; gate statement() and expression() at the entry point. #1219 — emitOp stackEffects[] OOB: validateNumParameters reports an error at arity == MAX_PARAMETERS+1 but does not stop or clamp, so callSignature emits (Code)(CODE_CALL_0 + arity) past CALL_16/SUPER_16 and emitOp reads stackEffects[] beyond its 77 entries. Clamp arity in callSignature and callMethod, and add a sizeof-based bounds guard in emitOp as the safety net. #1220 — NULL deref in getByteCountForArguments: after error recovery emits malformed bytecode, endLoop's body walk treats arg bytes as opcodes; a CODE_CLOSURE byte then dereferences constants[] with a bogus index against an empty (NULL data) constants buffer. Skip the walk entirely when parser->hasError is set — the function is going to be discarded anyway. #1221 — vsprintf overflow in printError: a 159-byte stack buffer was filled by sprintf+vsprintf with no length checks. Attacker-controlled identifiers (method/variable names ≤ MAX_VARIABLE_NAME * actual length) can blow the buffer via formats like "Method '%s' is already defined." Switch to snprintf+vsnprintf with remaining-bytes accounting; drop the now-redundant ASSERT. Thanks to oneafter for the careful fuzzing reports and reproducers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  568. Deucе
    Thu Apr 30 2026 09:37:06 GMT-0700 (PDT)
    Modified Files:
    

    3rdp/build/GNUmakefile diff
    src/build/Common.gmake diff
    src/ssh/CMakeLists.txt diff
    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/syncterm/GNUmakefile diff
    SyncTERM: macOS universal vendored-Botan + DeuceSSH pipeline Existing FAT=1 nightly build threaded -arch x86_64 -arch arm64 through SyncTERM's own CFLAGS but couldn't carry the same intent into the vendored-Botan recipe (Botan's configure.py emits per-arch intrinsic .cpp files only for the configured --cpu) or DeuceSSH (a CMake subproject that doesn't see the FAT cascade). Two flags compose: - FAT=1 alone: each slice picks its host's natural microarch defaults. Fine for a developer making a fat build for local testing. - MACOS_PORTABLE=1 alone: single-arch redistribution; pins -march=skylake on x86_64 hosts (oldest microarch macOS 13 boots on) and -mcpu=apple-m1 on arm64 hosts (only arm64 CPU macOS 13 supports; M2/M3/M4 are supersets). - FAT=1 MACOS_PORTABLE=1: universal binary with per-slice floors via -Xarch_x86_64 -march=skylake -Xarch_arm64 -mcpu=apple-m1. The intent of the existing nightly script. 3rdp/build/GNUmakefile: when os=darwin && FAT, set BOTAN_FAT and run the recipe twice — extract Botan.tar.xz into per-arch source trees, configure each with --cpu=<arch> --cc-abi-flags='-arch <arch> [floor] [-mmacosx-version-min=...]', build, install the x86_64 slice, then lipo-merge the two libbotan-3.a archives over the installed copy. Headers + pkgconfig are arch-independent so the install-once is fine. Common configure flags shared between fat and non-fat paths via BOTAN_CFG_BASE so neither drifts. src/build/Common.gmake: MACOS_PORTABLE adds -Xarch_<arch>-prefixed microarch flags to CFLAGS/LDFLAGS when paired with FAT, or host-arch flags otherwise. src/syncterm/GNUmakefile: new darwin branch in the DeuceSSH cmake toolchain args adds -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" (quoted to survive the recipe's shell), per-slice floor flags, and -DCMAKE_OSX_DEPLOYMENT_TARGET=<MIN_MAC_OSX_VERSION>. Both the parse-time deferred-config probe and the recipe-time cmake invocation now explicitly forward PKG_CONFIG_PATH; build/botan.gmake's make-level `export` doesn't reach $(shell ...) calls and didn't reach the recipe's child shell on darwin either. src/ssh/CMakeLists.txt: probes that decide which compiler flags get into DEUCESSH_COMPILE_OPTIONS now run with -Werror=unused-command- line-argument always (was only when -Werror was already on, which is Debug-only). AppleClang accepts a number of GCC-style hardening flags (-fstack-clash-protection, -fstrict-flex-arrays, ...) but treats them as no-ops, so plain check_c_compiler_flag returned HAVE_X=YES for flags the binary got none of. The strict probe correctly drops them. src/ssh/kex/libcrux_mlkem768_sha3.h: two narrowing fixes for warnings AppleClang flags that GCC doesn't. `~value0` (uint16_t promoted to int by ~) cast back to uint16_t at the wrapping_add() call; `-zetaN` (int16_t promoted to int by negation) cast back to int16_t at the four ntt_multiply_binomials() calls. Note: this header is vendored from OpenBSD's libcrux extraction — if it gets re-extracted the patches need re-applying (or upstreaming). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  569. Deucе
    Thu Apr 30 2026 01:44:46 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/ansi_filter.c diff
    src/syncterm/ansi_filter.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/SyncTERM.vcxproj diff
    src/syncterm/Wren.adoc diff
    src/syncterm/objects.mk diff
    src/syncterm/ripper.c diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_hook.c diff
    src/syncterm/wren_bind_hook.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: shared ANSI filter; Hook.onMatchClean; drop misleading onMatch consume Lifts ripper.c's `ansi_only` state machine into a reusable pair (`ansi_filter.[ch]`) with two modes — KEEP_ESC (ripper's existing behavior, drops plain text) and KEEP_TEXT (drops escape sequences, keeps the text the user actually sees). ripper.c's `ansi_only` is now a 4-line wrapper, behavior unchanged. `Hook.onMatchClean(pattern, fn)` is the escape-aware variant of `onMatch`: inbound bytes are pre-filtered through `ansi_filter` in KEEP_TEXT mode before reaching the regex VM, so a literal pattern matches the visible text even when the BBS interleaves colour codes through it. Per-hook entry tracks its own filter state. Both `onMatch` and `onMatchClean` are now passthrough-only. The historical "Bool true to drop the matched bytes" contract on `onMatch` was misleading — `dispatch_match_drain` only dropped the single byte that completed the match (every prior byte already went through cterm), so users never actually got "drop the matched span." Anyone needing wire-level drop should use Hook.onInput, which is byte-granular by design. Wren.adoc, the worked-example, and the hook table all updated. `check_speedwatch` in term.c was assessed for filter reuse but left alone — it does sub-byte param validation specific to the speed response (`ESC [ 0|1 ; digits+ * r`) that the general filter doesn't help with; wrapping the filter around it requires a CSI-body buffer plus a post-parser, net code goes up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  570. Deucе
    Thu Apr 30 2026 00:51:12 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind_fs.c diff
    SyncTERM: drop POSIX stat from Wren fs binding (MSVC link fix) Directory.contains used stat() + S_ISREG to gate "is regular file"; S_ISREG isn't defined on MSVC, so the Windows build linked with an unresolved external for _S_ISREG. Replace with the portable xpdev combo fexist() + !isdir(). While here, swap the two struct-stat-for-size queries in File.size and file_map_for_hash for flength() — same cross-platform reasoning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  571. Deucе
    Thu Apr 30 2026 00:38:09 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_pane.wren diff
    src/syncterm/scripts/ui_widget.wren diff
    src/syncterm/scripts/ui_widget_test.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    SyncTERM: Wren Input.wake + App.post; UI fit-and-finish; doc the language New host primitive: Input.wake(fiber, value) — queue a fiber resumption on the same result queue Input.nextEvent and Timer.trigger drain through. Safe to call from Hook.onInput; the resume happens on the next main-loop drain, so a network-driven app (IRC, ticker, log viewer) can wake a UI fiber parked on Input.nextEvent when remote bytes change visible state. If the target is also the parked-fiber slot, wake clears it (compared via wrenValuesSame on the underlying Value, since handles wrapping the same fiber are distinct pointers but equal Values) so the next Input.nextEvent re-arms cleanly. App.post() / App.post(value) wraps Input.wake against a captured _runFiber; App.onPost=(fn) is the user-visible handler. Container.focusStep_ now returns false when the only focusable child is already focused, so a Pane wrapping a single ListView (or any nested single-focusable Container) no longer traps Tab inside itself — Tab bubbles up to the parent. New regression test in ui_widget_test. Pane.helpButtonRect_ suppresses the [?] button when neither onHelp nor helpText is wired. A button that does nothing is worse than no button. Demos that want the button now set helpText with relevant key hints (gallery, Checkbox, RadioGroup, SpinBox, TextInput, Form). Wren.adoc gains two top-level chapters before Quick Start: a Wren Language Reference (literals, statement-termination rules, classes/fields scope, fibers, modules, common pitfalls) and a Wren Standard Library reference (System / Object / Class / Bool / Null / Num / String / List / Map / Range / Sequence / Fiber / Fn) so other LLMs pointed at this doc don't have to chase wren.io fragments. Also fixes asciidoctor's `...` -> ellipsis substitution wherever three dots are Wren range / slice syntax (escaped via \\...). Documents Input.wake, App.post / App.onPost, the popStatus z-order (below modals), and the gatesActiveLayer two-axis layer model. Updates check.on glyph reference (now √, not ■). wrentest gains T07: Input.wake delivers two values (a foreign KeyEvent and a String) to fibers parked on plain Fiber.yield (no Input.nextEvent registration), exercising both the result-queue plumbing and the WrenHandle pin/release across types. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  572. Deucе
    Wed Apr 29 2026 22:35:45 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/ui_checkbox.wren diff
    src/syncterm/scripts/ui_checkbox_test.wren diff
    src/syncterm/scripts/ui_form.wren diff
    src/syncterm/scripts/ui_form_test.wren diff
    src/syncterm/scripts/ui_menubar.wren diff
    src/syncterm/scripts/ui_menubar_test.wren diff
    src/syncterm/scripts/ui_radio.wren diff
    src/syncterm/scripts/ui_radio_test.wren diff
    src/syncterm/scripts/ui_spinbox.wren diff
    src/syncterm/scripts/ui_spinbox_test.wren diff
    src/syncterm/scripts/ui_statusbar.wren diff
    src/syncterm/scripts/ui_statusbar_test.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/ui.wren diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_draw.wren diff
    src/syncterm/scripts/ui_draw_test.wren diff
    src/syncterm/scripts/ui_pane.wren diff
    src/syncterm/scripts/ui_popup_test.wren diff
    src/syncterm/scripts/ui_style.wren diff
    src/syncterm/scripts/ui_widget.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_internal.h diff
    src/syncterm/wren_bind_screen.c diff
    SyncTERM: Wren UI form controls (Checkbox, Radio, Spin, Menu, Status, Form) Six new widgets: - Checkbox: single-row [X] Label toggle. - RadioGroup: vertical mutually-exclusive selector with cursor / selection split and a wrap= flag (Form turns wrap off so Up/Down escape the group at its edges). - SpinBox: numeric +/- with step / page / home-end keys. - StatusBar: bottom-row strip; text= or segments= [text, align]. - MenuBar: horizontal command strip with Left/Right/hotkey/click activation; per-item callbacks, no nested submenus. - Form: Container that lays out (label, widget) field rows with optional OK / Cancel buttons. Layer-awareness gets a second axis: Widget.gatesActiveLayer (default false; Pane overrides true). An unfocused gating ancestor dims its whole subtree, so multi-pane layouts go inactive consistently instead of just along the chrome. Cursor highlight on RadioGroup only paints when the group itself has focus. Glyphs gained an auto-fallback: Glyphs[name] now probes the primary's first codepoint via the new foreign Codepage.encodes_(text) and promotes to the per-entry ASCII fallback if the primary isn't in CP437 (cell storage codepage). Resolutions are cached. Default glyphs cleaned to CP437-representable primaries throughout (radio.on •, check.on √, tag.on »). App.popStatus overlay now draws *below* the modal stack, so a dialog the user is interacting with isn't obscured by an indicator behind it. Painter.frameTitle pre-truncates the title before the cell-budgeted text() call so the trailing-space pad doesn't bleed extra characters past the right bracket. decode_utf8_first lifted out of wren_bind_screen.c's static scope and declared in wren_bind_internal.h — Codepage.encodes_ in wren_bind.c needs the same UTF-8 first-codepoint decode. Wren.adoc: subsections for the new widgets, an import update, the gatesActiveLayer + CP437-fallback notes, and the popStatus z-order note. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  573. Deucе
    Wed Apr 29 2026 22:35:45 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/wl_events.c diff
    SyncTERM: drive Wayland toplevel resize when content size changes update_surface_size was setting wp_viewport's destination but never hinting the new content rect to the compositor. When a text mode change shrank the viewport below the toplevel's last-configured size (e.g. terminal mode → dialing directory after Alt-H disconnect), the viewport occupied a sub-rect and the rest of the toplevel was painted by the compositor — transparent → black, ticket 246's "screen blacks out" symptom. Add xdg_surface_set_window_geometry alongside the viewport update so floating compositors snap the toplevel to the new mode's natural integer-multiple size. Tiled compositors keep their layout-imposed size either way. Maybe fixes ticket 246. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  574. Rob Swindell (on Debian Linux)
    Wed Apr 29 2026 22:15:39 GMT-0700 (PDT)
    Modified Files:
    

    exec/txt_handler.js diff
    txt_handler: green-on-black palette, file mtime stamp, drop search - Recolor body and bars from blue/gray to green-on-black (mono-CRT look), with a thin themed scrollbar (was hidden with scrollbar-width:none, which left users with no visible scroll affordance). - Replace the "LIST v9.1" brand in the top bar with the file's mtime formatted as MM-DD-YY HH:MM. - Remove the custom "/" search, "n"/"N" next, and the search-bar UI — Ctrl-F covers it natively and with more features. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  575. Deucе
    Wed Apr 29 2026 21:20:29 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/ui.wren diff
    src/syncterm/scripts/ui_button.wren diff
    src/syncterm/scripts/ui_button_test.wren diff
    src/syncterm/scripts/ui_help.wren diff
    src/syncterm/scripts/ui_help_test.wren diff
    src/syncterm/scripts/ui_input.wren diff
    src/syncterm/scripts/ui_input_test.wren diff
    src/syncterm/scripts/ui_popup.wren diff
    src/syncterm/scripts/ui_popup_test.wren diff
    Modified Files:

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_draw.wren diff
    src/syncterm/scripts/ui_draw_test.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/scripts/ui_list_test.wren diff
    src/syncterm/scripts/ui_pane.wren diff
    src/syncterm/scripts/ui_style.wren diff
    src/syncterm/scripts/ui_style_test.wren diff
    src/syncterm/scripts/ui_widget.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    SyncTERM: Wren UI — popups, buttons, input, help, theme cascade Extends the pure-Wren UI library with TextInput, Button, Help, and Popup/Alert/Confirm/Prompt/PopStatus. Adds the inactive theme cascade (UIFC: white-on-cyan behind a modal), drop shadows, double-line title-bar frames, and [?]/[X] corner buttons on Pane. App now captures the screen as a backdrop, manages a modal stack, and exposes popStatus + F1 showHelp. Widgets skip redraw when the cached surface already matches the active/inactive layer state, and Container adds spatial Up/Down focus traversal. ui.wren re-exports the public classes for one-line consumer imports. Wren.adoc gets a "Built-in UI Library" reference chapter covering App, Widget/Container, Pane, ListView, TextInput, Button, the Popup family, Help, Theme/Style/Glyphs, Painter, and the demo gallery. Also fixes a pre-existing section-level jump in the Hook Events chapter. REPL.printTrace_ now fires WREN_ERROR_RUNTIME so caught-fiber errors land on stderr like uncaught ones do. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  576. Deucе
    Wed Apr 29 2026 20:03:50 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cg_cio.m diff
    src/conio/wl_events.c diff
    src/conio/x_events.c diff
    SyncTERM: fix CR sending two bytes on X11, Wayland, Quartz 637e9e5bb5 changed the 1-byte/2-byte split in send_key from "low byte is non-zero" to "key value > 0xff" to handle the new CIO_KEY_WREN_CONSOLE (0x29E0, low byte 0xE0). But ScanCodes entries pack scancode in the high byte and ASCII in the low byte (Enter = 0x1c0d), so the magnitude check sent every typed Enter as (0x0d, 0x1c) instead of just 0x0d — producing a stray scancode byte after every CR. rip_getch reassembles iff the first byte is 0x00 or 0xE0, so the correct discriminator is the low byte: extended when low byte is 0x00 or 0xE0, otherwise plain ASCII. Same fix applied to the Cocoa cg_send_key path which had the same bug under Ctrl+Enter (uncommon but real). Fixes ticket 247. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  577. Deucе
    Wed Apr 29 2026 15:28:16 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    SyncTERM: pass PKG_CONFIG_PATH explicitly to parse-time pkg-config Some gmake versions (observed on Ubuntu) don't propagate `export PKG_CONFIG_PATH := ...` from build/botan.gmake into $(shell) calls during the same parse phase, so vendored Botan's pkg-config dir isn't seen — pkg-config falls back to system search and fails to find botan-3. The make-side variable is set correctly, so re-emit it on the pkg-config command line for the three botan/libcrypto probes that matter (DSSH_CRYPTO_READY, TRAILING_LIBS, BOTAN_CXXFLAGS). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  578. Deucе
    Wed Apr 29 2026 15:11:58 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/ui_app.wren diff
    src/syncterm/scripts/ui_demo.wren diff
    src/syncterm/scripts/ui_draw.wren diff
    src/syncterm/scripts/ui_draw_test.wren diff
    src/syncterm/scripts/ui_list.wren diff
    src/syncterm/scripts/ui_list_test.wren diff
    src/syncterm/scripts/ui_pane.wren diff
    src/syncterm/scripts/ui_style.wren diff
    src/syncterm/scripts/ui_style_test.wren diff
    src/syncterm/scripts/ui_widget.wren diff
    src/syncterm/scripts/ui_widget_test.wren diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/menu.c diff
    src/syncterm/menu.h diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_bind_internal.h diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: pure-Wren UI library (Surface compositing, ListView, Pane, App) Full set of new scripts/ui_*.wren modules plus the C-side Surface foreign that backs them. Cells collapses into Surface — a single w×h cell-buffer that's both a linear Sequence (count/[i]/iter) and a 2D grid (cellAt/putRect/fill). Per-widget Surfaces composite into the App's screen-sized backbuffer for a single putRect blit per frame, eliminating the flicker the direct-screen-write path showed. App owns mouse/cursor lifecycle: saves and restores the terminal's mouse-event mask + CustomCursor on entry/exit, replaces the mask with exactly the events the UI wants (button-1 click+drag, wheel up/down), parks the hardware cursor on the focused leaf's cursorPos, applies CustomCursor.none unless cursorVisible is set (text inputs to come). ListView: Surface-backed rendering, scrollbar with proper thumb- position math, mouse drag follows endY, scroll wheel by 3 rows, hover ignored. Cursor pos tracks the selected row. Ctrl+S menu gets a "Wren Console" entry that synthesizes the Ctrl+\` keypress through wren_host_dispatch_key — works on ANSI/curses backends that can't capture the binding directly. Build: filter *_test.wren, *_demo.wren, and wrentest.wren out of the embedded scripts so user builds don't carry developer tooling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  579. Deucе
    Wed Apr 29 2026 15:08:59 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    SyncTERM: bootstrap DeuceSSH cmake configure at recipe time The deucessh recipe assumed configure had already run at parse time, but the parse-time probe deliberately skips configure when no system crypto is on disk — leaving the recipe to "cmake --build" a non-existent build tree. Run cmake -S/-B inside the recipe when deucessh.pc is missing so the unprobed path (fresh checkout, vendored Botan only) actually works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  580. Deucе
    Wed Apr 29 2026 14:39:27 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    DeuceSSH: drop blanket -Werror outside Debug builds Default CMAKE_BUILD_TYPE to Release for single-config generators so the user gets one predictable optimized build instead of three different defaults (empty/Debug/Release) producing three different compiles. Blanket -Werror now applies only to CMAKE_BUILD_TYPE=Debug. Targeted -Werror=format-security/=implicit/=incompatible-pointer-types/ =int-conversion stay on in every config — those catch C-obsolete constructs and security-relevant misuse, not stylistic diagnostics that a future compiler might broaden. Promoting every new compiler warning to a fatal error in a release tarball is hostile to packagers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  581. Deucе
    Wed Apr 29 2026 10:54:16 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/auto/connected/console.wren diff
    src/syncterm/wren_bind_screen.c diff
    SyncTERM: console UTF-8 — fix byte/codepoint mismatches in print path Two paired bugs that together caused test labels with multi-byte UTF-8 (em-dashes, arrows, box-drawing) to render as garbled + truncated text in the Wren console viewport. wren_bind_screen.c — fnScreenWindow_print used to putch each input byte individually, so a 3-byte em-dash advanced the cursor by 3 columns and rendered as 3 garbage CP437 cells (Γ Ç ö). Now decodes codepoint by codepoint and maps each via cpchar_from_unicode_cpoint(CIOLIB_CP437, cp, '?') before a single putch — same path Cell.ch= already takes — so cursor advance == rendered cell count and unmappable codepoints fall back to '?'. Invalid UTF-8 still passes through as a single raw byte for binary output. scripts/auto/connected/console.wren — String.count is codepoint count (Sequence iteration yields one codepoint per step), but String[i] / String[a...b] are byte indexed via wrenStringCodePointAt + calculateRange against string->length. Mixing the two in put_() truncated multi-byte log output by (bytes - codepoints) at the tail. Same trap was present across the input-line editor (BS, Del, arrows, Home/End, Ctrl+W, history recall) where cursor is byte-shaped but every end-of-input boundary was input.count. Switch to s.bytes.count everywhere a byte count was meant. No change for ASCII-only paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  582. Rob Swindell (on Debian Linux)
    Tue Apr 28 2026 20:23:15 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/ircd/config.js diff
    exec/load/ircd/unregistered.js diff
    ircd: phone-patch the webchat repeater (WEBIRC support) Add the WEBIRC handshake to ircd.js so a trusted webchat gateway -- think HAM phone-patch -- can call CQ as the relay station and announce the real operator's QTH (IP/host) ahead of NICK/USER. Without it, every webchat op keys up under the gateway's own callsign: useless for /whois, useless for K-lines, useless for accountability. Trusted gateways are configured via [WebIRC:N] sections in ircd.ini, or 'W:gateway-ip:password' lines in legacy ircd.conf -- matching the look of the existing [Operator]/[Server]/[Ban] sections (and O:/N:/K: rows). * exec/load/ircd/unregistered.js -- new case "WEBIRC" handler. Verifies the shared password against the W: line for the connection's source IP, rewrites this.ip / this.hostname to the supplied values, cancels any in-flight RDNS, and calls Unregistered_Check_User_Registration in case NICK/USER have already arrived. Idempotent: a second WEBIRC on the same channel is logged and dropped. Untrusted source or bad password is silently ignored (logged at WARN) so a misconfigured patch stays debuggable instead of breaking the band. * exec/load/ircd/config.js -- WLines storage in Clear_Config_Globals, ini_WebIRC parser registered in ini_sections, [WebIRC:N] block emitter in Write_Config_File, WLine constructor, and a 'W:' case in the legacy read_conf_config switch so both config formats round-trip cleanly. 73 de unregistered.js -- clear and QRT. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  583. Deucе
    Tue Apr 28 2026 19:35:13 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind_sftp.c diff
    SyncTERM: stub Wren SFTP foreigns under WITHOUT_DEUCESSH WITHOUT_DEUCESSH drops the SSH transport (and therefore SFTP) but wren_bind.c's BINDINGS table and lookup_class still reference the SFTP allocators / finalizers / methods. Without DeuceSSH the body of wren_bind_sftp.c can't compile at all (sftp.h lives in src/ssh, which the makefile doesn't add to the include path in that config). Wrap the body in #ifndef WITHOUT_DEUCESSH and provide stubs in the #else branch. SFTP.available reports false (so well-behaved scripts skip the subsystem entirely); the rest of the SFTP class methods abort the calling fiber. Per-instance accessors are unreachable since instances can't be constructed in this build, but they still need symbols for BINDINGS to link. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  584. Deucе
    Tue Apr 28 2026 19:24:51 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/wren_bind_conn.c diff
    src/syncterm/wren_bind_conn.h diff
    src/syncterm/wren_bind_fs.c diff
    src/syncterm/wren_bind_fs.h diff
    src/syncterm/wren_bind_hook.c diff
    src/syncterm/wren_bind_hook.h diff
    src/syncterm/wren_bind_internal.h diff
    src/syncterm/wren_bind_screen.c diff
    src/syncterm/wren_bind_screen.h diff
    src/syncterm/wren_bind_sftp.c diff
    src/syncterm/wren_bind_sftp.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/wren_bind.c diff
    SyncTERM: split wren_bind.c into per-class-group modules wren_bind.c grew past 5500 lines as Wren foreigns accumulated; split it into per-class-group .c files so each class group can be read, edited, and grep'd in isolation. Layout after split: wren_bind.c 1052 BBS + FlowControl, REPL, Clipboard, Timer, Platform, BINDINGS table, lookup_class wren_bind_screen.c 1678 Screen, Input, KeyEvent, MouseEvent, Cell, Cells, Font, Hyperlinks, Color, Palette, CustomCursor, VideoFlags wren_bind_fs.c 1109 Directory, File, Host (with the live-foreign registry that invalidates open Directory / File foreigns on rename / remove) wren_bind_sftp.c 920 SFTP, SFTPEntry, SFTPStat, SFTPHandle, SFTPError + the zero-copy pending → deliver handoff wren_bind_hook.c 411 Console (log buffer), Hook (registration entry points + RE1 setjmp catcher), HookHandle (GC-safe entry wrapper) wren_bind_conn.c 390 Conn, CTerm, ExtAttr, LastColumnFlag Stays in wren_bind.c because they touch each other or the VM internals: BBS owns FlowControl (BBS.flowControl returns a FlowControl); REPL.compile_ uses wrenCompileSource which isn't part of wren.h; Timer/Platform/Clipboard are too small to warrant their own modules. wren_bind_internal.h holds the shared SWF_* foreign-type tag enum, the wren_foreign_header that every foreign struct starts with, and the static inline slot_foreign_type / load_class_into_slot / wren_throw helpers — pulled into every module so foreigns can type-check each other across module boundaries. The single BINDINGS[] table stays in wren_bind.c, walked linearly by wren_bind_lookup_method (~250 entries — bsearch isn't worth the maintenance cost of keeping the table sorted). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  585. Deucе
    Tue Apr 28 2026 17:41:21 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    SyncTERM: File.sha1 / File.md5 + missing docs and tests File.sha1 and File.md5 hash the file's full content via xpmap and the existing src/hash sha1.c / md5.c. Zero-length files are special-cased to an empty buffer because xpmap rejects 0-sized maps. Returned as raw digest bytes (Wren strings are byte-safe) so they compare directly against SFTPEntry.hash from the sha1s@syncterm.net / md5s@syncterm.net SFTP extensions; format hex yourself if you need it for display. Also catches Wren.adoc + wrentest.wren up to recent work that shipped without docs / tests: - New Wren.adoc sections for Platform, Timer (+ TimerElapsed), SFTP (+ FileFlag, SFTPEntry, SFTPStat, SFTPHandle, SFTPError, and the shared async-op pattern used by Timer / SFTP / Input.nextEvent). File doc gets the sha1 / md5 row added. - wrentest.wren coverage: Platform.name returns non-empty String. File.sha1 / File.md5 of an empty file (exercises the zero-length code path) and of "hello" (exercises xpmap). Timer.trigger(ms=0) parks a fiber, the doterm sweep marks it past-due, the drain resumes with a TimerElapsed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  586. Deucе
    Tue Apr 28 2026 17:16:35 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: useful wren-cli module pieces — Platform + Timer Two small classes lifted in spirit from wren-cli, adapted to our fiber-arg / result-queue model rather than wren-cli's libuv + Fiber.transfer + Scheduler.await_ shape. Platform.name returns the uname(2) sysname on POSIX (e.g. "FreeBSD", "Linux", "Darwin"), "Windows" on Windows, "Unknown" elsewhere. Win32 checked first so POSIX-overlay environments (Cygwin, MSYS) report the native OS rather than uname's emulated reply. No isPosix or shell-exec / process surface — scripts on this host don't need it. Timer.trigger(fiber, ms) registers a one-shot resumption: pushes a TimerElapsed onto the result queue once xp_timer() reaches the absolute due-time, then the standard drainer resumes the fiber. Multi-fire / event-loop dispatch by type works straight out of the existing pattern: Timer.trigger(Fiber.current, 100) SFTP.stat(Fiber.current, "/foo") while (true) { var x = Fiber.yield() if (x is TimerElapsed) { spinner.tick(); Timer.trigger(Fiber.current, 100) } if (x is SFTPStat) { ... break } if (x is SFTPError) { ... break } } For recurring fire-and-forget callbacks (no fiber resume), use Hook.every(ms, fn) — different pattern, both useful. Implementation: bounded array of (fiber, due_s) on wren_host_state.pending_timers, swept once per doterm() iteration just before wren_result_drain. Past-due entries push TimerElapsed to the result queue with NULL data; the deliver fn builds a TimerElapsed foreign on demand. Cap is 32; overflow throws on Timer.trigger so a runaway script fails loudly rather than silently dropping registrations. Skipped: wren-cli's Scheduler (redundant with our doterm + result queue), io (async File/Directory; ours is intentionally synchronous on the owner thread), Process / shell-exec, Stdin/Stdout/Stderr (SyncTERM scripts don't have a CLI). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  587. Deucе
    Tue Apr 28 2026 16:50:13 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_bind.c diff
    SyncTERM: Wren foreign toString overrides The default Object.toString returns "instance of <ClassName>" — fine for opaque values, useless for data-bearing ones. Adding overrides on the foreigns where it earns its keep. Natural-form (no class prefix; the value has an obvious string form): Directory → path with trailing slash, or "Cache/" for the lazy BBS-cache directory; "(dead)" suffix when invalidated File → path with " (open)" / " (dead)" suffix SFTPEntry → server's longname (ls-style) when present, else name Debug-form ("ClassName(field=val, ...)"; no canonical string form): KeyEvent → "KeyEvent('a' 0x0061)" with text when printable, "KeyEvent(0x3B00)" otherwise MouseEvent → "MouseEvent(event=N at X,Y)" or "...at X,Y-X',Y'" Cell → "Cell(0xNN attr=0xNN)" Cells → "Cells(count=N)" — metadata only; scripts can use cells.join(", ") for content via Sequence HookHandle → "HookHandle(calls=N)" SFTPStat → "SFTPStat(size=N, mode=0NNN, mtime=T)" SFTPError → "SFTPError: <name>: <message>" using the new sftp_err_short_name helper for library codes and sftp_get_errcode_name for SSH_FX_* server statuses SFTPHandle deliberately keeps the Object default — the alive case is no improvement, and "is the handle closed" wants an explicit getter, not state buried in toString. ExtAttr / LastColumnFlag / FlowControl / CustomCursor / VideoFlags also keep the default — they're niche bitfield snapshots that scripts query field-by-field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  588. Deucе
    Tue Apr 28 2026 16:32:52 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/syncterm.wren diff
    SyncTERM: Cells is Sequence The iterate / iteratorValue protocol Cells already implemented makes it Sequence-compatible — declaring it `is Sequence` brings in all of Wren core's iteration helpers (each, where, map, all, any, contains, count(f), reduce, join, take, skip, isEmpty, toList) without any extra C-side bindings. The O(1) foreign `count` getter overrides Sequence's default (which would otherwise walk the iterator). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  589. Deucе
    Tue Apr 28 2026 16:32:26 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: Wren SFTP API + Input.nextEvent fiber-arg primitive Adds a Wren-callable SFTP surface on top of the async sftpc_ library and converts Input.nextEvent to the same fiber-arg shape. Async ops take the fiber-to-resume as their first argument, fire the underlying request, and return immediately: SFTP.<op>(fiber, args...) -> null | SFTPError null means the request was queued (the caller may yield to receive the result via fiber.call); SFTPError means the request couldn't be queued (session is gone, OOM at the foreign-method site) — no callback will fire. All other errors round-trip through the result queue so the caller's yield surfaces them as an SFTPError too. Common idioms: // fire and immediately await var r = SFTP.realpath(Fiber.current, ".") || Fiber.yield() // multi-fire event loop, demuxed by type SFTP.stat(Fiber.current, "/a") SFTP.stat(Fiber.current, "/b") Input.nextEvent(Fiber.current) while (true) { var x = Fiber.yield() if (x is SFTPStat) ... if (x is KeyEvent) { Input.nextEvent(Fiber.current); ... } if (x is SFTPError) break } // hook-friendly: callback fiber, calling fiber doesn't yield SFTP.realpath(Fiber.new {|r| ... }, ".") Library additions (src/sftp/): sftpc_mkdir / sftpc_rmdir / sftpc_remove / sftpc_rename — four status-only async ops that were stripped in 97dd3955d6. They reuse the bare struct sftpc_pending plus parse_status_only and share a do_one_path helper for the single-path variants. Wren classes (foreign): SFTP — static-only; available, pubdir, plus 12 async ops (realpath, stat, opendir, readdir, close, open, read, write, mkdir, rmdir, remove, rename). SFTPEntry — name, longname, size, mtime, isDir, hash. SFTPStat — size, mtime, atime, mode, uid, gid. SFTPHandle — opaque server file/dir handle; finalizer fires sftpc_close fire-and-forget when GC'd. SFTPError — code (sftp_err_code_t), serverStatus (SSH_FX_*), message, isTransient. FileFlag — six SSH_FXF_* bitmask constants for SFTP.open. Zero-copy delivery: the recv-thread cb runs under state->mtx and just stamps ctx->pending = p + pushes onto the result queue. The owner-thread deliver fn reads typed-pending fields directly (sftp_str_t bytes, sftpc_attrs getters, sftpc_dir_entry array) and frees the pending in the queue's free fn. No memcpy under the mutex; the only Wren-heap copy is wrenSetSlotBytes / strdup at delivery, which is unavoidable. Input.nextEvent converted to the same shape: Input.nextEvent(fiber) -> null …replacing the prior Input.park_(fiber) primitive + Input.nextEvent() auto-yielding wrapper. The wrapper existed only to work around now- removed quirks (implicit CTerm.suspended setting, the now-dropped Hook.onOutput). Dropping it lets hooks pass Fiber.new {|ev| ...} for callback-style delivery without nesting Fiber.new {...}.call(). Throws if another fiber is already registered (single-subscriber is structural). Wren.adoc, wrentest.wren, and the relevant internal-comment references updated to match the new API.
  590. Deucе
    Tue Apr 28 2026 13:03:41 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/sftp_wait.c diff
    src/syncterm/sftp_wait.h diff
    Modified Files:

    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_outcome.c diff
    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/sftp_browser.c diff
    src/syncterm/sftp_queue.c diff
    src/syncterm/ssh.c diff
    sftp: invert the client API to send-then-callback Each sftpc_<op>(state, args, cb, cbdata) builds + sends the request packet, registers a typed pending entry, and returns immediately. The recv thread (sftpc_recv) demuxes by request-ID, parses the reply into the typed pending's fields, and invokes the callback. No more send_and_wait inside the library — the wait belongs to the caller that wants it. Pending shape: a common struct sftpc_pending base (req_id, list links, parse + free_self function pointers, reply ptr, delivered/ aborted flags, callback + cbdata) plus per-op typed extensions that embed the base as their first field — sftpc_realpath_pending, sftpc_open_pending, sftpc_read_pending, sftpc_stat_pending, sftpc_readdir_pending, sftpc_descs_pending. Ops with no extra result data (close, write, setstat) use the base directly. Outcome lives on the pending: err, result, estr. estr is heap- allocated and grown via realloc as records accumulate (errors aren't a hot path) so there's no fixed-size cap on diagnostic text — important because parts of estr come straight from the remote (SSH_FXP_STATUS message + lang). struct sftpc_outcome and SFTPC_OUTCOME_DECL are gone from the client side; the server side keeps its existing flex-array struct sftps_outcome. SyncTERM scaffolding (throwaway, deletes when the Wren UI lands): - New sftp_wait.[ch] with sftp_wait_cb (signals an event from cbdata) plus sftp_sync_<op> blocking wrappers per op. The existing C-side SFTP UI (sftp_browser, sftp_queue, ssh.c's sftpc_init / authorized_keys read+write loop) all use these. - Migration is verbose but mechanical — each old call site becomes "front half + check err/result + read typed field + sftpc_pending_free". Fine for the alpha-level prototype. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  591. Deucе
    Tue Apr 28 2026 11:13:23 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind.c diff
    SyncTERM: fix MSVC build — drop direct S_ISDIR use in wren_bind MSVC's <sys/stat.h> doesn't define the POSIX S_ISDIR macro, so referring to it from wren_bind.c made the linker treat it as a function and fail with LNK2019. S_ISREG already had a local polyfill at the top of the file; the two S_ISDIR sites just predated it. Replace both with the xpdev wrappers — isdir() for the directory test, fexist() for "exists as a non-directory" (regular file, symlink, device on POSIX). Each call is one extra stat compared to the prior single-stat-then-check, but neither site is hot: dir_list_impl runs once per readdir entry on browse, and dir_delete_impl runs once per Directory.delete(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  592. Deucе
    Tue Apr 28 2026 09:21:54 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    SyncTERM: File.readLine + File.writeLine readLine() reads from the current offset to the first LF (0x0A) or EOF and returns the bytes with any trailing LF removed. Offset advances past the LF on a hit, or to EOF if none found. Returns null when already at EOF so a typical loop terminates cleanly; a blank line is the empty string, distinct from EOF. writeLine(s) writes the bytes of s at the current offset, then appends an LF. Offset advances past the LF. No special-casing if s already ends in LF — writeBytes() is the way to opt out of the trailing-LF behavior. Implementation chunks reads through a 512-byte buffer with geometric growth on long lines, so a 100GB file with short lines doesn't allocate the whole remainder up front. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  593. Deucе
    Tue Apr 28 2026 09:08:09 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/console.wren diff
    src/syncterm/scripts/wrentest.wren diff
    SyncTERM: WrenConsole.register for module-defined REPL commands Modules can plug in their own /<name> entries via WrenConsole.register(name, help, fn). The handler runs with the raw argument string (everything after the first separating space, or "" if none) inside a Fiber so a runtime abort surfaces as a logged error rather than tearing the console out from under itself. Re-registering a name overwrites; names can't contain spaces. /? now lists registered commands as continuation lines under the built-in "commands:" row, each annotated with the help text. WrenConsole.unregister(name) drops a registration (idempotent); WrenConsole.commands returns the sorted list of currently-registered names for tests + tooling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  594. Deucе
    Tue Apr 28 2026 06:24:54 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    SyncTERM: Hook.onInput String returns + filter spillover Extend Hook.onInput so a handler can return a String to replace the input byte with the string's bytes (up to 256; bigger replacements log a runtime error and the byte passes through). Bool true still drops, anything else still passes through. Decouple the wire-side buffer from recv_byte_buffer: a separate wire_buffer holds raw conn_recv_upto bytes, and the filter runs them into recv_byte_buffer until either input exhausts or output fills. When a replacement won't fit, the filter pauses on that input byte; the unprocessed wire-side tail stays parked in wire_buffer until the next recv_bytes() call drains something out and frees room for it. Spillover means recv_byte_buffer never has to grow past BUFFER_SIZE even with aggressive expansion. wren_host_dispatch_input now returns int — KEEP/DROP/N — and writes replacement bytes into a caller-provided buffer. The caller (wren_filter_input) commits N bytes only if they fit, otherwise backs out without consuming the input byte. Adds an LF→CRLF hook to wrentest.wren that ticks a counter when it fires; report_() asserts the counter is positive, exercising the new WREN_TYPE_STRING dispatch branch end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  595. Deucе
    Tue Apr 28 2026 05:57:58 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/auto/connected/console.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: Wren result-queue framework + CTerm.suspended Generic completion queue: callable C-side request data + fiber handle + deliver/free callbacks travel through one mutex-protected FIFO, drained at the top of each doterm() iteration. The drainer walks each entry, skips fibers where Fiber.isDone (cached primitive handle), builds the Wren foreign right before wrenCall, and releases the handle + frees the data after. Workers can push from any thread; delivery is owner-thread only. Input.nextEvent now flows through the queue: dispatch_key/dispatch_mouse push an input_result carrying the raw key code or mouse_event and transfer the parked fiber handle. One-iteration latency on delivery, but the wrenCall is no longer fired mid-foreign-stack. Replaces the implicit "parking on Input.nextEvent claims the screen" behavior with an explicit CTerm.suspended Bool. Backed by a doterm() local; while true, the wire pump halts and bytes pile up in the conn buffer until the TCP window fills and the remote sees backpressure. When the suspend flag transitions back to false, doterm() credits the byte pump with all the bytes that would have drained at the emulated rate during the suspended window. Those bytes burst past the speed gate one per pump iteration until the credit runs out; the visible output catches up to where it would have been with no suspend. No-op when speed emulation is disabled. Adds T06 to wrentest.wren that exercises the queue end-to-end: parks a fiber on Input.nextEvent, ungets a sentinel key, sets + clears CTerm.suspended around the resume, and verifies the fiber captured the right KeyEvent. Also flips console.wren's launcher hook to the filtered Hook.onKey(Key.wrenConsole) form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  596. Deucе
    Tue Apr 28 2026 04:51:29 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/term.c diff
    SyncTERM: fire Hook.onInput on the raw wire stream, pre-parse_rip Previously the input hook fired well downstream — after parse_rip, ZMODEM detection, and OOII processing — which meant scripts never saw the bytes those layers had already consumed. In RIP mode, the hook saw essentially nothing. Move the dispatch into recv_bytes(), right after conn_recv_upto and before parse_rip, and compact dropped bytes out of the buffer in place. Scripts now see every byte off the wire and can intercept them before any framing layer. Gated on wren_host_active() so the no-scripts path stays a single global bool check, no per-byte function-call overhead. Speed emulation does not apply: parse_rip already bypasses speed gating in bulk, so gating only the Wren hook would be inconsistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  597. Deucе
    Tue Apr 28 2026 04:41:40 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/conn.c diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/scripts/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: drop Hook.onOutput The hook fired on bytes the program itself was about to send — by definition, output the script already knows about. Its presence forced a re-entry guard (`output_dispatching`) around every Wren-originated `Conn.send`/`sendRaw`, plus a documented carve-out saying "your own sends won't fire onOutput". Net negative: a footgun with no real use case. Removes the hook event, its dispatcher, the foreign binding, and the re-entry bookkeeping. `Conn.sendRaw` stays — it still serves its other purpose, bypassing IAC escaping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  598. Deucе
    Tue Apr 28 2026 04:16:03 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/auto/connected/connected.wren diff
    src/syncterm/scripts/auto/connected/console.wren diff
    src/syncterm/scripts/auto/connected/runtests.wren diff
    src/syncterm/scripts/wrentest.wren diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Wren.adoc diff
    src/syncterm/wren_embed_gen.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: split Wren scripts into lazy lib vs auto-loaded entry trees Files at scripts/*.wren are pure library modules (loaded only when imported). Files at scripts/auto/<event>/*.wren auto-run when the framework fires <event> — currently only "connected" exists, but the layout reserves room for "startup", "ssh", "ui", etc. wren_embed_gen now infers the event from the path and emits it as a new field on struct embedded_script; entries with event=NULL are libraries. The host filters the auto-load loop by event match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  599. Deucе
    Tue Apr 28 2026 03:45:45 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/console.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: Wren API audit + Directory rework + doc completeness pass Three threads, committed together because they overlap in the same files: API shape audit --------------- * Input.next, Input.poll, Input.nextEvent — converted from getters to methods (Input.next(), Input.poll(), Input.nextEvent()). The rule "getters are for things that feel like variable access, not things that feel like they're doing something" — these block, poll, or park a fiber, so they're actions. * Directory.list — converted from method to a foreign Map getter. The directory's contents read like a property; indexing `Cache.list["RIP"]` returns the File or Directory handle for that name (or null), which composes naturally for tree traversal: `Cache.list["RIP"].list["icons.dat"]`. Directory rework ---------------- * Directory.create(name) — now actually creates the file (was a no-op File-object factory). Uses C11 exclusive-create (fopen("wbx")) for race-free atomic creation. Returns null on any failure (file exists, invalid name, path too long, OS reject). * Directory.createDir(name) — added. Mirrors create() for subdirectories via MKDIR (which is naturally exclusive). * Directory.delete(name) — added. Parent-acts-on-child shape: removes the named entry (regular file or empty directory only; refuses symlinks, devices, FIFOs). Returns bool. * File.delete() — removed. The instance-method-that-zombies-its- receiver shape was awkward; Directory.delete(name) covers the case from the parent. * Directory.list now returns Files AND Directories — the C implementation always built a Map keyed by name with File values for regular files; this extends it to also emit Directory values for subdirectories, matching the documented intent. Live-handle registry -------------------- A successful Directory.delete shouldn't leave outstanding handles to the removed entry quietly operating on stale paths. Each wren_file and wren_directory now self-registers on a doubly-linked list rooted on wren_host_state. Helpers fs_register_*, fs_unregister_*, fs_kill_*, fs_invalidate_subtree. Two layers of staleness protection on every File / Directory operation: 1. dead flag — set by fs_invalidate_subtree when a parent's delete removes the entry (or marks an ancestor). file_check / dir_check (called at the top of every method) abort the fiber on dead. 2. Per-call fexist() / isdir() — catches deletions that bypassed Directory.delete (other process, the user, another script). On miss: fs_kill_*(handle) (mark dead + unregister) + throw. Open-file exemption: a File between open() and close() skips the fexist() check (its fd is authoritative — Unix lets reads/ writes continue past unlink, and Windows refuses to delete open files at all). fs_invalidate_subtree skips fp != NULL entries on the same logic. fn_File_close re-runs fexist() after fclose; if the path is gone, the handle becomes dead. Wren.adoc completeness pass --------------------------- Stale "see ciolib.h" references replaced with full reference content: * Codepage — every entry described, _b suffix explained. * Key — full grouped tables (ASCII / cursor / modified Insert- Delete / modified arrows / function keys with all four modifier columns / synthetic markers). * Font — full 46-row table including the 1-31 unnamed-in-Wren slots that are still reachable numerically; "thin"/"swiss" font-style annotations explained. * Screen.supports, Screen.videoFlags — every flag described. * ConnType, Emulation, BBSListType, ScreenMode, AddressFamily, MusicMode, RipVersion, Parity, FlowControl, LogLevel, ExtAttr, LastColumnFlag, LogMode, StatusDisplay — all converted from bare name lists to descriptive tables. Corrections to wrong descriptions: * sxScroll — SIXEL scroll mode (not "smooth scroll" / DECSCLM). * blinkAltChars — repurposes attribute bit 7 to select the alt character set (not "animate alt-char-set on blink interval"). * StatusDisplay — VT320 DECSSDT semantics (host-writable status line, not "verbose status showing connected host"). Worked example replaced. The "auto-respond to a prompt" example was using onInput + manual line buffering with a logic bug that only checked for prompts on LF (so "Logon: " — which has no trailing LF — never matched). Replaced with a Hook.onMatch two-liner; added a smaller per-byte BEL-counter example that demonstrates onInput correctly without the broken pattern. Anchors added: [[hook-events]], [[modal-input]], [[codepage]], [[filename-policy]], [[directory-handle-staleness]] so the existing <<...>> cross-refs resolve. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  600. Deucе
    Tue Apr 28 2026 02:41:45 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/console.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_bind.c diff
    SyncTERM: Wren console — /mods command + REPL.modules Adds REPL.modules, a foreign static getter that walks vm->modules->entries directly to enumerate every module currently loaded into the VM (including core, every embedded module, every user script, and anything pulled in via import). Skips empty slots and tombstones (key is UNDEFINED_VAL for both); non-string keys are filtered defensively. console.wren grows a /mods command that calls REPL.modules, sorts via a byte-wise stringLT_ helper, and prints the result. Wren's String doesn't implement <, so List.sort()'s default {|a, b| a < b} comparator aborts on string lists; the helper does ASCII-safe byte comparison and is reusable for other string sorts that come up later. Wren.adoc refreshed: documents the new Hook.dispatch_ contract ("hooks must run synchronously; wrap parking work in Fiber.new {...}.call()"), corrects the Modal Input section's description of nextEvent-from-a-hook (now detected and reported, not silently hung), describes REPL.eval's actual statement-keyword pre-classifier (was still describing the old try-expression-first flow), and adds REPL.modules + /mods + /? + /q to the command tables. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  601. Rob Swindell (on Debian Linux)
    Mon Apr 27 2026 19:56:07 GMT-0700 (PDT)
    Added Files:
    

    exec/CLAUDE.md diff
    Instructions for Claude's creation or modification of JavaScript files
  602. Rob Swindell (on Debian Linux)
    Mon Apr 27 2026 19:55:24 GMT-0700 (PDT)
    Added Files:
    

    exec/load/md2asc.js diff
    exec/typemd.js diff
    Add Markdown viewer (typemd.js / md2asc.js) Mirrors the typehtml.js / html2asc.js pair: a thin command script in exec/ handles file I/O, while exec/load/md2asc.js does the conversion to plain text with optional Synchronet Ctrl-A attribute codes. Supports headings, bold/italic, strikethrough, code spans and fenced/indented code blocks, ordered and unordered lists, blockquotes, links, images, autolinks, horizontal rules, and strips inline HTML (with <details>/<summary> handled specially). Usage: typemd [-mono | -color] <filename> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
  603. Deucе
    Mon Apr 27 2026 18:19:10 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/load/wrentest.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: route Wren hook fires through Hook.dispatch_ Every hook fire (onKey/onInput/onOutput/onMouse/onStatus/every) now goes through Hook.dispatch_(fn[, arg]) instead of fn.call() directly. The wrapper runs the handler in a child fiber via Fiber.try; if the handler yields directly (e.g. naively calls Input.nextEvent or a parking SFTP op at its top level), the wrapper detects f.isDone == false, logs a clear error naming the right escape hatch, and returns null so the dispatcher's bool/string slot read falls through to passthrough/default. Errors are caught the same way and pushed through REPL.printTrace_ for visibility. Hook drops its (misleading) `foreign` modifier — it has no instance state and no allocator was ever registered. Plain `class` lets the new dispatch_ / finishDispatch_ methods sit alongside the foreign statics, matching the Color / ScreenSupports pattern. C dispatchers cache state.hook_class + dispatch0/dispatch1 method handles after the syncterm module loads, then push slot 0 = Hook, slot 1 = fn, slot 2+ = arg(s). build_match_list now takes a list_slot parameter so the regex match list can land at slot 2. Six tests in wrentest.wren cover: passthrough (1-arg, 0-arg, bool true/false), direct-yield rejection (1-arg, 0-arg), child-fiber yield permitted (the supported `Fiber.new { ... }.call()` pattern for parking work from inside a hook), and Fiber.abort caught. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  604. Deucе
    Mon Apr 27 2026 18:18:41 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/console.wren diff
    SyncTERM: wrap Wren console run-loop body in Fiber.try A runtime error inside the run loop would skip the screen-bounds and screen-contents restore, leaving the user staring at the console's painted state with no recovery short of disconnecting. Move the body into runBody_ called via Fiber.new {...}.try() so the restore path always runs. On error, log the abort + stack trace via REPL.printTrace_ and skip Console.markSeen() — the unread-log indicator surfaces it for the next console session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  605. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ini_crypt.h diff
    src/syncterm/legacy_ciphers/idea.c diff
    src/syncterm/legacy_ciphers/legacy_ciphers.h diff
    src/syncterm/legacy_ciphers/rc2.c diff
    src/syncterm/legacy_ciphers/register.c diff
    src/syncterm/scripts/runtests.wren diff
    src/syncterm/wren_embed_gen.c diff
    SyncTERM: drop SPDX banners + spurious copyright headers Files Claude generated had picked up two stale conventions: per-file SPDX-License-Identifier banners (redundant with the per-tree license) and "Copyright by Stephen Hurd" lines on code Stephen didn't write. Strip both from the offending files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  606. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Wren.adoc diff
    src/syncterm/scripts/console.wren diff
    src/syncterm/scripts/load/wrentest.wren diff
    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: Wren HookHandle + main-loop hook compaction Foreign HookHandle returned by Hook.on*/Hook.every — no Wren-side constructor, so scripts can only remove their own hooks. Carries per-entry metrics (callCount, totalRuntime, min/maxRuntime) timed through xp_timer(). Hook + timer entries are now heap-allocated structs reached through pointer arrays in state.hooks[]/state.timers[]. HookHandle.remove() releases the fn handle and links the entry onto a cleanup queue; wren_host_compact() drains that queue from the doterm() outer loop, shifts the entry out of its dispatch array, and frees regex resources. The struct itself stays alive until both compaction has run and Wren's GC has fired the foreign-class finalizer, so removed handles keep returning sensible metric reads until the script drops them. The unified wren_hook_entry struct discriminates hook vs timer via ev (extended with WREN_HOOK_TIMER), letting the dispatch infrastructure share one path for the lifetime + cleanup machinery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  607. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/syncterm.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: lazy-cache foreign class handles, move Cache into Wren Cleanup after rev to per-module entry scripts: * The four foreign-class handles (Cell, Cells, KeyEvent, MouseEvent) were captured eagerly in wren_host_init, which forced an `import "syncterm"` bootstrap to run first so the wrenGetVariable lookups had a loaded module to read from. None of that was necessary — the handles are pure caches around symbol-table lookups, and by the time any allocation site runs, the calling script has already imported syncterm. New load_class_into_slot helper does the fetch-and-cache lazily; six allocation sites (push/resume key & mouse, fn_Screen_readRect, cells_make_view) call it at the slot they were already filling. The `if (... || st->X_class == NULL)` bailouts on those sites were guarding a case the lazy fill makes impossible. * `Cache` (the singleton Directory pointing at the script-cache directory) was injected into the syncterm module from C via wren_bind_define_cache, reaching into vm->modules and calling wrenDefineVariable. Replaced with a Wren-side `var Cache = ...` in syncterm.wren backed by a new foreign static Host.cacheDirectory. Cache is now an ordinary module variable instead of a C-injected ghost. A user override of syncterm.wren has to include the same boilerplate (one line) to keep Cache available — that's the trade-off, and matches the rest of the module's "the override is responsible for the contract" model. * The entry-script iteration's special-case skip for "syncterm" is gone; both loops now use wrenHasModule to skip any module another entry script's import already loaded. Same effect for syncterm, plus handles any future library-also-embedded module generically. wren_host_init shrinks to: make VM → wire callbacks → cache call/call(_) handles → set ownership → glob → run embeds → run user scripts. No bootstrap, no eager class capture, no Cache injection, no name-specific skip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  608. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/console.wren diff
    SyncTERM: Wren console — Enter on blank line is a no-op handleLine_ now short-circuits when the input trims to empty: still echo the prompt+line for visual feedback (so Enter advances to a fresh prompt row, matching shell behavior) but skip the REPL.eval call. Without this, Wren's compiler errors on empty source with "Expected expression" and the message lands in the log every time the user presses Enter on a clean line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  609. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/connected.wren diff
    src/syncterm/scripts/console.wren diff
    src/syncterm/wren_host.c diff
    SyncTERM: load each Wren entry script into its own module Was: every embedded script (console, connected, runtests) got loaded into the shared `syncterm` module so it could see the foreign-class declarations and Wren-side helpers without an import. That forced a chunk of "load syncterm first, route user overrides of any embedded script back into the syncterm module" gymnastics in wren_host_init and load_one_script. Now: each entry script runs as its own filename-named module and pulls bindings from `syncterm` via a top-of-file import. * scripts/console.wren imports Hook, Screen, Console, Input, Clipboard, Key, REPL, LogSource, MouseEvent * scripts/connected.wren imports Hook, Conn, BBS, CTerm, ConnType, Emulation * scripts/runtests.wren was already importing what it needed. `host_load_module` becomes the single source of truth for "find a module's source" — search order is user scripts/<name>.wren, then user scripts/load/<name>.wren, then EMBEDDED_SCRIPTS. The embedded fallback returns a static pointer with onComplete == NULL so Wren skips the free. Extracted read_script_file so the loader and load_one_script share one reader. is_embedded_script_name and the "route overrides into syncterm" branch in load_one_script are gone. wren_host_init replaces the old user-override-vs-embedded-fallback ladder with one bootstrap line — `wrenInterpret(vm, "_bootstrap", "import \"syncterm\" for Cell, Cells, KeyEvent, MouseEvent\n")` — which triggers the unified lookup and verifies the foreign classes exist before the C side caches their handles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  610. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/console.wren diff
    SyncTERM: Wren console editing — cursor + scrollback + line edits Three layered REPL improvements that had stacked up uncommitted in console.wren: * Line editing. `cursor` byte-index now tracked alongside `input`. Left/Right move it (clamped), Home/End jump to ends, Delete removes the char at the cursor, Backspace removes the char before it, printable insert (and clipboard paste) splice at the cursor, Ctrl+W kills the word ending at the cursor (tail past it stays put), and history Up/Down snap the cursor to the end of the recalled line. drawPrompt_ re-positions the conio cursor to prefix.count + cursor + 1 after painting so Left/Right/Home/End are visible. * Scrollback. PgUp/PgDn move by a viewport, Up/Down by a single display row. scrollTop == -1 is "live tail"; any other value pins the row at the top of the viewport. renderAllRows_ flattens the log into a list of display rows using the same chain-on-same- row rule as emitEntry_; paintViewport_ does the full repaint and scrollViewportUp1_/Down1_ do incremental single-row scrolls via Screen.moveRect. Submitting or typing a printable rejoins live mode; Ctrl+L clears scroll state too. * Window bounds. Force Screen.window.bounds to the full screen for the console's lifetime — a BBS may have set a DEC scrolling region; the console expects the whole screen. Original bounds are restored on exit alongside the saved screen. Plus the /? help is regrouped into editing / history / scroll / exit so the new keys are discoverable, and the SPDX header was stripped (per the project's per-tree license policy). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  611. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    Mention Wren.
  612. Deucе
    Mon Apr 27 2026 16:09:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/load/wrentest.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: Wren foreign-type tags + output-dispatch re-entry fix Three related changes shaken out by the wrentest suite: * Tag every wren_* foreign struct with a leading `enum syncterm_wren_foreign type` so foreign-method bodies can identify which foreign they hold (wrenGetSlotType only reports WREN_TYPE_FOREIGN, not the class). `slot_foreign_type()` reads the tag; `fn_Screen_writeRect` uses it to fast-path a Cells argument straight to ciolib_vmem_puttext with no copy or iteration, while still accepting a List of Cell. * Guard against wrenCall re-entry from inside fn_Conn_send / fn_Conn_sendRaw. Calling wrenCall while a foreign method is on the stack resets vm->fiber->stackTop (wren_vm.c:1464) and corrupts the outer fiber, producing unbounded recursion or SIGSEGV. fn_Conn_send now brackets conn_send with state.output_dispatching++/--, so wren_host_dispatch_output short-circuits for sends that originated inside Wren. C-side conn_send (terminal responses, modem keepalives) still fires hooks normally. * Drop wren_cell.parent (a raw pointer to a sibling Cells' foreign data, valid only as long as the Cells stayed in slot 0) and replace with parent_buf — the malloc'd vmem_cell buffer pointer captured at view creation, kept alive by the existing parent_handle pin. No raw foreign-data pointer held across VM calls, no scratch slot needed for cell_data(). Plus test fixes in wrentest.wren: rect-roundtrip uses the natural readRect → mutate → writeRect(cells) shape; sentinel filter key 0xFFFF -> 0xFE00 (ciolib's ungetch / rip_getch reassembly only re-composes a 16-bit key when the low byte is 0x00 or 0xe0); new T05 step issues `sleep 1 && printf` so the Hook.every wall-clock assertion is no longer racy on local PTYs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  613. Deucе
    Mon Apr 27 2026 16:09:04 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    SyncTERM: tint Wren log indicator by entry kind (red=error, yellow=print) struct wren_log gains an error_total counter (incremented in log_append when source != WREN_LOG_PRINT) and the seen-snapshot records both totals, so wren_host_log_unread_error() can answer "is the unread set tainted by an error?" without walking the ring. The status-bar bug glyph at col 27 now paints bright red ‼ when at least one unread entry is a compile/runtime/stack-frame error, bright yellow ‼ when only print output is unread, blank when caught up. A new redraw bit (0x400) tracks the error-vs-not transition so the tint changes are picked up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  614. Deucе
    Mon Apr 27 2026 16:09:04 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/syncterm.wren diff
    Modified Files:

    src/syncterm/re1/parse.y diff
    src/syncterm/re1/regexp.h diff
    src/syncterm/re1/y.tab.c diff
    src/syncterm/scripts/runtests.wren diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    SyncTERM: extract syncterm Wren module to scripts/syncterm.wren The 740-line foreign-class-declaration string literal in wren_host.c moves to scripts/syncterm.wren as plain Wren source. Now editable with syntax highlighting, comments stay readable, no escape-quote gymnastics. The build embeds it via the existing scripts/*.wren glob alongside connected.wren and console.wren. Load order: wren_host_init() now resolves "syncterm" first (preferring a user override at scripts/syncterm.wren on disk, falling back to the embedded copy), captures the Cell/Cells/KeyEvent/MouseEvent class handles, then walks the rest of EMBEDDED_SCRIPTS[] and the user script dir — both passes skip the syncterm entry to avoid double-load. Bonus fix shaken out by the move: parse() in RE1 unconditionally prepended `.*?` to every pattern for match-anywhere semantics, which defeated the streaming dispatcher's IMPOSSIBLE-shift trim. Add parse_unanchored() that returns the user's pattern wrapped only in the implicit group-0 capture; Hook.onMatch uses it. parse() keeps its original behavior for any future buffer-shaped caller. scripts/runtests.wren needed an explicit `import "syncterm" for Hook` since it loads as its own "runtests" module (not embedded, not in the syncterm scope). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  615. Deucе
    Mon Apr 27 2026 16:09:04 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    SyncTERM: rename Input._park to Input.park_ — Wren leading-underscore is a field Wren parses identifiers with a leading underscore as private field references; foreign classes can't have fields, so the embedded \`Input\` declaration was failing to compile with "Cannot define fields in a foreign class" on both the call site and the \`foreign static _park(fiber)\` declaration. Trailing underscore matches the convention already used by Input.ungetKey_ / Input.ungetMouse_ in the same class. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  616. Deucе
    Mon Apr 27 2026 16:09:04 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/load/wrentest.wren diff
    src/syncterm/scripts/runtests.wren diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    SyncTERM: add Wren self-test suite (Alt+T runs against bash-PTY) Two scripts: - scripts/runtests.wren — Alt+T hotkey; imports the suite and kicks off WrenTest.run(). In-tree only: filtered out of the embed globs (GNUmakefile + CMakeLists.txt) so a stripped install doesn't try to import the suite at every connect. - scripts/load/wrentest.wren — the suite, loaded on demand by the module loader. Six inline binding sanity checks (Conn.connected, Conn.type, Screen.size, CTerm.x/y, Console.total grows on print, Hook.onMatch leading-.* rejection) plus two sentinel-driven shell roundtrips (literal echo, capture group) using a single Hook.onMatch dispatcher and a Hook.every watchdog. PTY-echo duplicates are guarded by a __pending-equals check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  617. Rob Swindell
    Mon Apr 27 2026 09:45:44 GMT-0700 (PDT)
    Modified Files:
    

    docs/slyedit_readme.txt diff
    exec/SlyEdit.js diff
    exec/load/slyedit_misc.js diff
    exec/slyedcfg.js diff
    Merge branch 'slyedit_upload_msg_ansi_fix' into 'master' SlyEdit - Bug fix: When uploading a message (with /UPLOAD or /UL), use the uploaded message file as-is (don't interpret attribute codes, etc.). Reported by Codefenix. See merge request main/sbbs!678
  618. Eric Oulashin
    Mon Apr 27 2026 09:07:17 GMT-0700 (PDT)
    Modified Files:
    

    docs/slyedit_readme.txt diff
    exec/SlyEdit.js diff
    exec/load/slyedit_misc.js diff
    exec/slyedcfg.js diff
    SlyEdit - Bug fix: When uploading a message (with /UPLOAD or /UL), use the uploaded message file as-is (don't interpret attribute codes, etc.). Reported by Codefenix.
  619. Deucе
    Mon Apr 27 2026 07:32:03 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    I meant ‼, not ¶
  620. Deucе
    Mon Apr 27 2026 07:20:54 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Wren.adoc diff
    src/syncterm/re1/parse.y diff
    src/syncterm/re1/pike.c diff
    src/syncterm/re1/regexp.h diff
    src/syncterm/re1/y.tab.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: add Hook.onMatch streaming-regex input hooks via RE1 `Hook.onMatch(pattern, fn)` registers a regex against the inbound byte stream. Implemented as a streaming Pike VM on top of the vendored RE1 (BSD-3-Clause): each byte feeds one VM step returning tri-state {MATCH, IMPOSSIBLE, PENDING}; the dispatcher trims a byte off the front on IMPOSSIBLE, fires fn and trims through the match on MATCH, waits on PENDING. Filtered, unfiltered, byte-equality, and regex input hooks all share the same per-event array, preserving registration order across all forms. RE1 changes are additive: new pikevm_new/free/start/step/match symbols in pike.c reuse the existing file-local helpers; original pikevm() stays untouched. fatal() now consults a host-installed hook so RE1 syntax errors longjmp back to the Wren binding instead of exit(2)'ing SyncTERM. Patterns whose leading construct can match without consuming a byte (*, +, ? at the start) are rejected at registration time — without that constraint the IMPOSSIBLE-shift trick can't trim the buffer and matches would silently start being dropped past the 4 KB cap. Build wires re1/{compile,pike,sub,y.tab}.c with -w to confine the pedagogical-code warnings to RE1's TUs. Wren.adoc grows a regex section documenting the limited grammar (no backslash escapes, no character classes, no anchors), the leftmost-first-completing match semantics, and the leading-anchor constraint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  621. Deucе
    Mon Apr 27 2026 07:20:54 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/re1/LICENSE diff
    src/syncterm/re1/Makefile diff
    src/syncterm/re1/backtrack.c diff
    src/syncterm/re1/compile.c diff
    src/syncterm/re1/main.c diff
    src/syncterm/re1/parse.y diff
    src/syncterm/re1/pike.c diff
    src/syncterm/re1/recursive.c diff
    src/syncterm/re1/regexp.h diff
    src/syncterm/re1/sub.c diff
    src/syncterm/re1/thompson.c diff
    src/syncterm/re1/y.output diff
    src/syncterm/re1/y.tab.c diff
    Pull in Russ Cox's RE1 code
  622. Deucе
    Mon Apr 27 2026 07:20:53 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/connected.wren diff
    Use filtered-key binkding for Alt-L
  623. Deucе
    Mon Apr 27 2026 06:15:16 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host_internal.h diff
    SyncTERM: add filtered Wren hook variants (C-side match) Hook.onKey(key, fn), Hook.onInput(byte, fn), Hook.onMouse(event, fn) register a callable that fires only when the event's discriminator matches. The match check is in the C dispatcher: filtered entries whose filter != value are skipped before any Wren slot is touched, so the per-event hot path stays cheap. Filtered and unfiltered hooks share one per-event array (struct wren_hook_entry { fn, filter, filtered }), preserving registration order across both forms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  624. Deucе
    Mon Apr 27 2026 05:48:00 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/syncterm.c diff
    src/syncterm/term.c diff
    SyncTERM: move Wren VM lifecycle inside doterm() doterm() now owns wren_host_init/shutdown; the caller in syncterm.c no longer brackets the call. Converted to a single-return shape via \`bool ret\` and \`goto end\` so shutdown runs on every exit path. The pre-init cterm-failure return stays a plain \`return false\` — no VM exists yet. Sets up locals (speed, ooii_mode) being in scope where the VM is initialized, so accessors for them can read live values for the entire VM lifetime. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  625. Deucе
    Mon Apr 27 2026 05:31:53 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/wren_host.c diff
    SyncTERM: add Wren module loader from scripts/load/ `import "name"` resolves to <SYNCTERM_PATH_SCRIPTS>/load/<name>.wren. Names are restricted to [A-Za-z0-9_]; an invalid name aborts the import via a synthesized Fiber.abort module body so the failure is distinguishable from "module not found." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  626. Deucе
    Mon Apr 27 2026 05:20:20 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/scripts/console.wren diff
    src/syncterm/term.c diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    SyncTERM: add Wren console "unread log" indicator to status bar Bright red CP437 ¶ at column 27 (immediately left of the SFTP queue arrows) lights up when log entries arrive while the Wren console is closed. Snapshot of wren_log_total() is taken when WrenConsole.run() exits, exposed to Wren as Console.markSeen(). Status bar consults wren_host_log_unread() each refresh; new bit 0x200 in oldbits forces a redraw on transition. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  627. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 21:35:33 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qwk.cpp diff
    Add missing CPS argument to FiTransferTime formatted text output As mentioned by-the-by in issue #1133
  628. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 21:35:33 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/msgtoqwk.cpp diff
    A more full fix for removing the @address from local mail for QWKnet nodes We needed to perform the truncation before the HEADERS.DAT file is created. Re: commit 6859427055bc4d0765b3
  629. Deucе
    Sun Apr 26 2026 21:18:13 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    SyncTERM: fix vendored Botan import path on MSVC (botan-3.lib) Botan 3.x's MSVC build produces botan-3.lib (matching libbotan-3.a on POSIX), not the bare botan.lib our CMakeLists expected. The configure + build steps succeeded, then the syncterm link failed with LNK1181 'cannot open input file vendored-botan\lib\botan.lib'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  630. Deucе
    Sun Apr 26 2026 21:08:27 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    SyncTERM: use CXX_STANDARD 20 instead of -std=c++20 for Botan TUs The per-source -std=c++20 flag is GCC/Clang syntax; MSVC silently ignored it (D9002 warning) and then errored on Botan 3's C++20 requirement. Switch to the target's CXX_STANDARD property so CMake emits the right flag for each compiler (/std:c++20 for MSVC). Set unconditionally on the syncterm target — harmless when no .cpp sources are present (OpenSSL / none crypto backends). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  631. Deucе
    Sun Apr 26 2026 21:00:37 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    src/syncterm/sftp_queue.c diff
    src/syncterm/sftp_session.h diff
    src/syncterm/wren_bind.c diff
    SyncTERM: portability fixes shaken out by post-Wren CI Four follow-ups to the Wren scripting host merge, surfaced by the gmake (macOS) and MSVC (Windows) CI runs. GNUmakefile: the Wren VM rules wrote .o files into per-subdir output trees ($(MTOBJODIR)/wren/vm/...) gated by an explicit mkdir -p. The project's convention is flat MTOBJODIR with vpath search, and that's what every other build-system step (rules.mk's mkdir, the clean rule's shallow rm) assumes. Switched to vpath %.c wren/vm wren/optional and flat $(MTOBJODIR)/wren_*.o output. Basenames are already prefixed with wren_ across both subdirs so there's no collision. Wren-private include paths (-Iwren/vm -Iwren/optional -UPREFIX) move to a target- specific CFLAGS rule covering the VM, wren_host.o, and wren_bind.o. wren_bind.c: replaced <dirent.h> with xpdev/dirwrap.h, which falls through to <dirent.h> on POSIX and provides its own opendir / readdir / closedir / struct dirent on MSVC. Added an S_ISREG shim for older Windows SDKs that ship only the _S_IF* constants. sftp_queue.c: dropped the direct <unistd.h> and <utime.h> includes in favour of xpdev/filewrap.h (covers unlink and the read / write / close family) and the already-included xpdev/genwrap.h (which selects between <utime.h> and <sys/utime.h>). sftp_session.h: added the same #undef __STDC_NO_ATOMICS__ trick conn.h already uses for MSVC. Even with /std:c17 + /experimental:c11atomics the macro stays defined because MSVC's atomic support is incomplete (no generic _Atomic with locks), and <stdatomic.h> refuses to expand otherwise. Without this every TU including sftp_session.h tripped "C atomic support is not enabled" on the Windows build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  632. Deucе
    Sun Apr 26 2026 20:04:59 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    SyncTERM: show Wren scripts directory in File Locations Adds a "Wren Scripts Directory" entry to the File Locations help dialog so users can find where to drop *.wren overrides without hunting through the manual. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  633. Deucе
    Sun Apr 26 2026 20:04:59 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/Wren.adoc diff
    Modified Files:

    src/syncterm/Manual.txt diff
    Wren Scripting Slop
  634. Deucе
    Sun Apr 26 2026 20:04:59 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/scripts/connected.wren diff
    src/syncterm/scripts/console.wren diff
    src/syncterm/wren/LICENSE diff
    src/syncterm/wren/README.md diff
    src/syncterm/wren/include/wren.h diff
    src/syncterm/wren/include/wren.hpp diff
    src/syncterm/wren/optional/wren_opt_meta.c diff
    src/syncterm/wren/optional/wren_opt_meta.h diff
    src/syncterm/wren/optional/wren_opt_meta.wren diff
    src/syncterm/wren/optional/wren_opt_meta.wren.inc diff
    src/syncterm/wren/optional/wren_opt_random.c diff
    src/syncterm/wren/optional/wren_opt_random.h diff
    src/syncterm/wren/optional/wren_opt_random.wren diff
    src/syncterm/wren/optional/wren_opt_random.wren.inc diff
    src/syncterm/wren/vm/wren_common.h diff
    src/syncterm/wren/vm/wren_compiler.c diff
    src/syncterm/wren/vm/wren_compiler.h diff
    src/syncterm/wren/vm/wren_core.c diff
    src/syncterm/wren/vm/wren_core.h diff
    src/syncterm/wren/vm/wren_core.wren diff
    src/syncterm/wren/vm/wren_core.wren.inc diff
    src/syncterm/wren/vm/wren_debug.c diff
    src/syncterm/wren/vm/wren_debug.h diff
    src/syncterm/wren/vm/wren_math.h diff
    src/syncterm/wren/vm/wren_opcodes.h diff
    src/syncterm/wren/vm/wren_primitive.c diff
    src/syncterm/wren/vm/wren_primitive.h diff
    src/syncterm/wren/vm/wren_utils.c diff
    src/syncterm/wren/vm/wren_utils.h diff
    src/syncterm/wren/vm/wren_value.c diff
    src/syncterm/wren/vm/wren_value.h diff
    src/syncterm/wren/vm/wren_vm.c diff
    src/syncterm/wren/vm/wren_vm.h diff
    src/syncterm/wren_bind.c diff
    src/syncterm/wren_embed_gen.c diff
    src/syncterm/wren_host.c diff
    src/syncterm/wren_host.h diff
    src/syncterm/wren_host_internal.h diff
    Modified Files:

    src/build/Common.gmake diff
    src/conio/cg_events.m diff
    src/conio/ciolib.h diff
    src/conio/retro.c diff
    src/conio/sdl_con.c diff
    src/conio/win32cio.c diff
    src/conio/wl_events.c diff
    src/conio/x_events.c diff
    src/syncterm/CMakeLists.txt diff
    src/syncterm/DarwinWrappers.m diff
    src/syncterm/GNUmakefile diff
    src/syncterm/conn.c diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.h diff
    src/syncterm/term.c diff
    SyncTERM: Wren scripting host Embed the Wren VM as a per-connection scripting host. Each connection spins up its own VM at start, runs scripts, and tears it down on disconnect, so per-connection isolation falls out for free. Scripts come from two sources: * Embedded — shipped in the binary. scripts/*.wren is preprocessed at build time by wren_embed_gen (a small C99 codegen tool, host-CC compiled so cross-builds work) into a name+source table compiled alongside wren_host.c. Two ship today: console.wren (the REPL) and connected.wren (Alt+L send-login, extracted from term.c). * User overrides — files placed in the platform's user-data scripts directory (XDG \$XDG_DATA_HOME on Linux/BSD; Win32, macOS, and Haiku branches in syncterm.c) override the embedded script of the same module name, or add new ones. Six dispatch points are wired into doterm() / conn_send(): keyboard, mouse, inbound bytes, outbound bytes, status text, and a periodic timer. Hooks consume or pass through; consumed events skip the rest of the pipeline. Owner thread is captured at init and non-owner dispatches short-circuit to pass-through (background SFTP and SSH writes call conn_send from worker threads). Foreign classes exposed to scripts: * Screen — global console ops (save/restore, attr, supports, font, palette, cursor, videoFlags, color, hyperlinkId, window scope). * Input — event-driven: next / next(ms) / poll return a KeyEvent or MouseEvent foreign object; nextEvent parks the calling Wren fiber and resumes it on the next event, giving scripts modal input without busy-spinning. While a fiber is parked, the remote-byte pump is gated off so server output can't paint over a dialog. Key.* constants (escape, f1-f12, shiftTab, mouse, ...) come from CIO_KEY_* in conio/ciolib.h; Esc-by-scancode is normalized to 0x001B in the KeyEvent ctor so scripts only ever see one Esc. Input.unget(ev) routes by event type. * Clipboard — text getter/setter. * CTerm — read-only window into cterm state (cursor, origin, margins, color, fonts, scrollback geometry, mode flags), with ExtAttr / LastColumnFlag bitfield-snapshot classes and LogMode / StatusDisplay / Emulation / Codepage enum classes. * Conn — pending/queued/peek/recv/send/sendRaw. send() routes through conn_send so telnet IAC escaping and onOutput hooks fire; sendRaw is the bypass. peek/recv clamp to the actual inbuf size. * BBS — read-only struct bbslist getters (~30 fields) plus password / syspass and ConnType / FlowControl enum classes. * Cell / Cells / Font / Hyperlinks — vmem_gettext / puttext bindings. Cell wraps struct vmem_cell with separate palette/rgb getters and setters that preserve bits 24-30 of fg/bg; ch round-trips through CP437. Hyperlinks is Map-like with [id], containsKey, add, params. Font has named built-in indices plus runtime name(i) / available(i). * Cache — opaque Directory + File pair with strict 1..64-char [A-Za-z0-9._-] filename policy (rejects leading dot/dash, trailing dot, "..", Windows reserved device names). File covers open/close/read/write/delete/seek with deleted-handle poisoning. * Console — ring-buffered print/error log indexed by monotonic sequence so incremental rendering survives ring eviction. * Hook — registration API for the six dispatch events. REPL: * Ctrl+\` opens an immediate-mode REPL (console.wren). Compiles user input via wrenCompileSource directly so vars persist across submissions; runtime aborts caught with Fiber.try() and walked for a real stack trace. Compile-time errors that only mean "input wasn't an expression" are filtered through a side-log so expression-form printing works without dumping the noise. * Expression results print quoted with C-style escapes — "7" the string and 7 the number are visibly distinct. * Module-aware /in <module> moves the eval target. All embedded scripts (and any user override matching an embedded basename) share the "syncterm" module so console.wren can use Screen / Input / Cell / etc. directly and the REPL can inspect anything those scripts define. Pure user scripts keep file-per-module isolation. * /quit, /help, command history (Up/Down with prefix filter when text was typed first), Ctrl+W backward-kill-word, middle-click paste (LF-split, line-by-line submit), Alt+drag handoff to the existing mousedrag select-and-copy. Two pre-existing key-write bugs surfaced by adding the Ctrl+\` keytable entry (CIO_KEY_WREN_CONSOLE = 0x29E0) and are fixed here: both X11 and Wayland used "low byte non-zero -> 1 byte" as a stand-in for "ASCII", which fails for synthetic keys with both bytes set (low = 0xE0 marker, high = scancode). Aligned with cg_cio.m / win32gdi.c / sdl_con.c: write 2 bytes whenever the high byte is non-zero. GNUmakefile uses per-pattern -UPREFIX to silence Wren's PREFIX function-like macro colliding with SYNCTERM_PATH_PREFIX. The Wren VM (wren/) is vendored verbatim from the upstream Wren repository, MIT-licensed; LICENSE and README are checked in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  635. Deucе
    Sun Apr 26 2026 20:04:59 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    SyncTERM: enable MSVC C11 atomics for the syncterm target MSVC's <stdatomic.h> rejects compilation with #error "C atomic support is not enabled" unless built with /std:c11+ AND /experimental:c11atomics. The DeuceSSH sub-build sets both, but the SyncTERM target itself never did, so bbslist.c/conn.c/etc. failed in CI as soon as conn.h pulled in <stdatomic.h>. Set C_STANDARD 17 on the syncterm target and pass /experimental:c11atomics under MSVC. Bump cmake_minimum_required to 3.22 — C_STANDARD 17 was added in 3.21, and 3.5 was already tripping a deprecation warning under the CI's CMake 3.31. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  636. Deucе
    Sun Apr 26 2026 20:04:59 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Info.plist diff
    src/syncterm/Manual.txt diff
    src/syncterm/PackageInfo.in diff
    src/syncterm/dpkg-control.in diff
    src/syncterm/haiku.rdef diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.rc diff
    Downgrade this dumpster fire to Alpha.
  637. Deucе
    Sun Apr 26 2026 20:04:59 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    SyncTERM: redraw Prestel double-height bottom on top-row content change When a Prestel double-height "top" row is overwritten with new content without the DOUBLE_HEIGHT bit toggling (e.g., a frame swap from "Welcome" to "Sign In"), the bottom row's own vmem cells stay unchanged. The per-cell diff in update_from_vmem then skipped the bottom row, leaving it showing the bottom-half pixels of the previous top row's glyph. Fix at the write site in bitmap_vmem_puttext_locked: when a cell with the DH bit set has its content actually changed, mark the bitmap_drawn entry one row below dirty (CIOLIB_BG_DIRTY) so the next update redraws it. May over-invalidate one row when the cell turns out to be a Prestel bottom rather than a top, but the dirty flag does not propagate further. Fixes ticket 243. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  638. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 18:28:21 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/ciolib.c diff
    src/conio/win32gdi.c diff
    Include gdi_kbwait() in conio only if targeting >= Windows 8 This allows us to build Synchronet UIFC utils that work on Windows 7, at least for a little while still. See commit 93d46cb6ef33f1a0613aa5f57a for details.
  639. Deucе
    Sun Apr 26 2026 18:25:29 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-lang.h diff
    src/ssh/deucessh-mac.h diff
    src/ssh/docs/audit-rules.md diff
    DeuceSSH: drop static_assert(!offsetof(...)) — not valid C17 ISO C does not classify offsetof()'s standard expansion ((size_t)&(((T*)0)->m)) as a constant expression, so MSVC in C mode rejects it inside static_assert (error C2057). GCC/Clang accept it as an extension. Remove the six "next must be at offset 0" assertions from the module headers and the now-unused <assert.h> includes. The "must be first" comment on each next field stays as documentation, and the generic-traversal paths are exercised by the test suite. Also drop the audit-rules.md bullet that referenced the assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  640. Deucе
    Sun Apr 26 2026 18:16:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    SyncTERM: query the actual device for supported baud rates For CONN_TYPE_SERIAL/SERIAL_NORTS, briefly open item->addr and call comGetBaudRates() to populate the Comm Rate picker with rates the OS and hardware actually support. Falls back to the static rate_names[] list on any failure (open fails, query returns 0, etc.). TCP and modem connections continue to use the static list, since for them the field is an emulated display throttle, not a hardware rate.
  641. Deucе
    Sun Apr 26 2026 18:06:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-lang.h diff
    src/ssh/deucessh-mac.h diff
    src/ssh/kex/sntrup761.c diff
    DeuceSSH: fix MSVC build (C17, static_assert, sntrup761 VLAs) - Bump cmake_minimum_required to 3.22 so CMP0128 is NEW and MSVC honors C_STANDARD 17 (emits /std:c17). - Add #include <assert.h> to the six module headers that use static_assert; MSVC's static_assert is a macro defined there. - Replace the three remaining VLA sites in sntrup761.c with fixed-size arrays sized to their constant maxima (p for Encode/Decode where len <= p; PUBLICKEYBYTES for Hash_prefix where that's the largest inlen across all callers). MSVC does not support VLAs in C mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  642. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 17:02:35 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/msgtoqwk.cpp diff
    Remove "@address" part of QWK to-user for local mail in QWKnet node packets I think'll resolve the issue of QWKnet node accounts receiving email from gitlab@synchro.net (e.g. registration verification messages).
  643. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 16:44:09 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/netmail.cpp diff
    src/sbbs3/qwktomsg.cpp diff
    src/sbbs3/sbbs.h diff
    Use uint (instead of uchar) for `fromhub` argument For those systems with > 255 QWK network hubs. :-)
  644. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 14:46:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/prntfile.cpp diff
    src/sbbs3/sbbs.h diff
    printfile() now handles files >= 2GiB
  645. Rob Swindell (on Windows 11)
    Sun Apr 26 2026 13:22:52 GMT-0700 (PDT)
    Modified Files:
    

    install/upgrade.iss diff
    Use the jsexec -c option to specify the install-to CTRL directory Address the issue demonstrated by JasHud in https://www.veed.io/view/ad2b464a-af68-43c3-96eb-8802f0ad396d where the SBBSCTRL environment variable was not set before the upgrade. This does mean that the "ctrl" directory must be a subdirectory of the installation target directory (e.g. "c:\synchronet\ctrl") but there's other places in this script (e.g. the text.dat target location) that make this same assumption: so any really edgy sysops that don't have their sbbs directories with a common parent will need to deal. The ultimate fix would be to prompt the sysop for the location to the ctrl directory instead (not the parent), defaulting to the SBBSCTRL environment variable and then discover all the other directories (exec, text, etc.) by reading ctrl/*.ini files. I'm not doing that today.
  646. Rob Swindell
    Sun Apr 26 2026 12:30:45 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/synchess/synchess.js diff
    Merge branch 'fix_synchess_exit' into 'master' js.exit isn't a thing, Claude. See merge request main/sbbs!677
  647. Nigel Reed
    Sun Apr 26 2026 12:30:45 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/synchess/synchess.js diff
    js.exit isn't a thing, Claude.
  648. Rob Swindell (on Debian Linux)
    Sun Apr 26 2026 00:41:41 GMT-0700 (PDT)
    Modified Files:
    

    src/sexpots/sexpots.c diff
    src/tone/tone.c diff
    src/vdmodem/vdmodem.c diff
    Ran uncrustify - whitespace and paren changes only
  649. Rob Swindell (on Debian Linux)
    Sun Apr 26 2026 00:32:07 GMT-0700 (PDT)
    Modified Files:
    

    src/comio/comio.c diff
    src/comio/comio.h diff
    src/comio/comio_nix.c diff
    src/comio/comio_win32.c diff
    Ran uncrustify - whitespace and paren changes only
  650. Rob Swindell (on Windows 11)
    Sat Apr 25 2026 23:56:26 GMT-0700 (PDT)
    Modified Files:
    

    src/comio/comio.h diff
    src/comio/comio_nix.c diff
    src/comio/comio_win32.c diff
    comio: add comGetBaudRates() to query settable baud rates for a COM port Win32 implementation uses GetCommProperties()/dwSettableBaud bitmask to reflect what the device driver actually supports; nix implementation enumerates the compile-time B* macro set (same list used by rate_to_macro). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  651. Rob Swindell (on Windows 11)
    Sat Apr 25 2026 23:06:57 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    Add /experimental:c11atomics for MSVC compile command-line This is needed.
  652. Rob Swindell (on Windows 11)
    Sat Apr 25 2026 22:06:18 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    Need to specify make, not nmake to fix build issue on Windows
  653. Deucе
    Sat Apr 25 2026 15:32:50 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp.h diff
    sftp: brace SFTP*_OUTCOME_DECL union initializer The {0} initializer for the union containing struct sftp[cs]_outcome triggers clang's -Wmissing-braces in consumers (e.g. sbbs3/main.cpp). Use {{0}} so the inner struct gets its own braces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  654. Deucе
    Sat Apr 25 2026 15:31:59 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sftp.cpp diff
    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_common.c diff
    src/sftp/sftp_server.c diff
    src/syncterm/sftp_browser.c diff
    sftp: add pubdir@syncterm.net extension; sbbs3 advertises /files Server-to-client one-shot string carried in the SFTP VERSION extension list. Client advertises name/"1" in INIT; server replies name/<path> in VERSION when state->pubdir is set. Path is captured into the client's private state and exposed via sftpc_get_pubdir(). The asymmetric data field is spec-legal (extension-data is opaque bytes per draft-ietf-secsh-filexfer-02) but doesn't fit the existing table-driven negotiation, so the server masks SFTP_EXT_PUBDIR off the standard append_extensions() emit and appends name+path manually, and the client recognises the entry by name and captures the data explicitly (extension_match() can't fire when the data is the path). sbbs3/sftp.cpp sets pubdir = SLASH_FILES so SFTP clients land in the public filebase root. syncterm's SFTP browser now prefers the advertised pubdir as its initial cwd, falling back to realpath(".") then "/" for servers that don't offer the extension. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  655. Deucе
    Sat Apr 25 2026 14:52:41 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/wl_events.c diff
    conio/wayland: fix key repeat sending numpad digits for arrows bug ticket 242: holding arrows under Wayland produced ASCII digits (Left=4, Right=6, Up=8, Down=2, etc.) once key-repeat began. send_scancode() stored the remapped BIOS scancode in repeat_key, but handle_key_repeat() feeds repeat_key back into send_scancode() as if it were an evdev code. BIOS scancode 75 (Left) collides with evdev KEY_KP4, so the xkb path returned '4' on repeat — and the same coincidence covers all the other reported keys. Store the original evdev_key in repeat_key consistently, and match release events against evdev_key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  656. Deucе
    Sat Apr 25 2026 04:51:43 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/syncterm.c diff
    syncterm: runtime-init popup_q_mutex for Win32 portability PTHREAD_MUTEX_INITIALIZER isn't a constant expression under xpdev's Win32 pthread wrapper, so the static initializer broke the mingw64 build. Initialize the mutex with pthread_mutex_init() at the top of main() before any thread that could post to the popup queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  657. Deucе
    Sat Apr 25 2026 04:31:46 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sftp.cpp diff
    src/syncterm/sftp_queue.c diff
    sftp consumers: adopt static/borrowed sftp_str_t variants Replace heap-roundtrip allocations in the SFTP consumers with the new caller-provided struct + static/memstatic helpers. Every site that was sftp_strdup/memdup'ing a string only to hand it to the library (which immediately copies it into a tx packet or extension entry) now wraps the source bytes directly. sbbs3/sftp.cpp: - get_lib_attrs / get_dir_attrs / get_filebase_attrs: convert all six sftp_strdup + sftp_memdup pairs feeding sftp_fattr_add_ext to sftp_strstatic / sftp_memstatic. Drive-by fix: the heap forms were leaked in every call (sftp_fattr_add_ext deep-copies its args, so the caller's strs become orphans the moment the call returns). Static wrappers eliminate both the allocation and the leak. - sftp_open / sftp_opendir handle assembly: replace sftp_asprintf("%u", ...) and sftp_asprintf("D:%u", ...) with snprintf into a stack buffer + sftp_memstatic. - extdesc reply: sftp_strdup(ed) -> sftp_strstatic. ed is a smb library buffer that's stable through smb_freefilemem. syncterm/sftp_queue.c: - per-chunk upload loop: replace sftp_memdup of the read buffer with sftp_memstatic. This fires once per ~32KB chunk during file uploads, so the saved alloc/copy/free is the highest- volume win in the audit. ssh.c's authorized_keys append site is left as sftp_asprintf for now — the formatted line is short, the call runs once per session when sftp_public_key is set, and the string composition genuinely needs heap storage of unknown length. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  658. Deucе
    Sat Apr 25 2026 04:23:39 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp.h diff
    src/sftp/sftp_str.c diff
    sftp: support static and borrowed sftp_str_t variants Generalize sftp_string to three lifetime modes selected at construction: - heap: library single-allocates header+bytes; a release callback on the struct frees the whole thing in free_sftp_str() (existing behavior, preserved). - static: caller provides both struct and bytes with program/scope lifetime; release is NULL and free_sftp_str() is a no-op. - borrowed: caller provides the struct, owns the bytes externally, and supplies a release callback (refcount, mmap teardown, etc.) that fires when the library is done with the data. NULL release is supported and functionally equivalent to the static form. To make this work, c_str moves from a flexible array member to a plain uint8_t pointer. Consumers that read ->c_str and ->len keep working unchanged; the only internal site that depended on the FAM layout (sftp_alloc_str's offsetof) now uses sizeof(struct) and points c_str just past the header. New helpers: void sftp_strstatic(struct sftp_string *out, const char *str); void sftp_memstatic(struct sftp_string *out, const uint8_t *buf, uint32_t len); void sftp_strborrow(struct sftp_string *out, const char *str, void (*release)(struct sftp_string *), void *cbdata); void sftp_memborrow(struct sftp_string *out, const uint8_t *buf, uint32_t len, void (*release)(struct sftp_string *), void *cbdata); Lets consumers wrap their own buffers in an sftp_str_t without the strdup/memdup round-trip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  659. Deucе
    Sat Apr 25 2026 04:10:55 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    syncterm: simplify SSH auth flow; PuTTY-style KBI password autofill Replace the auth state machine in ssh_connect() with a flat, strongest-to-weakest order driven by the RFC 4252 "none" probe: 1. probe; if "none" was accepted we're done 2. publickey (if advertised) 3. password (if advertised — stored value first, then up to 3 prompts) 4. keyboard-interactive (if advertised) Each method is gated on the server's advertised list, so users aren't prompted for credentials the server would reject regardless (e.g. an OpenSSH target with PasswordAuthentication=no no longer cycles three dead password prompts before falling through to KBI). Also fixes a latent bug in the SSHNA path that unconditionally set auth_rc=0 on any non-error return from dssh_auth_get_methods, even when the response was "methods available, none-auth not accepted". In kbi_prompt_cb: - Auto-fill the saved password when the server sends exactly the literal prompt "Password: " (PuTTY-style: single prompt, echo off, literal text match). This avoids burning credentials on 2FA "Passcode:" prompts, GPG-style "Passphrase:" prompts, password- change flows, or anything else dressed up to look password-like. The fire-once latch ensures a wrong saved password doesn't loop; subsequent prompts fall through to the user. - Strip a trailing ':' from the server's prompt before passing it to uifcinput(), since uifc.input always appends ':' itself and "Password:" would otherwise render as "Password::". Drops the speculative "Cryptlib mishandles failed ssh-ed25519 publickey probe" gate — the comment was likely a debugging artifact, not a verified server behavior, and gating production logic on an unreproducible claim made the flow harder to reason about than the risk justified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  660. Deucе
    Sat Apr 25 2026 03:34:50 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.h diff
    src/syncterm/term.c diff
    syncterm: cross-thread popup queue; wire SSH_MSG_DEBUG callback Add popup_queue_{post,drain}() to syncterm.c with declarations in syncterm.h. Background threads can post (title, body) pairs from any thread; doterm()'s main loop calls drain() at the top of each iteration and displays them via uifcmsg() on the UI thread. Same mechanism can serve any future cross-thread popup source (SFTP errors, global-request callbacks, etc.). Wire ssh_debug_cb for SSH_MSG_DEBUG (RFC 4253 11.3): logs every debug line to log_fp as "SSH display ..." or "SSH debug ..." and posts an always_display message to the popup queue when bbslist isn't suppressing popups. Skip dssh_unimplemented_cb for now -- the seq number it carries can't be correlated to anything actionable without a DSSH-side send-counter API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  661. Deucе
    Sat Apr 25 2026 03:18:46 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    syncterm: identify in SSH banner, add RSA-SHA2-512, timeout, cleanse Four small additions in ssh.c using DeuceSSH APIs we hadn't wired up: - build_ssh_software_version() derives an RFC 4253 software-version token from syncterm_version (e.g. "SyncTERM_1.9b") and registers it via dssh_transport_set_version() so server admins can identify SyncTERM in their logs. The build flavor (Debug suffix) is deliberately stripped because the version banner is sent before encryption is established. - dssh_register_rsa_sha2_512() rounds out the host-key set; we already advertised SHA-256. Costs nothing and lets us interoperate with servers that prefer or require the SHA-512 variant. - dssh_session_set_timeout(60000) caps the library's peer-response waits at 60s. The default is 75s; the tighter bound surfaces hung handshakes before users assume SyncTERM has frozen. - dssh_cleanse() wipes the local password buffer in ssh_connect() after the auth attempts finish and the kbd-interactive answer buffer in kbi_prompt_cb(). Prevents secrets from lingering in stack slots that the compiler might otherwise leave intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  662. Deucе
    Sat Apr 25 2026 03:08:08 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rlogin.c diff
    src/syncterm/ssh.c diff
    syncterm: send full pty-req mode set; default RLogin to raw ssh.c's pty-req previously sent an empty terminal-modes blob. RFC 4254 section 8 says the client SHOULD send any modes it knows about, so emit the full set: standard control characters, 8-bit clean (ISTRIP=0, CS8=1), no software flow control (IXON/IXOFF=0), server-side LF-to-CRLF (ONLCR=1), and friendly cooked-mode echo defaults for the times the user lands at a real shell. ATASCII and PETSCII emulations override VERASE/VEOL. For consistency with the SSH IXON=0 advertisement, RLogin now defaults to raw mode (binary_mode=true) at connect: DC1/DC3 in user input pass straight to the server instead of being consumed locally. A server can still request cooked mode via OOB 0x20 if it wants the legacy behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  663. Deucе
    Sat Apr 25 2026 02:34:25 GMT-0700 (PDT)
    Added Files:
    

    src/sftp/sftp_outcome.c diff
    Modified Files:

    src/sbbs3/main.cpp diff
    src/sbbs3/sftp.cpp diff
    src/sftp/sftp.c diff
    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_server.c diff
    src/syncterm/sftp_browser.c diff
    src/syncterm/sftp_queue.c diff
    src/syncterm/ssh.c diff
    sftp: replace last_error TLS hack with sftp{c,s}_outcome The pthread_key_t TLS for sftpc_get_err was removed for portability in 2710c20e54, but that exposed a real race the TLS had been masking: the three SyncTERM user threads (browser, uploader, downloader) sharing one sftpc_state_t can clobber each other's last_error between op return and sftpc_get_err call. Reshape the API so per-op state lives in caller-supplied storage and shared scalar state on the connection goes away. New struct sftp{c,s}_outcome carries: - err code (sftp_err_code_t -- flat enum of specific lib failure modes; SFTP_ERR_OK on success), valid when op returns false - result code (SSH_FX_* from server reply, client-side only), valid when op returns true - estr text accumulator with file:line prefixes per record, plus server-supplied SSH_FXP_STATUS messages as `Reply: "..."` lines Bool contract: true - op completed; act on result. err/estr are diagnostic only. false - op didn't complete; result undefined. Switch on err for retry decisions; show estr to the user. Stack-allocated via SFTPC_OUTCOME_DECL(name, textsz) / SFTPS_OUTCOME_DECL macros. Pass NULL for "don't care", or sz=0 for "codes but no text". A single internal outcome_record_impl backs both public record entry points so formatting/accumulation lives in one place. sftpc_outcome_reply formats server-supplied SSH_FXP_STATUS text as `Reply: "msg" (lang)` lines, distinguishable from file:line-prefixed lib-internal records. Server side gets sftps_outcome too (no result field -- server supplies the SSH_FX_* code rather than receiving it). Threading covers sftps_recv, sftps_send_packet/error/handle/data/name/attrs/extended_reply and the static dispatch helpers. sbbs3/main.cpp's sftps_recv sites get real outcomes that log via lprintf on failure; sbbs3/sftp.cpp's ~100 sftps_send_* sites pass nullptr for now (existing lprintf plumbing in the lib already logs the immediate context). SyncTERM consumers (ssh.c, sftp_browser.c, sftp_queue.c) take real outcomes. The queue worker uses sftp_err_is_transient() to choose between SFTP_JOB_QUEUED (retryable) and SFTP_JOB_FAILED for op-level failures. ssh.c's SSH_FX_EOF handling correctly lives on the post-true branch. set_thread_err / get_thread_err / sftpc_get_err / state->last_error all gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  664. Deucе
    Sat Apr 25 2026 01:46:11 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_server.c diff
    src/syncterm/sftp_browser.c diff
    sftp: drop speculative public API surface with no callers Eight client ops (sftpc_lstat/fstat/fsetstat/remove/rmdir/mkdir/rename /reclaim) and the sftpc_debug_last_reply_type accessor were declared in sftp.h and defined in sftp_client.c with no callers anywhere in the tree (verified: src/sftp, src/syncterm, src/sbbs3). They came in with f193bd5695 as speculative surface; nothing uses them today, and the debug accessor only existed to prepend a reply-type line to a single error string in sftp_browser.c. sftps_reclaim is internal-only (one self-caller in sftp_server.c); make it static rather than exposing it as public API. Also drop the dead state->last_reply_type field, its two writes in sftp_client.c, and the do_one_path helper that was only used by the removed sftpc_remove/rmdir. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  665. Deucе
    Sat Apr 25 2026 01:41:37 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    Add the prototype SFTP client
  666. Deucе
    Fri Apr 24 2026 23:23:36 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/sftp_browser.c diff
    syncterm: include datewrap.h in sftp_browser.c for localtime_r MSVC's CRT doesn't ship POSIX localtime_r. xpdev/datewrap.h is the project's portable shim for it (used the same way in webget.c:7). sftp_browser.c calls localtime_r in format_mtime() but never pulled the header in, so the MSVC build fails with C2065 / implicit-decl. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  667. Deucе
    Fri Apr 24 2026 23:19:00 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    syncterm: hoist sftp_up/dn_active out of WITHOUT_DEUCESSH guard term.c references sftp_up_active / sftp_dn_active again at the col-28/29 status-bar update (lines 581/591), outside the activity-poll block that e8f12ae5ea wrapped in #ifndef WITHOUT_DEUCESSH. Move the declarations above the guard so the variables are still in scope (and zero, so the status bar shows idle) when SSH is compiled out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  668. Deucе
    Fri Apr 24 2026 23:19:00 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    We define our own M_PI now.
  669. Deucе
    Fri Apr 24 2026 23:12:20 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    syncterm: guard SFTP UI calls in term.c with WITHOUT_DEUCESSH term.c referenced sftp_browser_run, sftp_degraded_run, sftp_queue_activity, and sftp_queue_screen_run unconditionally, but their .c files (sftp_browser.c, sftp_degraded.c, sftp_queue.c, sftp_queue_screen.c) are gated behind NOT WITHOUT_DEUCESSH in CMakeLists.txt:319. When the MSVC builder auto-flips WITHOUT_DEUCESSH ON (vendored Botan unavailable), the link fails with four LNK2019s. Wrap the four call sites and the four sftp_*.h includes in #ifndef WITHOUT_DEUCESSH, matching the pattern used in syncterm.c, conn.c, and bbslist.c. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  670. Deucе
    Fri Apr 24 2026 23:00:36 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp_client.c diff
    sftp: drop pthread_key TLS for last-error, store on state struct pthread_key_create / pthread_setspecific / pthread_getspecific are not shimmed by xpdev/threadwrap.h (only pthread_mutex_* is), so sftp_client.c did not build on Win32 or any other non-pthreads target. Replace the TLS slot with a plain uint32_t last_error field on struct sftp_client_state; state->mtx already serializes every op, so no new lock is needed. Public API (sftpc_get_err and the bool returns from sftpc_open/read/write/etc.) is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  671. Deucе
    Fri Apr 24 2026 15:19:32 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/CMakeLists.txt diff
    sftp: fix CMake build after single-TU reorg CMakeLists.txt still listed sftp_attr.c, sftp_client.c, sftp_pkt.c, sftp_server.c, sftp_str.c as standalone sources and referenced the deleted sftp_internal.h / sftp_static.h headers. Post-reorg only sftp.c is a real translation unit; it #includes the others. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  672. Deucе
    Fri Apr 24 2026 14:18:51 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sftp.cpp diff
    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_common.c diff
    src/sftp/sftp_server.c diff
    src/syncterm/sftp_browser.c diff
    src/syncterm/sftp_queue.c diff
    SFTP: sha1s/md5s/descs extensions, hash-verified compare, history drop-on-connect Library: two new handshake extensions (sha1s@syncterm.net, md5s@syncterm.net) carry the raw binary digest as a file-attribute extension; a third (descs@syncterm.net) is an on-demand SSH_FXP_EXTENDED query that takes a path and returns the extdesc string as SSH_FXP_EXTENDED_REPLY. Adds sftpc_descs(), sftps_send_extended_reply(), and sftp_rx_get_string() for the server-side extended callback to parse args. Also fixes a missing break in the SSH_FXP_EXTENDED dispatch case. Server (sbbs3/sftp.cpp): implements the descs handler — walks the path through path_map, smb_findfile + smb_getfile(file_detail_extdesc), sends back file.extdesc. Wires sftp_state->extended. Also fixes a dormant C++ bug in path_map(const char*, ...) — it tried to delegate by instantiating a temporary in the body, which left the real object with result_ = MAP_FAILED. Delegation now goes through the member init list. SyncTERM browser: when size matches, the local-compare now verifies via SHA-1 or MD5 (preferring SHA-1) if the server advertised a hash for the file; otherwise falls back to mtime. F2 on a highlighted file queries the descs extension and displays the extended description in a scrollable popup. Queue history is dropped at connect time — DONE / FAILED / CANCELLED rows from the previous session aren't reloaded, so each session starts with just its still-pending transfers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  673. Deucе
    Fri Apr 24 2026 13:01:17 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/sftp_degraded.c diff
    src/syncterm/sftp_degraded.h diff
    src/syncterm/sftp_queue_screen.c diff
    src/syncterm/sftp_queue_screen.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/sftp_browser.c diff
    src/syncterm/sftp_queue.c diff
    src/syncterm/sftp_queue.h diff
    src/syncterm/ssh.c diff
    src/syncterm/term.c diff
    SyncTERM: SFTP queue screen, per-BBS persistence, degraded mode Queue persistence: each BBS's cache dir now holds sftp_queue.ini. On connect, sftp_queue_attach_bbs() reloads any pending jobs; ACTIVE rows come back as QUEUED so interrupted transfers restart cleanly. Every state transition atomically rewrites the file. Alt-Q opens the queue screen: live per-job progress (done/total %), sorted ACTIVE first, then QUEUED then terminal rows. Del cancels the highlighted job; Esc closes. Uses WIN_DYN + gen_ctr polling so the view updates as workers tick but the bar only repaints on real change. Browser keys finalised around uifc's built-in letter-jump: Enter downloads a file (and stays in the list), Ins uploads, Del cancels. Directories descend on Enter as before. Shell-dead degraded mode: if ssh_chan closes while the SFTP queue still has work, ssh_input/output threads now leave conn_api.terminate clear so sftp_send and the recv thread keep flowing. term.c's disconnect branch runs sftp_degraded_run() first — a small overlay that blocks until the queue drains or the user chooses "Hang up now" / Esc. Queued-but-unfinished jobs are preserved on disk for the next connection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  674. Deucе
    Fri Apr 24 2026 12:31:23 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sftp.cpp diff
    Add SFTP_EXT_LNAME support.
  675. Deucе
    Fri Apr 24 2026 12:26:26 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/sftp_queue.c diff
    src/syncterm/sftp_queue.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/sftp_browser.c diff
    src/syncterm/ssh.c diff
    src/syncterm/term.c diff
    SyncTERM: SFTP transfer queue + status-bar indicators Adds a session-scoped upload/download queue sitting on the persistent SFTP channel. Two worker threads (one per direction) transfer jobs in 4 KB chunks so the live terminal keeps getting CPU and the SFTP mutex is only held one round-trip at a time. Workers preserve mtime on both sides so a later browse shows [==]. The browser's status chip now reports queue state ([\x18\x18]/[\x19\x19] active, [Q\x18]/[Q\x19] queued, [er]/[cx] terminal) and falls back to the size+mtime compare when no job exists. U/D enqueue upload/download from the session's configured ul/dl dirs; Del cancels. Status bar paints up/down arrows at cols 28/29, folded into the dirty hash so the bar only repaints on activity transitions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  676. Deucе
    Fri Apr 24 2026 08:25:05 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/sftp_browser.c diff
    src/syncterm/sftp_browser.h diff
    Modified Files:

    src/syncterm/GNUmakefile diff
    src/syncterm/term.c diff
    SyncTERM: Alt-S opens a read-only SFTP browser New modal screen that lists the remote filesystem while the terminal stays connected. Each row shows size (with B / KiB / MiB / GiB suffix via byte_estimate_to_str), last-modified date, a local-compare status [==] / [<>] / blank (size + mtime vs the session's download dir), and the filename. Enter on a directory descends; Enter on `..` returns to the parent; Esc leaves. If the lname@syncterm.net extension is negotiated, directories render the supplied lname in the filename column, and highlighting a file with an lname draws it centred on the second-last line (cleared to uifc.bclr | (uifc.cclr << 4)). The preview updates reactively via WIN_DYN polling with a 50ms throttle. The list uses WIN_FIXEDHEIGHT with list_height = nentries + vbrdrsize capped at scrn_len - 4 so the window's shadow never covers the preview line. Phase 3 is read-only — files can be viewed but not queued. Queue actions and the queue screen follow in later phases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  677. Deucе
    Fri Apr 24 2026 08:24:11 GMT-0700 (PDT)
    Modified Files:
    

    src/sftp/sftp.h diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_pkt.c diff
    sftp: fix getstring bounds + distinguish do_open failure modes getstring() bounded against pkt->sz - offsetof(data) - pkt->cur - sizeof(sz). The extra -sizeof(sz) made the check require 4 bytes of trailing slop past the string's actual content, which rejected small valid replies in any packet whose allocation was tight (e.g. the reply packets extract_packet() hands to the pending waiter). A redundant second check after get32() duplicated work get32 had already done. Drop the -sizeof(sz) and the redundant second check; roll cur back fully on failure so retries see an untouched buffer. do_open() previously returned false without setting the per-thread err code when anything other than a real SSH_FXP_STATUS reply went wrong. Callers saw get_err() == SSH_FX_OK and had no way to tell whether the send failed, the reply was NULL, or the reply type was unexpected. Now every failure branch sets a specific code: FAILURE for local build errors, CONNECTION_LOST for send/delivery failures, BAD_MESSAGE for unrecognized or malformed replies. Add sftpc_debug_last_reply_type() exposing the type byte of the most recent reply for diagnostic messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  678. Deucе
    Fri Apr 24 2026 07:08:58 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/sftp_session.h diff
    Modified Files:

    src/syncterm/ssh.c diff
    SyncTERM: persistent SFTP subsystem channel per SSH session Open the SFTP subsystem channel once, right after the shell channel opens, and keep it alive for the lifetime of the SSH session. The channel's sftpc_state_t, atomics for "SFTP available" and "shell alive", and the recv thread are all session-scoped; browser / transfer queue code (later phases) pulls the shared state from sftp_session.h. add_public_key() stops opening its own channel and just uses the shared sftp_state. ssh_close() tears down in the right order: sftpc_finish waits for any in-flight ops to drain, the recv thread stops polling sftp_chan, then the channel closes, then sftpc_end releases the state. If the server doesn't support the SFTP subsystem, sftp_available stays false and the shell continues normally — this is not an error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  679. Deucе
    Fri Apr 24 2026 06:50:57 GMT-0700 (PDT)
    Added Files:
    

    src/sftp/sftp.c diff
    src/sftp/sftp_common.c diff
    Modified Files:

    src/sftp/objects.mk diff
    src/sftp/sftp.h diff
    src/sftp/sftp_attr.c diff
    src/sftp/sftp_client.c diff
    src/sftp/sftp_pkt.c diff
    src/sftp/sftp_server.c diff
    src/sftp/sftp_str.c diff
    Removed Files:

    src/sftp/sftp_static.h diff
    sftp: single-TU build + request-id dispatch + extension negotiation Rebuild the SFTP library as one translation unit (sftp.c #includes the five source files) so the compiler sees every internal function. Drop the cross-TU shim pattern that was in sftp_static.h — its wrappers were only there to share state-oriented helpers between client.c and server.c across TU boundaries, which is moot now. The file is renamed to sftp_common.c and reduced to genuinely shared pieces (extension table, appendheader). Non-public functions lose their sftp_*_ prefix and become static. Public API in sftp.h is unchanged from the consumer's POV; only 60 T-symbols remain in libsftp_mt.a, all legitimately public. Client gets a request-id demux (struct sftpc_pending per in-flight op on the caller's stack; sftpc_recv dispatches completed packets to the matching waiter's event) plus new ops: stat / lstat / fstat / opendir / readdir / mkdir / rmdir / remove / rename / setstat / fsetstat. Per-thread last-error via pthread_key_t so concurrent callers don't clobber each other's status. INIT/VERSION now negotiate extensions: both sides advertise (name, version) pairs; an extension is enabled only when name AND version match. Initial extensions: lname@syncterm.net and descs@syncterm.net, both at version "1". sftps_state_t now splits into a public outer struct (callbacks, version, extensions) and an opaque priv struct (rxp/txp/mtx/running/ id/terminating) living inside the TU so consumers can't tamper with internal fields. Packet struct bodies (sftp_tx_pkt, sftp_rx_pkt, sftp_extended_file_attribute) move out of sftp.h for the same reason. Client ops validate NULL/range args before taking the mutex so a doomed call never acquires the lock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  680. Deucе
    Thu Apr 23 2026 23:30:14 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    SyncTERM: stop rebuilding xp_crypt_botan3 / xp_tls_botan3 every run The botan: delegation target in build/botan.gmake has no prerequisites and no recipe that creates a botan sentinel file. Every gmake run therefore reruns the recipe, and gmake marks botan as "just remade" — any target that lists botan as a normal prerequisite rebuilds along with it. xp_crypt_botan3.o and xp_tls_botan3.o did exactly that. Move botan to the order-only side of the prerequisite list on both rules. The recursive Botan check still runs first (fast no-op when up to date), but its timestamp no longer invalidates the two C++ objects. A second back-to-back gmake now produces no Compiling lines. Mirrors the cl: / cryptlib: pattern, whose consumers depend on the real \$(CRYPT_LIB) file path — not on the cl pseudo-target — and so never hit this bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  681. Deucе
    Thu Apr 23 2026 23:30:14 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Manual.txt diff
    src/uifc/filepick.c diff
    uifc: Ctrl+A tags every file in the multi-file picker Adds a Select-All shortcut to the multi-pick file browser: Ctrl+A in the directory or file pane tags every entry currently visible in the file pane (directories live in s->dirs, not s->files, so they are never selectable). sel_add() is idempotent, so pressing Ctrl+A on an already-fully-tagged list is a no-op. Status-bar hint is left alone; the shortcut is documented in the File Browser section of Manual.txt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  682. Deucе
    Thu Apr 23 2026 23:30:14 GMT-0700 (PDT)
    Modified Files:
    

    src/uifc/filepick.c diff
    uifc: fix MSVC build of filepick.c On Windows globi is a function-like macro that forwards to glob, so taking its address (globfn = globi) left the identifier undeclared. Replace the function-pointer dance with a small fp_glob_call() wrapper that invokes glob or globi at the call site, expanding the macro normally. Also swap strcasecmp for stricmp in cmp_entry_ci() to match the rest of uifc (genwrap.h aliases stricmp to strcasecmp on Unix; MSVC has stricmp natively). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  683. Rob Swindell (on Windows 11)
    Thu Apr 23 2026 23:23:59 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/bat_xfer.cpp diff
    Defeat screen pause during batch upload processing <Deuce> tribingo.zip has already been uploaded to Modem Madness BBS Doors (M-Z) <Deuce> ezslot23.zip (58,476 bytes) received. <Deuce> rtckrs25.zip (150,904 bytes) received. <Deuce> loded.zip (131,201 bytes) received. <Deuce> rtbs19.zip (161,604 bytes) received. <Deuce> [Hit a key] -
  684. Deucе
    Thu Apr 23 2026 22:59:26 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Manual.txt diff
    SyncTERM: document batch upload in Manual.txt Expand the Alt+U entry to describe the new protocol-first flow, the ZMODEM Batch and YMODEM Batch variants, and the cross-reference to the multi-file picker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  685. Deucе
    Thu Apr 23 2026 22:56:26 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/xmodem.c diff
    src/sbbs3/xmodem.h diff
    src/syncterm/term.c diff
    src/syncterm/term.h diff
    SyncTERM: batch uploads for YMODEM and ZMODEM (feature request 28) Adds "ZMODEM Batch" and "YMODEM Batch" entries to the upload protocol menu. Batch selections use filepick_multi() to tag any number of files and transmit them in a single session using the protocol's native batch framing — ZRQINIT/ZFILE×N/ZFIN for ZMODEM, block-0 headers plus a zero-filename terminator for YMODEM. begin_upload() now shows the protocol menu before the file picker so batch choices can route to filepick_multi() while single-file choices stay on filepick() (which still allows typing a path). The existing zmodem_upload() / xmodem_upload() single-file signatures are unchanged, so ripper.c's seven call sites keep working. The new zmodem_batch_upload() and xmodem_batch_upload() helpers mirror sexyz.c:send_files(): pre-scan the file list for totals, loop over each path, and fire the trailing ZFIN / YMODEM zero-filename block once at the end. ZRQINIT is sent only for the first file of the session. xmodem_t gains current_file_num and current_file_name fields, populated by xmodem_send_file() (name) and by the batch / single-file / download drivers (num / totals). xmodem_progress() gains a "File (n of N): name" line gated on YMODEM mode, matching the one in zmodem_progress(). Adding that line exposed a latent scroll bug in xmodem_progress(): the progress bar draws tww-4 characters in a tww-4 wide window, and writing the final character advances the cursor past the last column. With _wscroll = 1 (the default) and the new line pushing the bar into the last row of the 5-row progress area, the wrap-advance scrolled the window up and chewed off the File line. Now set _wscroll = 0 for the redraw, matching zmodem_progress(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  686. Deucе
    Thu Apr 23 2026 21:40:19 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/Manual.txt diff
    src/uifc/filepick.c diff
    src/uifc/filepick.h diff
    src/uifc/uifc.h diff
    src/uifc/uifc32.c diff
    src/uifc/uifctest.c diff
    uifc: rework filepick, add multi-select and directory pickers filepick.c is refactored from a single 450-line function into small field handlers around an fp_state struct. Long-standing TODO flags (UIFC_FP_UNIXSORT, UIFC_FP_SHOWHIDDEN) now work; results are sorted case-insensitively with directories first and ".." pinned to the top. Magic keycode constants are replaced with a new UIFC_EXTKEY() macro, file sizes are rendered via byte_estimate_to_str() with IEC suffixes, and glob() is replaced with globi() so case-insensitive matching is portable. Two new public entry points: filepick_multi() — tag any number of files across directories filepick() — now also honours UIFC_FP_DIRSEL for dir selection Layout gains an info pane (filename + size + mtime), dedicated OK/Cancel footer buttons (plus Review in MULTI mode), truncated-path display that keeps the deepest directory visible, and a uifcapi_t edit_item field so callers can override the F2 label that bottomline() draws when WIN_EDIT is set. uifctest gets Multi-file picker and Directory picker menu entries. syncterm/Manual.txt documents the shared file browser, its three panes, footer buttons, and full keyboard + mouse bindings including Ctrl+Enter as an OK shortcut and F2 to review the multi-select set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  687. Deucе
    Thu Apr 23 2026 18:30:43 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    SyncTERM: include modifier keys in mouse reports fill_mevent() ignored mouse_event.kbmodifiers, so Shift/Ctrl/Alt chords on the mouse always arrived at the host with the modifier bits cleared. BBS-side code could not distinguish Ctrl+Click from a plain click. Map CIOLIB_KMOD_SHIFT/ALT/CTRL to xterm's Cb modifier bits (4/8/16) and OR them into the reported button code for both legacy (CSI M) and SGR (CSI <) reporting. X10 compatibility mode (DECSET 9) is press-only and button-bits-only per spec, so modifiers are suppressed there. In the legacy branch the OR happens after the "release -> button = 3" clamp, so release events still carry modifiers. The modifier bits (4/8/16) do not collide with the existing +61/+121 high-button offsets or the +32 motion bit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  688. Deucе
    Thu Apr 23 2026 18:20:01 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    src/syncterm/Manual.txt diff
    src/syncterm/term.c diff
    SyncTERM: hold Alt during drag-select to copy a rectangular region Default drag-select still flows stream-shaped across line wraps. When the Alt modifier is held at the start of the drag, the selection becomes rectangular: each row of the rectangle is copied as its own line with trailing spaces trimmed. The Alt state is sampled once at drag start and locked for the rest of the drag. All ciolib display backends already populate mevent.kbmodifiers, so no backend-specific work is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  689. Deucе
    Thu Apr 23 2026 18:12:36 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    Changes
  690. Deucе
    Thu Apr 23 2026 18:00:16 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    SyncTERM: display SSH auth banners (RFC 4252 §5.4) Wire DeuceSSH's per-session banner callback to uifc.showbuf(). Each SSH_MSG_USERAUTH_BANNER from the server is shown modally as it arrives during authentication; auth resumes when the user dismisses. Skipped under bbs->hidepopups (automated sessions with no human to read the banner). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  691. Deucе
    Thu Apr 23 2026 17:54:52 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    SyncTERM: warn the user about weak SSH host keys Treat any RSA-family host key under 2048 bits as weak (NIST 2024 floor; Ed25519 is always 256 and considered strong). The host-key verify callback now stashes the algorithm name and key size so the post- handshake UI can act on it: - HOSTKEY_NEW + weak: prompt "Weak host key (NNNN-bit algo)" with a Disconnect/Accept choice instead of silent TOFU. Under hidepopups (no human present) refuse the connection rather than auto-trust a weak key. - HOSTKEY_MISMATCH + weak: existing change-fingerprint dialog grows a "WARNING: the new key is a NNNN-bit algo, below the 2048-bit safety floor" block, and the title itself becomes "Fingerprint Changed — WEAK NNNN-bit algo key" so the warning is visible without F1. - Strong keys: behaviour unchanged (NEW silently TOFU's, MISMATCH uses the original dialog). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  692. Deucе
    Thu Apr 23 2026 17:28:16 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/test/test_alloc.c diff
    DeuceSSH: reject weak DH-GEX groups below client_min The client sends GEX_REQUEST(min=2048, n=4096, max=8192) but never verified the bit-length of the server-provided p in GEX_GROUP. RFC 4419 §3 only SHOULDs that the server honor min; a hostile or misconfigured server could downgrade to, say, a 768-bit group and the client would complete the handshake, deriving session keys over a Logjam-scale weak group before the host-key signature is checked. Add a backend-neutral bit-length check in dhgex_client() before ops->client_keygen(), so both the OpenSSL and Botan backends benefit from a single fix. Introduce a small mpint_bits() helper that walks past the at-most-one 0x00 sign-pad byte and counts the remaining bits. Reject with DSSH_ERROR_INVALID when |p| < client_min. New negative test test_dhgex_client_group_too_small feeds a 768-bit p through the existing bad_server_group_thread harness and confirms the client rejects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  693. Deucе
    Thu Apr 23 2026 17:01:13 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ssh.c diff
    SyncTERM: send TERM environment variable on SSH channel Add an SSH "env" request (RFC 4254 §6.4) alongside the existing pty-req TERM, so servers that read TERM from the environment (rather than from the pty allocation) pick up SyncTERM's emulation string. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  694. Deucе
    Thu Apr 23 2026 16:57:52 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/conn.h diff
    src/syncterm/conn_telnet.c diff
    src/syncterm/rlogin.c diff
    src/syncterm/rlogin.h diff
    SyncTERM: RLogin raw/cooked mode + scope rlogin hooks to RLOGIN conn types Implement the remaining RFC 1282 OOB control bytes: 0x10 (enter raw) and 0x20 (return to cooked). Cooked mode (the default) now does local TTY flow control — DC3 in user input pauses the recv loop, DC1 resumes — and neither byte reaches the remote. Raw mode passes both through. The rlogin protocol has no client-initiated raw request, so server- and client-initiated raw share a single flag: conn_api.binary_mode, upgraded to _Atomic bool. conn_binary_mode_on() now flips raw mode for rlogin, so zmodem/ymodem/xmodem transfers stay transparent to DC1/DC3 without the transfer code knowing about rlogin at all. The side-effect hook rlogin_binary_mode_on() just clears any pending pause; no off-hook needed. Also scope the rlogin-specific OOB / DC1/DC3 / SO_OOBINLINE paths to CONN_TYPE_RLOGIN and CONN_TYPE_RLOGIN_REVERSED. rlogin_connect is shared with RAW and MBBS_GHOST, and the input/output thread pair is shared with telnet_connect too; a new rlogin_active atomic gates all rlogin behavior so those other conn types remain transparent pipes. The 4 rlogin handshake conn_send()s now run BEFORE tx_parse_cb is installed, so a password or username containing 0x11/0x13 is passed through verbatim instead of being stripped as XON/XOFF. Atomics are explicitly atomic_store()'d at the top of rlogin_connect and telnet_connect; memset in conn_connect isn't a valid _Atomic init, and stale flag state from a prior session would be a mess. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  695. Deucе
    Thu Apr 23 2026 16:22:20 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/conn.c diff
    src/syncterm/conn.h diff
    src/syncterm/rlogin.c diff
    SyncTERM: honor RFC 1282 TIOCFLUSH (0x02) OOB byte in rlogin When the rlogin server sends the 0x02 urgent byte, it's signalling (after a server-side interrupt) that any client output still queued is stale and should be dropped. Add a conn_buf_reset() helper that clears a conn_buffer under its mutex and wakes any writer blocked on free space, and call it on conn_outbuf from rlogin_handle_control(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  696. Deucе
    Thu Apr 23 2026 16:18:45 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/syncterm/conn.c diff
    src/syncterm/conn.h diff
    src/syncterm/conn_conpty.c diff
    src/syncterm/conn_conpty.h diff
    src/syncterm/conn_pty.c diff
    src/syncterm/conn_pty.h diff
    src/syncterm/rlogin.c diff
    src/syncterm/rlogin.h diff
    src/syncterm/ssh.c diff
    src/syncterm/ssh.h diff
    src/syncterm/telnet_io.c diff
    src/syncterm/telnet_io.h diff
    src/syncterm/term.c diff
    src/syncterm/window.c diff
    SyncTERM: propagate terminal resizes to the remote end When DECSSDT toggles the status row the cterm's text dimensions change live, but nothing was telling the remote BBS. Add a size-change callback in cterm (fired from cterm_resize_rows) that term.c wires through a new conn_api.send_window_change dispatcher. Per-protocol implementations: - Telnet/TelnetS: NAWS subnegotiation, gated on DO NAWS, with RFC 855/1073 0xFF byte-stuffing (previously missing on the inline DO-NAWS reply too). - SSH/SSHNA: SSH2 "window-change" via dssh_chan_send_window_change. - RLogin: RFC 1282 0xFF 0xFF 's' 's' message. First OOB handling in rlogin.c — enables SO_OOBINLINE, detects urgent bytes via SIOCATMARK, and dispatches through an extensible rlogin_handle_control() switch (stubs for 0x02/0x10/0x20; 0x80 latches winsize-enabled and flushes the current size). - Local shell: TIOCSWINSZ on the pty master (unix) and ResizePseudoConsole on the HPCON (win32 conpty). - Raw/Modem/Serial: NULL; wrapper treats NULL as no-op. Pixel dimensions are normalized across cterm's callback, the conn callers, and get_term_win_size(): per-cell size comes from runtime vstat.charwidth/charheight (not the static vparams table, which was wrong for EGA once the font changed), and when the terminal fills every row/column of the framebuffer we report vstat.scrnwidth/ scrnheight so leftover scanlines (EGA 80x43 has 6 unused rows) aren't truncated. ssh_connect now passes real pixel dims to the initial pty-req instead of zeros. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  697. Deucе
    Thu Apr 23 2026 03:31:56 GMT-0700 (PDT)
    Added Files:
    

    3rdp/dist/Botan.tar.xz diff
    src/build/botan.gmake diff
    src/ssh/posix/threads.c diff
    src/ssh/posix/threads.h diff
    src/ssh/win32/threads.c diff
    src/ssh/win32/threads.h diff
    src/syncterm/ini_crypt.c diff
    src/syncterm/ini_crypt.h diff
    src/syncterm/legacy_ciphers/idea.c diff
    src/syncterm/legacy_ciphers/legacy_ciphers.h diff
    src/syncterm/legacy_ciphers/rc2.c diff
    src/syncterm/legacy_ciphers/register.c diff
    src/syncterm/xp_crypt.h diff
    src/syncterm/xp_crypt_botan3.cpp diff
    src/syncterm/xp_crypt_none.c diff
    src/syncterm/xp_crypt_openssl.c diff
    src/syncterm/xp_sndfile.c diff
    src/syncterm/xp_sndfile.h diff
    src/syncterm/xp_tls.h diff
    src/syncterm/xp_tls_botan3.cpp diff
    src/syncterm/xp_tls_none.c diff
    src/syncterm/xp_tls_openssl.c diff
    src/xpdev/extdeps.mk diff
    Modified Files:

    .gitlab-ci-unix.yml diff
    .gitlab-ci.yml diff
    3rdp/build/Common.gmake diff
    3rdp/build/GNUmakefile diff
    src/build/Common.gmake diff
    src/doors/gac/gac_bj/src/GNUmakefile diff
    src/doors/gac/gac_fc/src/GNUmakefile diff
    src/doors/gac/gac_wh/src/GNUmakefile diff
    src/ssh/CMakeLists.txt diff
    src/ssh/TODO.md diff
    src/ssh/deucessh-portable.h diff
    src/ssh/kex/sntrup761.c diff
    src/ssh/key_algo/rsa-sha2-256-botan.cpp diff
    src/ssh/key_algo/rsa-sha2-512-botan.cpp diff
    src/ssh/key_algo/ssh-ed25519-botan.cpp diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/syncterm/CHANGES diff
    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/audio_apc.c diff
    src/syncterm/bbslist.c diff
    src/syncterm/bbslist.h diff
    src/syncterm/build.bat diff
    src/syncterm/conn.c diff
    src/syncterm/release.bat diff
    src/syncterm/ssh.c diff
    src/syncterm/ssh.h diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.h diff
    src/syncterm/targets.mk diff
    src/syncterm/telnets.c diff
    src/syncterm/webget.c diff
    src/xpdev/CMakeLists.txt diff
    src/xpdev/Common.gmake diff
    src/xpdev/GNUmakefile diff
    src/xpdev/ini_file.c diff
    src/xpdev/ini_file.h diff
    src/xpdev/xpbeep.c diff
    SyncTERM: drop Cryptlib — move SSH to DeuceSSH + TLS/symmetric crypto to OpenSSL or Botan 3 SyncTERM's SSH, TLS (TelnetS + HTTPS cache refresh), and encrypted- bbslist.ini paths all ran through a single monolithic Cryptlib dependency. Cryptlib is aging, actively deprecated in parts of Synchronet, and its opaque session model fit awkwardly with SyncTERM's ring-buffered I/O threads. This branch ports all of it: - SSH: Cryptlib → DeuceSSH (src/ssh/, from-scratch library already maintained in this repo). - TLS client: Cryptlib → xp_tls shim backed by OpenSSL or Botan 3. - Encrypted bbslist.ini: Cryptlib KDF + ciphers → xp_crypt shim backed by the same two backends. DeuceSSH's explicit-callback API maps cleanly onto SyncTERM's ring buffers; its test suite + branch coverage live alongside. All three workloads share a single backend choice at compile time. Architecture ------------ - xp_tls.[ch] (syncterm/): thin client-TLS API — open/push/pop/ flush/close — matching the shape of the old inline cryptPushData / cryptPopData calls in telnets.c and webget.c. OpenSSL / Botan 3 / none stubs. - xp_crypt.[ch] (syncterm/): thin symmetric-crypto + KDF API — xp_crypt_open(algo, keySize, salt, kdf, pwd, encrypt) → handle-based process(buf, n) semantics. Same backend trio. - ini_crypt.c (syncterm/): iniReadEncryptedFile / iniWriteEncryptedFile built on xp_crypt + ini_file.c. Moved out of xpdev so libxpdev / libsbbs / Synchronet server binaries don't transitively depend on OpenSSL / Botan 3 when they don't use TLS or encrypted INI themselves. - src/ssh/ DeuceSSH: src/syncterm/ssh.c rewritten against its native session + channel API. Algorithm preferences registered once at init; host-key callback does SHA-256 fingerprint verification; authentication tries pubkey → password → keyboard-interactive; channel setup passes terminal type + PTY size; resize via dssh_chan_send_window_change. - SFTP pubkey upload: ssh.c's add_public_key() rewritten on top of a DeuceSSH "sftp" subsystem channel. src/sftp/ itself had zero Cryptlib references and is unchanged. Encrypted bbslist.ini format ---------------------------- Reader honours both on-disk formats so existing files keep opening: v1 (Cryptlib era): PBKDF2-HMAC-SHA256, KDFiterations literal. Any of 3DES / IDEA / CAST / RC2 / RC4 / AES / ChaCha20. v2 (new writes): scrypt, N=2^15 / r=8 / p=1 by default — KDF parameters embedded in the header so the reader never has to guess. Write-side cipher choice restricted to AES-256 and ChaCha20; the list- encryption menu drops the legacy-cipher options. Legacy-read support: OpenSSL's legacy provider (3DES / CAST5 / RC4) is loaded lazily on first decrypt; IDEA and RC2 don't exist in either backend today, so SyncTERM ships bundled decrypt-only reference implementations (src/syncterm/legacy_ciphers/) registered with xp_crypt at startup. Automatic v1 → v2 migration: iniReadBBSList does a one-shot migration immediately after a successful decrypt of the user bbslist. A PBKDF2 KDF or a 3DES/IDEA/CAST/RC2/RC4 cipher triggers re-encryption as AES-256 + scrypt and a matching update to syncterm.ini's KeyDerivationIterations setting. Both files (bbslist r+b, syncterm.ini r+) are opened and syncterm.ini is pre-read before either is mutated; any open or write failure emits a uifc.msg and exit( EXIT_FAILURE) so in-memory algo / keysize / KDF spec can't drift out of sync with disk. On the good-path the bbslist is rewritten first, then the syncterm.ini setting; the original PBKDF2 iteration hint survives until both writes commit. KeyDerivationIterations: Now a KDF-spec string in syncterm.ini, not an integer. New installs get "scrypt-N15" (N=2^15 = 32,768, ~16 MiB). Cryptlib-era digit values (e.g. "50000") are still recognised on read as the PBKDF2 iteration hint for v1 files and survive until the auto-migration above rewrites them. The config menu still takes an integer from the user — scrypt's cost_log2 (8..24) — stored as "scrypt-N<X>"; writer parses the string and falls back to the compiled-in default for anything it can't interpret as scrypt, so a PBKDF2-shaped write is impossible by construction. iniWriteEncryptedFile's parameter type switched from int KDFiterations to const char *kdf_spec to match. SSH host-key fingerprints ------------------------- Stored SHA-1 fingerprints from pre-migration bbslist.ini files remain valid on first reconnect — on a match, the stored value is silently upgraded to SHA-256. The three-option mismatch dialog (Disconnect / Update / Ignore) is preserved. Build-time knobs ---------------- - DEUCESSH_BACKEND / OpenSSL | Botan. Shared probe in XP_CRYPTO_BACKEND build/Common.gmake picks one for the whole tree so every subproject of a given source tree + command-line agrees. Cached libxpdev_mt.a interoperates with its downstream linkers. - USE_VENDORED_BOTAN Fall back to building Botan 3 from 3rdp/dist/Botan.tar.xz when neither system Botan 3.6+ nor OpenSSL 3.0+ is available. Windows cross-compile always uses vendored. A shared 3rdp/build `botan:` delegation target mirrors the existing `cryptlib:` machinery. - WITHOUT_DEUCESSH Drops src/syncterm/ssh.c, CONN_TYPE_SSH / _SSHNA, the SFTP pubkey-upload bridge, and the DeuceSSH sub-build. - WITHOUT_CRYPTO Drops src/syncterm/telnets.c, CONN_TYPE_TELNETS, the encrypted bbslist.ini menu + dispatch, and the bundled legacy-cipher reference code. webget.c still compiles; its HTTPS paths no-op. xp_tls / xp_crypt fall back to "none" stub backends so nothing drags in OpenSSL or Botan 3. Auto-degradations so CI stays honest across the runner fleet: - Python 3 absent on Windows (needed for vendored Botan's configure.py) → WITHOUT_CRYPTO + WITHOUT_DEUCESSH. - Compiler cmake can't drive at C17, OR the SDK/libc doesn't ship the C11 runtime functions DeuceSSH uses (pre-10.15 macOS SDKs lack timespec_get / TIME_UTC even with an AppleClang that nominally supports -std=c17) → DeuceSSH configure fails → WITHOUT_DEUCESSH; the rest of syncterm still builds. DeuceSSH's CMakeLists probes timespec_get explicitly — CMake's C_STANDARD 17 + C_STANDARD_REQUIRED ON only checks compiler-version tables, not the actual SDK. DeuceSSH stays strict-C17 as its upstream- portability guarantee; the probe covers the handful of platforms that fall outside that window. The SyncTERM Build Options dialog reflects all of this — [*] / [ ] columns for DeuceSSH, OpenSSL, Botan 3, JPEG XL, libsndfile, display backends, audio backends — so users can tell at a glance what the binary they're running actually has. MinGW + MSVC + older macOS portability -------------------------------------- DeuceSSH is strict C17. Added thin shims where the libc falls short of C11's <threads.h> requirement: - src/ssh/posix/threads.{h,c}: pthread → C11 threads wrapper (mtx_*, cnd_*, thrd_*). Platform- gated in DeuceSSH's CMakeLists via a HAVE_THREADS_IN_LIBC probe. Covers macOS pre-14 SDK, older NetBSD, any libc without <threads.h>. - src/ssh/win32/threads.{h,c}: CRITICAL_SECTION / CONDITION_VARIABLE / _beginthreadex wrapper. MinGW-w64 and MSVC both lack the libc header. xpdev MSVC cmake: added /experimental:c11atomics (the hand-written xpdev.vcxproj has it; cmake path was missing it) and three C source files that had been in the gmake objects.mk but not the cmake SOURCE list (os_info.c, rwlockwrap.c, stbuf.c). SyncTERM MSVC Windows build moved onto cmake: src/syncterm/build.bat calls VsDevCmd.bat then cmake / nmake / msbuild. The hand-written SyncTERM.vcxproj + helpers are no longer the path of record. Build ordering + CI ------------------- The top-level all-target rule is Botan → xpdev-mt → ciolib-mt → uifc-mt → (sftp-mt + deucessh) → syncterm. Both gmake and cmake paths pre-probe DeuceSSH's configure once the crypto backend it needs is on disk; if cmake can't produce deucessh.pc for any reason (C17 unsupported, incomplete C11 runtime, missing crypto headers, etc.), WITHOUT_DEUCESSH is set automatically and the main build continues. New .gitlab-ci-unix.yml [botan] job mirrors [cryptlib]: it runs `gmake botan`, archives 3rdp/*.release/botan, and on systems where the build is a no-op (system Botan/OpenSSL detected) produces an empty-directory placeholder archive so `tar -T /dev/null` never has to emit an empty tarball. The .pc file is rewritten to use ${pcfiledir}-relative prefix at install time so the archive relocates cleanly across runner concurrent-ids. [syncterm] extracts the archive; other jobs that don't build SyncTERM don't (dropping the botan.tgz extract + [botan] dep from 20+ non-syncterm jobs). [syncterm-cmake] also extracts the archive rather than building a second vendored copy via its own ExternalProject. Files moved / renamed --------------------- src/xpdev/{xp_tls,xp_crypt}*.[ch,cpp] → src/syncterm/{xp_tls,xp_crypt}*.[ch,cpp] src/xpdev/ini_crypt.c → src/syncterm/ini_crypt.c src/xpdev/ini_file.h : encrypted-INI decls moved to new src/syncterm/ini_crypt.h (including enum iniCryptAlgo); ini_file.h stays crypto-free. src/syncterm/sndfile.{h,c} → src/syncterm/xp_sndfile.{h,c} (the old name shadowed libsndfile's <sndfile.h> when the syncterm source dir was on -I path). Verification ------------ SSH: - password auth, pubkey auth, keyboard-interactive auth against Synchronet test BBS. - first-connect fingerprint prompt; reconnect uses stored fingerprint; stored-SHA-1 upgrade to SHA-256 on next connect. - SFTP pubkey upload round-trip. TLS: - TelnetS handshake + interactive session against a known TelnetS BBS (Dave's, vert). - HTTPS: `lst://` bbslist refresh path; ETag / Last-Modified caching behaves unchanged. Encrypted bbslist.ini: - Read-compat: pre-migration files (AES-256, 3DES, ChaCha20, IDEA, RC2) all decrypt and populate entries. v1 PBKDF2 header is honoured; KDFiterations hint in syncterm.ini supplies the count. - Auto-migration: v1 files (PBKDF2 or legacy cipher) are re-encrypted as AES-256/scrypt in one shot at decrypt time, and syncterm.ini's KeyDerivationIterations is updated to "scrypt-N15" the same way. Any migration failure exit()s — next run retries from the untouched v1 state. - Round-trip: save encrypted → reload → verify entries. - Legacy-write UI gone — menu offers AES-256 + ChaCha20 only. - Scrypt params embedded in v2 header; reader reconstructs N / r / p from there rather than guessing. Automated: - cterm_test + termtest pass unchanged. - Disconnect from an active SSH session doesn't leak threads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  698. Deucе
    Thu Apr 23 2026 01:32:24 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    Note the APC interface for audio exists now
  699. Rob Swindell (on Windows 11)
    Wed Apr 22 2026 22:28:52 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/MainFormUnit.cpp diff
    When NT Services are in use, the ServiceStatusTimerTick() controls the status start/stop button states, status caption Fixes toggling between local server status ("Down") and NT service status (e.g. "Running NT Service") as reported by Haxor. Not functional impact, just cosmetic.
  700. Rob Swindell (on Windows 11)
    Wed Apr 22 2026 22:28:30 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/syncterm.iss diff
    Increment version to 1.8 no other changes
  701. echicken
    Wed Apr 22 2026 07:33:12 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/wttr.in/wttr-lib.js diff
    Removed Files:

    xtrn/wttr.in/xterm-colors.js diff
    Colour conversion no longer needed. Also, use 'd' view option by default for regular things, you know those things that words are made of, there's usually a bunch of them or at least one, but not the smiley face ones, those are weird.
  702. Deucе
    Wed Apr 22 2026 01:16:49 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    Use equal-power ramp for Volume command too. This effectively let's you pan channels around as they play.
  703. Deucе
    Wed Apr 22 2026 00:44:29 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.adoc diff
    src/syncterm/audio_apc.c diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpbeep.h diff
    Add a duration for channel volume commands This does a dB-linear ramp from the current value to the new value over duration.
  704. Deucе
    Wed Apr 22 2026 00:09:36 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    Switch to an equal-power cross-fade. The old dB-linear cross-fade was terrible... the two samples crossed at -30dB from the channel level (normally -12), which while perceptible isn't exactly what people imagine when they think of a cross-fade. The equal-power curve actually seems reasonable for all fades.
  705. Deucе
    Wed Apr 22 2026 00:05:23 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/sndfile.c diff
    Fix fractional source index calculation
  706. Deucе
    Tue Apr 21 2026 23:34:04 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/audio_apc.c diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpbeep.h diff
    Differentiate between "stop" and "close" operations
  707. Deucе
    Tue Apr 21 2026 22:01:51 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/audio_apc.c diff
    src/syncterm/audio_apc.h diff
    src/syncterm/sndfile.c diff
    src/syncterm/sndfile.h diff
    Oh, add missing files too.
  708. Deucе
    Tue Apr 21 2026 21:59:44 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.adoc diff
    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_cterm.c diff
    src/syncterm/GNUmakefile diff
    src/syncterm/bbslist.c diff
    src/syncterm/term.c diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpbeep.h diff
    Do an APC audio API thing.
  709. Deucе
    Tue Apr 21 2026 20:06:52 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    xpbeep: fix non-MT build broken by xptone rewrite The node-FIFO mixer commit (897325cdb8) rewrote xptone() to open its own xp_audio stream via pthread_once + assert_pthread_mutex_lock + xp_audio_open — all MT-only symbols — but xptone() sits outside the XPDEV_THREAD_SAFE guard in xpbeep.c, so non-MT builds (single-threaded xpdev consumed by the Borland SBBS build) no longer compiled. Split xptone() into two variants under #ifdef XPDEV_THREAD_SAFE / #else: - MT keeps the new single-stream xp_audio_open + chunk-append path with the stream opened at -12 dB for headroom. - Non-MT restores the pre-rewrite per-chunk xp_play_sample16s flow and bakes the -12 dB attenuation into the wave buffer with an inline 0.251 scale, since the non-MT path has no stream/mixer volume to carry it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  710. Deucе
    Tue Apr 21 2026 16:29:17 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_cterm.c diff
    src/conio/sdl_con.c diff
    src/syncterm/ooii.c diff
    src/syncterm/ripper.c diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpbeep.h diff
    xpbeep: node-list FIFO mixer, dB volumes, soft-clip, -12 dB stream base Replaces the per-stream fixed 1 s S16 ring with a head/tail linked list of producer-supplied frame buffers. The caller malloc()s the PCM data and xp_audio_append() transfers ownership; the channel free()s it once the mixer has fully consumed the node. Append is non-blocking in the steady state — it only waits when the per-node metadata allocation itself fails and we need the mixer to drop a buf to free memory. This unblocks the scene-music use case where queuing an arbitrarily long playlist must return to the terminal immediately. Fold xp_audio_append_faded + xp_audio_append into a single entry point that takes a NULLable xp_audio_opts_t carrying per-entry volume (dB, summed with the stream base), fade_in/fade_out frames, crossfade, and loop. Envelopes live on the node and are evaluated per-sample in the mixer, which means looping bufs rise from silence exactly once and a crossfade append starts the overlap immediately — old tail fades out via an overlay envelope while the new buf fades in, both mixing concurrently until the overlay expires. XP_AUDIO_OPTS_INIT presets unity dB on all fields so = {0} is equivalent. Rework xp_mixer_pull to accumulate all streams into an int32_t scratch (grown under mixer_lock, persistent) and apply tanh soft-clipping in a single narrow pass at the end — replaces the old per-add int16_t saturate that distorted multi-stream mixes the moment any contribution pushed the running sum to full scale. Volumes now compose in dB (per- entry + stream base → one powf per channel per buf per pull) with 0 dB as unity. Lift the -12 dB headroom reduction out of xptone_makewave (both the U8-wrap noisy path and the double clean path) and apply it as stream base dB instead: cterm->music_stream and cterm->fx_stream open at -12 dB, xptone opens its ephemeral stream at -12 dB, sdl_beep bakes -12 dB into its static wave once at generation. Synth output keeps its full 16-bit resolution; OOII/RIP samples now ride at the same level as tones instead of being 12 dB louder than the rest of the mix. Add cterm_play_fx/_tone/_u8 on a per-cterm fx_stream (lazy-opened, distinct from music_stream so MF/MB ANSI-music state doesn't interact with RIP/OOII SFX). Migrate ripper.c rv_sound cases (A, BE, BL, M, P, R) and the ~40 xp_play_sample call sites in ooii.c to the new API; drop background bool since the persistent fx_stream handles sequencing. Teach parse_rip_new to forward the 0x0E music terminator to cterm when the preceding CSI introducer (| unconditional, M when music_enable == ENABLED, N when music_enable >= BANSI) actually arms cterm_accumulate_music. Previously parse_rip ate all SO/SI bytes as RIPterm text-window controls, which left the music accumulator stuck until the connection ended. xptone chunks and play_music notes switch to per-chunk malloc + append transfer; ripper rv_sound A/BE/BL/M/P/R go through cterm_play_fx* and no longer re-open the device per tone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  711. Deucе
    Tue Apr 21 2026 11:49:42 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/lord2/l2lib.js diff
    xtrn/lord2/lord2.js diff
    Add a custom waitkey() wrapper This properly times out all input prompts, not just the string ones. It's no longer possible to wait past the timeout but just not hitting a key.
  712. Deucе
    Tue Apr 21 2026 11:36:15 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/ini_file.c diff
    And fix some Bornings.
  713. Deucе
    Tue Apr 21 2026 11:29:00 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm_dec.c diff
    Another warning
  714. Deucе
    Tue Apr 21 2026 11:23:27 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.h diff
    src/conio/cterm_cterm.c diff
    src/conio/cterm_dec.c diff
    src/conio/cterm_prestel.c diff
    src/syncterm/modem.c diff
    src/syncterm/telnets.c diff
    src/syncterm/webget.c diff
    Try to get rid of some warnings. Since we're looking at the Win32 pipes and are early in the 1.9 beta cycle, try to fix some warnings.
  715. Deucе
    Tue Apr 21 2026 11:23:27 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/lord2/l2lib.js diff
    Fix up "last key" time storage. If there's no key, don't update lastkey.
  716. Deucе
    Tue Apr 21 2026 10:46:18 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    Fix warning.
  717. Deucе
    Tue Apr 21 2026 10:35:04 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    Add some "idiomatic" idiocy for Windows.
  718. Deucе
    Tue Apr 21 2026 10:21:00 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    src/xpdev/xpdev_mt.props diff
    Fix(?) Borland and MSVC builds.
  719. Deucе
    Tue Apr 21 2026 09:41:21 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    src/syncterm/syncterm.c diff
    src/xpdev/CMakeLists.txt diff
    src/xpdev/Common.gmake diff
    src/xpdev/GNUmakefile diff
    src/xpdev/xpbeep.c diff
    xtrn/lord2/lord2.js diff
    Switch native Win32 audio from WaveOut to WASAPI WASAPI is better and has been around since Vista... WaveOut is a crappy wrappy around it.
  720. Deucе
    Mon Apr 20 2026 23:35:08 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.h diff
    More Borland hate.
  721. Deucе
    Mon Apr 20 2026 23:27:20 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/xpbeep.c diff
    And, of course, fix the Borland build.
  722. Deucе
    Mon Apr 20 2026 23:19:23 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/CMakeLists.txt diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpdev.vcxproj diff
    src/xpdev/xpdev_mt.vcxproj diff
    The pipes! FIX THE PIPES!
  723. Deucе
    Mon Apr 20 2026 22:41:56 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_cterm.c diff
    src/conio/cterm_cterm.h diff
    src/conio/sdl_con.c diff
    src/syncterm/CHANGES diff
    src/syncterm/ripper.c diff
    src/xpdev/sdlfuncs.c diff
    src/xpdev/xpbeep.c diff
    src/xpdev/xpbeep.h diff
    xpdev: S16 stereo 44100 audio with streaming mixer Phase 1: format upgrade to signed 16-bit stereo 44100 Hz across all 7 backends (CoreAudio, PulseAudio, PortAudio, SDL, Win32 waveOut, ALSA, OSS). SINE/SAWTOOTH/SQUARE become ideal native S16; the noisy shapes (SINE_HARM / SINE_SAW / SINE_SAW_CHORD / SINE_SAW_HARM) preserve their intentional U8 numeric artifacts by computing in U8 then expanding. Phase 2: xp_audio_* streaming API with per-stream ring buffers, per-channel volume, dB-linear fade in/out, and crossfade. Phase 3: unified pull-based mixer. Pull-native backends (SDL, CoreAudio, PortAudio, PulseAudio async) drive xp_mixer_pull from their own callback; push-native backends (Win32 waveOut, ALSA, OSS) run a single device thread that pulls and writes. Phase 3d: ANSI music (MML) rewritten around xp_audio_append; note_params / tone_or_beep / cterm_playnote_thread scaffolding deleted from cterm.c / cterm_cterm.c. PortAudio: dropped v1.8 pablio support, v19 callback API only. PulseAudio: converted from libpulse-simple to libpulse async with pa_threaded_mainloop, now pull-native. Win32 waveOut: pre-allocated 4-buffer ring with CALLBACK_EVENT. SDL audio: sdlfuncs.c libnames now lists SDL2 first. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  724. Deucе
    Mon Apr 20 2026 21:10:57 GMT-0700 (PDT)
    Modified Files:
    

    src/build/Common.gmake diff
    Default to use SDL audio on Haiku
  725. Deucе
    Mon Apr 20 2026 13:09:37 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.adoc diff
    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_dec.c diff
    src/conio/cterm_dec.h diff
    src/conio/cterm_internal.h diff
    src/conio/cterm_test.c diff
    src/syncterm/CHANGES diff
    src/syncterm/ripper.c diff
    src/syncterm/term.c diff
    xtrn/termtest/termtest.js diff
    cterm: add DECSSDT + DECSASD status-line control (VT320) The puts the BBS in control of the status bar. A BBS can enable, disable, or create custom status bars now. Lets the host hide, keep, or write to the bottom status row via `CSI Ps $ ~` (DECSSDT: 0=none, 1=indicator, 2=host-writable) and `CSI Ps $ }` (DECSASD: 0=main, 1=status). Ps=2 spawns a 1-row sub-cterm; cterm_write routes into it while DECSASD=1 and status- display sequences dispatched on the sub bubble to the parent. Main height changes via a new cterm_resize_rows() helper that preserves content and user DECSTBM. DECRQSS `$~`/`$}` report state. Mouse events on the indicator row stay local; Ps=0/2 pass through. 14 new cterm_test cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  726. Deucе
    Mon Apr 20 2026 10:31:53 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Fix doorway mode We have a small number of controls that are ALWAYS available in SyncTERM. These are checked before cterm_encode_key(). The rest are only checked if cterm_encode_key() didn't send anything.
  727. Deucе
    Mon Apr 20 2026 10:18:35 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Move hangup handing into helper function This enables splitting key handing into pre/post CTerm processing.
  728. Deucе
    Mon Apr 20 2026 10:09:13 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    Unsloppify
  729. Deucе
    Sun Apr 19 2026 22:35:48 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_test.c diff
    src/syncterm/term.c diff
    cterm: move keyboard encoding from term.c into cterm_encode_key() The six per-emulation key-mapping tables, DECBKM backspace/delete logic, ATASCII inverse toggle, Prestel/BEEB reveal toggle, and doorway NUL+scancode encoding all consulted state (emulation, extattr, doorway mode) that already lives inside struct cterminal, yet the translation logic sat in syncterm/term.c. Pulling it into cterm mirrors what cterm already does for the inbound (cterm_write) and response (cterm_respond) paths, tightens the module boundary, and unblocks multi-instance conio. ATASCII inverse state now lives on the cterm as `bool atascii_inverse`, read back for the status bar via cterm_atascii_inverse(). All byte emission goes through cterm_respond()/response_cb, so cterm stays decoupled from conn.c. The doorway short-circuit is absorbed into cterm_encode_key(); term.c keeps only the Alt-Z local filter. Adds 18 cterm_test cases covering ANSI raw/F-keys/arrow, DECBKM on/off for BS and DEL, VT52 arrow+DECBKM, ATASCII inverse toggle, PETSCII F-keys, Prestel '#' remap, BEEB HOME, and three doorway-mode variants (scancode, Alt-Z exemption, doorway off). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  730. Deucе
    Sun Apr 19 2026 21:36:18 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_dec.c diff
    src/conio/cterm_ecma48.c diff
    src/conio/cterm_test.c diff
    src/sbbs3/syncview/syncview.c diff
    src/sbbs3/umonitor/spyon.c diff
    src/syncterm/HACKING.md diff
    src/syncterm/ooii.c diff
    src/syncterm/term.c diff
    src/xpdoor/xpdoor.c diff
    cterm: drop vestigial retbuf/retsize from cterm_write + apc_handler cterm_write's retbuf/retsize parameters predated the response_cb callback. Every caller passed NULL, 0. Remove them, simplify cterm_respond to drop the fallback branch, delete the response_buf flush in cterm_handle_sts, and remove response_buf/response_buf_size from struct cterminal. The apc_handler callback carried the same dead retbuf/retsize pass-through and is cleaned up in the same pass. cterm_test loses its retbuf-leak detector (unreachable once the parameters are gone). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  731. Deucе
    Sun Apr 19 2026 20:17:44 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/lord2/lord2.js diff
    Disable pending state on timeout/disconnect Without better investigation of how LORD2 actually behaves, it's better for the door the end up with cheats and/or corrupt game state than for infinite loops to occur.
  732. Deucе
    Sun Apr 19 2026 13:33:45 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/lord2/lord2.js diff
    Have vbar return -1 on disconnect This fixes issue #1130 *specifically* for Wendel's house. Wendle's house sets the player busy, then has a choice with a default value of one. If one is chosen, it does a goto back to the choice. This is the infinite loop. If there's a ref that does the opposite... for any unhandled value does a goto, that will now be broken. Fundamentally, the root cause is the way the timeout handler ignores disconnects when player.busy is true (after offmap). Someone needs to dig in and write a REF file that tests what the result of a choice when the user disconnects in offmap is in the DOS version. If the program effectively does update in this case, that can be put into handle_timeout() in the player.busy handler and eliminate the possibility for offmap loops. Loops for the 'busy' command would need to be investigated separately... 'busy' is more interesting because it's much more susceptible to carrier drop cheats (busy is what's used during almost all battles).
  733. Deucе
    Sun Apr 19 2026 13:33:45 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/dorkit.js diff
    Mark connection as closed immediation on getting notified This doesn't actually fix anything by itself, it just triggers the immediate waitkey() return and CONNECTION_CLOSED key flood sooner. This makes issue #1130 worse because it jumps immediately to 100% CPU rather than waiting for the idle timeout. Interestingly, this is calling mswait(1) every time, so it shouldn't be 100% CPU (and isn't on my system).
  734. Rob Swindell (on Windows 11)
    Sun Apr 19 2026 02:14:52 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgxfr2.c diff
    src/sbbs3/scfglib.h diff
    src/sbbs3/scfglib2.c diff
    src/sbbs3/scfgsave.c diff
    src/sbbs3/scfgsave.h diff
    SCFG import/export *dirs.ini file support, replacing the old dirs.txt purpose Complete the fix for issue #1128
  735. Rob Swindell (on Windows 11)
    Sun Apr 19 2026 02:04:32 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/con_out.cpp diff
    src/sbbs3/sbbs.h diff
    Fix GCC warning about singed/unsigned comparisons
  736. Deucе
    Sun Apr 19 2026 01:34:49 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/conio.vcxproj diff
    src/conio/conio_gdi.vcxproj diff
    src/conio/conio_sdl.vcxproj diff
    Hack in some XML and maybe fix the pipes for Win32.
  737. Deucе
    Sun Apr 19 2026 01:28:23 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm_cterm.c diff
    src/conio/cterm_petscii.c diff
    Fix the pipes.
  738. Deucе
    Sun Apr 19 2026 01:19:28 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/dorkit.js diff
    Revert "Terminate upon user disconnect at various "choice" prmopts in lord2.js" This reverts commit bb7f76b061a070657520f1b1bf555cec42dee76f. sbbs_init.js already checks bbs.online and triggers a connection close. If this isn't working, there's something wrong with the sbbs connection type. Checks for transport-specific state don't belong here.
  739. Deucе
    Sun Apr 19 2026 01:13:05 GMT-0700 (PDT)
    Added Files:
    

    src/conio/cterm_atascii.c diff
    src/conio/cterm_atascii.h diff
    src/conio/cterm_cterm.c diff
    src/conio/cterm_cterm.h diff
    src/conio/cterm_dec.c diff
    src/conio/cterm_dec.h diff
    src/conio/cterm_ecma48.c diff
    src/conio/cterm_ecma48.h diff
    src/conio/cterm_internal.h diff
    src/conio/cterm_petscii.c diff
    src/conio/cterm_petscii.h diff
    src/conio/cterm_prestel.c diff
    src/conio/cterm_prestel.h diff
    src/conio/cterm_vt52.c diff
    src/conio/cterm_vt52.h diff
    Modified Files:

    src/conio/CMakeLists.txt diff
    src/conio/cterm.c diff
    src/conio/cterm.h diff
    src/conio/cterm_test.c diff
    src/conio/objects.mk diff
    cterm: refactor to table-driven dispatch Step one in properly modular emulation. Replace the 2565-line do_ansi() switch with bsearch-based dispatch tables keyed by a 21-bit (intro | final | priv | interm) sequence key, and split the 7988-line cterm.c across per-standard files: cterm_ecma48.c ECMA-48 CSI/C1 handlers cterm_dec.c DEC private extensions (DECSM, DECRQM, DECCARA, ...) cterm_cterm.c SyncTERM/CTerm/XTerm extensions (incl. SCOSC/DECSLRM dispatcher, ANSI-music vs DL, CTerm RGB palette) cterm_vt52.c VT52 / Atari ST VT52 cterm_atascii.c Atari 8-bit ATASCII cterm_petscii.c Commodore C64/C128 PETSCII cterm_prestel.c Prestel / BEEB serial attrs, VDU 23/28, prog memory The parser is now an incremental seq_feed() state machine that drops the legacy three-pass legal_sequence / parse_sequence / parse_parameters pipeline and the heap-allocated struct esc_seq it populated. SGR and DECCARA read cterm->seq_param_int[] / seq_param_strs[] directly. cterm_write's byte loop is reduced to the canonical two-check form (pick_accumulator -> dispatch); every emulation installs its own cterm->dispatch pointer at cterm_init time. Per-emulation dispatch wiring: * ANSI-BBS and Atari ST VT52 share cterm_accumulate_ecma_seq and seq_feed, each installing its own sorted dispatch table (cterm_ansi_dispatch[], cterm_st_vt52_dispatch[]) for bsearch. VT52 has its own single-byte dispatcher (cterm_dispatch_vt52) for VT52-specific C0 semantics (VT/FF as LF, bytes 1-6/14-31 silently dropped) while sharing the sequence-parsing path. * PETSCII and ATASCII use a shared cterm_dispatch_byte with a 256-bit per-emulation ctrl_bitmap[32] marking control bytes and a byte_handlers[256] table of per-byte handlers. Printable bytes go through cterm_c64_get_attr. ATASCII's one-shot ESC inverse installs cterm->accumulator (file-local atascii_literal_byte) which takes priority in cterm_pick_accumulator. * Prestel keeps its custom programmable-memory state machine (cterm_accumulate_prestel_seq) because the ESC 1 / ESC 2 grammar does not benefit from a bsearch table. * BEEB has no sequence accumulator; VDU 23 (9 trailing bytes) and VDU 28 (2 trailing bytes) install the generic cterm_accumulate_trailing via cterm->seq_trailing_handler. Supporting machinery: * Cascade consumption tracking (seq_consumed[] bitmap + seq_consumed_any latch) so handlers like SGR/DECSM can split across standards without the "empty list -> apply default" path clobbering already-consumed parameters. * Accumulators for multi-byte state: ecma_seq, prestel_seq, command_string, sos, music, font, doorway, trailing (+ ATASCII's one-shot atascii_literal_byte installed directly on cterm). * Dispatch entries carry a `trailing` count; when set, the matching handler is deferred until cterm_accumulate_trailing collects N raw bytes into seq_param_int[] (VT52 ESC Y / ESC b / ESC c). * Handlers in new files follow a cterm_handle_<name> convention; promoted statics uniformly gain the cterm_ prefix. The static library exports no unprefixed symbols. 283/283 cterm_test + 67/67 termtest cases pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  740. Deucе
    Sun Apr 19 2026 01:13:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    Clean up orphaned BBS cache directories del_bbs() now removes the cache dir alongside the INI section, the rename path in edit_name() renames the cache dir to match, and show_bbslist() sweeps orphaned cache dirs on startup (preserving syncterm-system-cache). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  741. Deucе
    Sun Apr 19 2026 01:13:05 GMT-0700 (PDT)
    Modified Files:
    

    3rdp/build/CMakeLists.txt diff
    Add hack for FreeBSD port system FETCHCONTENT_FULLY_DISCONNECTED=TRUE prevents FetchContent from working at all, even when DOWNLOAD_COMMAND is empty.
  742. Deucе
    Sun Apr 19 2026 01:13:05 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    Fix manpage install dir
  743. Rob Swindell (on Debian Linux)
    Sun Apr 19 2026 00:17:28 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/dorkit.js diff
    Terminate upon user disconnect at various "choice" prmopts in lord2.js It appears dorkit just depends on inactivity timeout and the door to set some connection inactivity limit, which lord2 doesn't do. So lord2 would just spin forever when a user disconnects at any vbar/choice prompt. This is an SBBS only fix as I don't know what happens under the same conditions in jsdoor or if or how it actually detects a disconnected user. This fixes the issue that Cru Jones keeps emailing me about.
  744. Rob Swindell (on Windows 11)
    Sat Apr 18 2026 16:27:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbsdefs.h diff
    Bump version to 3.22a
  745. Rob Swindell (on Windows 11)
    Sat Apr 18 2026 16:27:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfg.h diff
    src/sbbs3/scfg/scfgmsg.c diff
    src/sbbs3/scfglib.h diff
    src/sbbs3/scfglib1.c diff
    src/sbbs3/scfgsave.c diff
    src/sbbs3/scfgsave.h diff
    SCFG import/export *subs.ini file support, replacing the old subs.txt purpose Partially address issue #1128
  746. Rob Swindell (on Windows 11)
    Sat Apr 18 2026 16:27:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsub.c diff
    Update help text on sub Sempahore File and Pointer Index options. Also fixed a typo from 100 years ago.
  747. Rob Swindell (on Windows 11)
    Sat Apr 18 2026 16:27:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/con_out.cpp diff
    sbbs_::attr() no longer sends unnecessary/redundant terminal color change codes ... like it did before the great terminal abstraction of v3.21. This should fix the issue reported by deon (ALTERANT).
  748. Deucе
    Sat Apr 18 2026 13:50:55 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    src/syncterm/CMakeLists.txt diff
    src/syncterm/GNUmakefile diff
    src/syncterm/Info.plist diff
    src/syncterm/Manual.txt diff
    src/syncterm/PackageInfo.in diff
    src/syncterm/dpkg-control.in diff
    src/syncterm/haiku.rdef diff
    src/syncterm/syncterm.c diff
    src/syncterm/syncterm.rc diff
    Start 1.9b
  749. Deucе
    Sat Apr 18 2026 13:49:14 GMT-0700 (PDT)
    Added Files:
    

    3rdp/build/cl-iowait-pollhup.patch diff
    Modified Files:

    3rdp/build/CMakeLists-cl.txt diff
    3rdp/build/CMakeLists.txt diff
    3rdp/build/GNUmakefile diff
    Poll for HUP in ioWait() as well Fixes macOS crash on disconnect.
  750. Deucе
    Sat Apr 18 2026 13:49:13 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CMakeLists.txt diff
    Fix CMake on macOS to use static JPEG XL libs
  751. Deucе
    Sat Apr 18 2026 02:26:39 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/sbbsecho.c diff
    Fix compile errors on GCC 15.2 error: passing argument 2 of ‘getfmsg’ from incompatible pointer type [-Wincompatible-pointer-types]
  752. Deucе
    Sat Apr 18 2026 01:38:58 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix some Coverity warnings.
  753. Deucе
    Sat Apr 18 2026 01:00:29 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/clans-src/src/video.c diff
    src/doors/clans-src/src/win_wrappers.c diff
    More Win32 cleanups video.c: - ClearScrollRegion: anchor to srWindow.Top and use window height; dwSize.Y was the whole buffer, so the old formula overcleared into scrollback whenever the console buffer exceeded the visible window (default conhost has ~300-row buffer, 25-row window). - ScrollUp: same srWindow.Top anchoring; check GetConsoleScreenBufferInfo return; guard 1-row scroll regions that would otherwise build an inverted SMALL_RECT. - Video_Init: treat both INVALID_HANDLE_VALUE and NULL as handle failure; check GetConsoleScreenBufferInfo return; pin output CP to 437 so CP437 box-drawing renders correctly on non-US locales and systems with "Use UTF-8 worldwide" enabled. - exit(0) -> exit(1) on error paths. win_wrappers.c: - display_win32_error: initialize message pointer, check FormatMessage return, use _sntprintf, explicit NUL terminator, skip LocalFree on NULL. - DirExists: switch to GetFileAttributesA; handles "C:\\" and other drive roots that the old stat + trailing-slash-strip mishandled. - FilesOrderedByDate: replace _findfirst/_findnext with FindFirstFileA/FindNextFileA. _findnext's errno behavior on end-of-iteration is CRT-dependent; Win32 GetLastError() == ERROR_NO_MORE_FILES is deterministic. struct Sortable.wt widened to uint64_t (FILETIME 100-ns ticks). - plat_getftime: floor seconds instead of rounding up (tm_sec=59 produced an out-of-range 30 that read back as invalid 60s). - plat_fsopen: add bounded retry on EACCES to approximate Unix F_SETLKW behavior -- _fsopen fails fast on sharing violation while fcntl(F_SETLKW) blocks, so transient contention (packet writes, log rotation) no longer surfaces as spurious open failures. Capped at 20 * 50ms = 1s before giving up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  754. Deucе
    Sat Apr 18 2026 00:25:50 GMT-0700 (PDT)
    Modified Files:
    

    src/doors/clans-src/CLAUDE.md diff
    src/doors/clans-src/src/system.c diff
    src/doors/clans-src/src/video.c diff
    src/doors/clans-src/src/win_wrappers.c diff
    Some Win32 fixes and fiddlins
  755. Deucе
    Fri Apr 17 2026 23:50:02 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/x_events.c diff
    XFlush() _NET_WM_PING responses Would ideally fix ticket 239. I assume the timeout there is relatively low.
  756. Deucе
    Fri Apr 17 2026 22:38:27 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Avoid implying to Coverity that rip.stw.size belongs in a mutex
  757. Deucе
    Fri Apr 17 2026 22:22:35 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/termtest/termtest.js diff
    Fix some tests.
  758. Deucе
    Fri Apr 17 2026 18:12:56 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/termtest/termtest.js diff
    Don't clear screen before asking what the user sees on the screen.
  759. Deucе
    Fri Apr 17 2026 17:50:12 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    If a cells hyperlink id has change from or to zero, redraw Spaces weren't getting redrawn with the link underscore thing.
  760. Deucе
    Fri Apr 17 2026 14:31:46 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Save font when drawing buttons and use it. Black Flag RIP menus changed fonts after drawing buttons, and that made things look bad.
  761. Deucе
    Fri Apr 17 2026 14:00:26 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Save/restore CTerm receive callback when reiniting Otherwise ANSI responses stop flowing back to the BBS in RIP mode.
  762. Deucе
    Fri Apr 17 2026 13:42:27 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix invalid ESC buffer expansion/stuffing
  763. Deucе
    Fri Apr 17 2026 11:16:10 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Some more type twiddling
  764. Deucе
    Fri Apr 17 2026 11:08:48 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix possible strdup(NULL)
  765. Deucе
    Fri Apr 17 2026 10:59:15 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix up arc_collect usage. size_t where appropriate, handle realloc() correctly.
  766. Deucе
    Fri Apr 17 2026 10:28:27 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Handle malloc() failure loading icons.
  767. Deucе
    Fri Apr 17 2026 10:26:55 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Don't set values that end up never used.
  768. Deucе
    Fri Apr 17 2026 10:19:21 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    Handle corrupted INI files better
  769. Deucе
    Fri Apr 17 2026 10:16:41 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/bbslist.c diff
    Don't sort NULL lists.
  770. Deucе
    Fri Apr 17 2026 10:00:52 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/webget.c diff
    On Linux asprintf() strp contents are undefined on failure.
  771. Rob Swindell
    Thu Apr 16 2026 21:51:59 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    Merge branch 'dd_msg_reader_sub_board_color_settings_fix' into 'master' DDMsgReader: Sub-board color settings fix - Don't try to check sub-board color settings when reading personal email See merge request main/sbbs!675
  772. Eric Oulashin
    Thu Apr 16 2026 10:39:47 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    DDMsgReader: Sub-board color settings fix - Don't try to check sub-board color settings when reading personal email
  773. Deucе
    Wed Apr 15 2026 23:53:09 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/wl_cio.c diff
    src/conio/x_cio.c diff
    Reduce stack sice increased in previous commit. Just go four times as large, not 16... I'm seeing 62k of 64k in use on my system, so increasing to 256k seems "reasonable"
  774. Deucе
    Wed Apr 15 2026 21:25:28 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/wl_cio.c diff
    src/conio/x_cio.c diff
    Bump event thread stacks up to 1MB 64k is... quite tight for the vent threads, they do a LOT. This may be the root cause of mystery crashes on Ubuntu 24.
  775. Rob Swindell
    Wed Apr 15 2026 19:51:07 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/attr_conv.js diff
    xtrn/DDMsgReader/DDMsgReader.js diff
    xtrn/DDMsgReader/ddmr_cfg.js diff
    xtrn/DDMsgReader/readme.txt diff
    xtrn/DDMsgReader/revision_history.txt diff
    Merge branch 'dd_msg_reader_per_subboard_attr_config' into 'master' DDMsgReader: Support for per-subboard message attribute toggles for the various BBS software attribute codes See merge request main/sbbs!674
  776. Eric Oulashin
    Wed Apr 15 2026 19:51:07 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/attr_conv.js diff
    xtrn/DDMsgReader/DDMsgReader.js diff
    xtrn/DDMsgReader/ddmr_cfg.js diff
    xtrn/DDMsgReader/readme.txt diff
    xtrn/DDMsgReader/revision_history.txt diff
    DDMsgReader: Support for per-subboard message attribute toggles for the various BBS software attribute codes
  777. Deucе
    Wed Apr 15 2026 12:29:05 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Also reset current_font[] on error
  778. Deucе
    Wed Apr 15 2026 12:09:03 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Even MORE font[0] paranoia on failure... Try as hard as possible to get an allocated copy of CP437 into there.
  779. Deucе
    Wed Apr 15 2026 12:04:14 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Protect the hell out of font[0] We REALLY want it to always be valid.
  780. Deucе
    Wed Apr 15 2026 11:59:13 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Don't FREE_AND_NULL() font[0] This is the last-ditch fallback, so if bitmap_loadfont_locked() fails, it should stay available.
  781. Deucе
    Wed Apr 15 2026 11:46:15 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/rwlockwrap.c diff
    Fix unlikely Win32 rwlock issue. If WaitForSingleObject() fails, the loop waiting for readers to zero would continue without wlk held, meaning it would be possible for either wlk to be left without entering, or two writers to hold the lock at the same time, depending on the value of lock->readers at the time of the failure.
  782. Deucе
    Wed Apr 15 2026 11:41:23 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Ensure fonts are loaded before screens are allocated. Should make the assertion at bitmap_con.c:659 even more impossible than it already is.
  783. Deucе
    Wed Apr 15 2026 11:09:00 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix build for SyncVIEW
  784. Deucе
    Wed Apr 15 2026 10:51:24 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    src/syncterm/uifcinit.c diff
    src/syncterm/uifcinit.h diff
    RIP polishing... Support ^^ and `` escaped characters Add support for |1R and $>FILENAME$ Add persistent variable store Add support for templates and HKEYOFF/HKEYON Enable DTW Enable DTC, STATBAR, TWIN, TWFONT, TWH, TWW, TWX0, TWX1, TWY0, TWY1 Implement SAVE/RESTORE functionality. Support tab navigation and TABON/TABOFF
  785. Deucе
    Tue Apr 14 2026 01:13:55 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rip_test/rip_full_scan.py diff
    Add a SIGINFO (CTRL-T) handler Prints the last scanned filename.
  786. Deucе
    Tue Apr 14 2026 01:04:26 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Remove segment/offset references from comments.
  787. Deucе
    Tue Apr 14 2026 01:02:54 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix whitespace
  788. Deucе
    Tue Apr 14 2026 00:49:15 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Add support for justified text. Bug-compatible with RIPterm as usual.
  789. Rob Swindell (on Debian Linux)
    Mon Apr 13 2026 21:45:46 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Create/use new logmsg() function for the JPEG-XL load library log messages lprintf/lputs() may write to the screen, so don't use them for this purpose. Deleted the Win32 OutputDebugString block since it was #ifdef'd out anyway.
  790. Rob Swindell (on Debian Linux)
    Mon Apr 13 2026 21:32:22 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Fix typo in JPEG-XL lib load failure log message
  791. Rob Swindell (on Debian Linux)
    Mon Apr 13 2026 21:30:03 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    Log a message about JEPG-XL library load failure or success
  792. Deucе
    Mon Apr 13 2026 21:04:10 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/libjxl.c diff
    Some additionaly JXL paranoia. 1. When we can't resolve a symbol from libjxl_threads, close the libjxl_threads handle, not the libjxl one! 2. If a symbol from libjxl_threads can't be resolved, set all of them to NULL so we don't try to call an unloaded object.
  793. Deucе
    Mon Apr 13 2026 20:03:28 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    More RIP tweaks Correctly handle the "selected" flag. This also makes selecting buttons match the RIPterm rendering. Support "highlight icon" buttons. This is what you're expected to use with radio buttons and checkboxen Fix paste clipping It clips at the bottom of the screen, rejects at the right.
  794. Deucе
    Mon Apr 13 2026 13:55:47 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/rip_icons.c diff
    src/syncterm/rip_icons.h diff
    Modified Files:

    src/syncterm/CMakeLists.txt diff
    src/syncterm/objects.mk diff
    src/syncterm/ripper.c diff
    Add all the default RIP icons to the binary These are included with RIPterm just like the fonts we already compile in. While most of them are terrible, it's still conceivable that someone would want to use some of them sometimes.
  795. Deucе
    Mon Apr 13 2026 11:19:00 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rip_test/rip_full_scan.py diff
    Test all RIPs. Including *.RIP and files with ! in the name. We now have 100% pixel-perfect rendering vs. RIPterm 1.54.00!
  796. Deucе
    Mon Apr 13 2026 11:18:18 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rip_ansi_validator.py diff
    src/syncterm/ripper.c diff
    More RIP fixes... Clipboard button vertical label centering. Enforce clipboard buffer size limit. The clipboard in RIPterm is a single segment, so the contents MUST fit in 65536 bytes. RIPterm actually pops up an "Image too large" error window... SyncTERM silently fails the copy. Also, drawing the popup modifies various bits of global state. Fix global state corruption When RIPterm fails to load an icon file, it forces the line thickness to 1. Do the same thing.
  797. Deucе
    Mon Apr 13 2026 09:33:03 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rip_ansi_validator.py diff
    Apparently I missed this in the last commit somehow
  798. Deucе
    Mon Apr 13 2026 01:13:31 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/rip_ansi_validator.py diff
    More errors: Validate RIP parameters are in legal range (Warning in relaxed mode) Check for |1C before |1P If the RIP pastes before copying, it's an error in strict mode. Remove the meganum split warning... it's fine. Fix parsing of ANSI in RIP commands
  799. Deucе
    Mon Apr 13 2026 01:03:39 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    More fixes. RIPterm doesn't render \x0e or \x0f at all. (perseid.rip) Fixed size is ALWAYS used for plain buttons. Fix stroked-font tabs. The actual movement after a glyph is drawn comes from the stroke data, NOT the width data. In SANS.CHR at least, the table has TAB with a width of 0, but it's actually 24 based on the stroke. Only use npoints for |p, |P, and |l
  800. Rob Swindell (on Debian Linux)
    Sun Apr 12 2026 23:23:18 GMT-0700 (PDT)
    Modified Files:
    

    exec/tests/global/interrupt_rearms.js diff
    Skip interrupt_rearms test on pre-SM128 engines The shim bug the test guards against is SM128-specific. Under SM185 the operation callback is driven by the engine's own bytecode counter (no background trigger thread), so a 1-second spin can legitimately produce only a single callback invocation — indistinguishable from the SM128 buggy state. Skip the test on older engines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  801. Rob Swindell (on Debian Linux)
    Sun Apr 12 2026 23:02:28 GMT-0700 (PDT)
    Modified Files:
    

    exec/tests/global/interrupt_rearms.js diff
    Rework interrupt_rearms test to use js.counter observation The previous version set js.time_limit=1 expecting it to terminate a tight loop after 1 second, but time_limit is a callback-count limit (for infinite-loop detection), not a wall-clock timer. On test runners with low time_limit defaults, the test tripped the infinite-loop safeguard immediately. The new version disables auto_terminate, spins for ~1 second, and verifies js.counter advanced — the background trigger thread fires interrupts every ~100ms, so a working callback path produces several invocations. With the shim bug, only one ever fires. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  802. Rob Swindell (on Debian Linux)
    Sun Apr 12 2026 22:51:11 GMT-0700 (PDT)
    Added Files:
    

    exec/tests/global/interrupt_rearms.js diff
    Add test for JS interrupt callback re-arming Regression test for a bug in the SM128 JS_SetOperationCallback shim where the callback self-disabled after first firing, preventing js.time_limit from terminating tight loops. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  803. Deucе
    Sun Apr 12 2026 20:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/GNUmakefile diff
    src/syncterm/ripdiff.py diff
    src/syncterm/ripper.c diff
    This is a combination of 81 commits. Massive overhaul of RIP support using Claude and the RIPterm test harness Basically, we went through a collection of over 2,000 RIP files collected over years from all over the internet. The majority of them are from ACiD packs, but there's also a lot from door games, BBS menu sets, and RIP tutorials as well. Almost everything has been touched aside from RIP_SET_PIXEL Major notes: Fonts updated, expecially the default bitmap fonts. SyncTERM now has a new RIPterm font that is used for ANSI output, but this is NOT the same as the "Default 8x8 Bitmap" font that BGI uses. Both are now correct. Flood fill now hits the exact same stack limit as RIPterm and uses the same seed point ordering and detection, so even RIPs where the fill is incomplete are pixel-perfect. A HUGE amount of work on the polygon ordering/degenerate handling/filling The RIP command parser appears to exactly match RIPterm, so the same syntax fails or is parsed and in the same way. Full iterative Cohen-Sutherland clipper, just like RIPterm, which special magic for thick lines that are partly outside of the viewport. The RIP_RESET "fixed" to not reset nearly enough things, just like in RIPterm. Handle Vertical Tabs in RIP mode. MASSIVELY fix a huge amount of button tweakage. RIPterm tends to accumulate errors, now SyncTERM does too!
  804. Deucе
    Sun Apr 12 2026 19:53:47 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/rip_test/hctl diff
    src/syncterm/rip_test/rip_full_scan.py diff
    src/syncterm/rip_test/rip_harness.md diff
    src/syncterm/rip_test/rip_harness.py diff
    src/syncterm/rip_test/rip_server.py diff
    Add RIPscrip pixel comparison test harness Three-component harness for pixel-perfect rendering comparison between SyncTERM and RIPterm running under DOSBox: - rip_server.py: terminal connection manager with inline flow control (RIP_QUERY sync at pipe and BOL boundaries), SAUCE stripping, and sync point identification that avoids injection inside backslash continuations and varlen command args - rip_harness.py: control server providing reset, sendfile, sendlines, capture (sync + XWD), snap, diff, and diffpixels commands over a TCP control port. Parallel send to both terminals, EGA palette-aware pixel comparison - hctl: CLI wrapper for sending commands to the control port - rip_full_scan.py: batch scanner that tests all .rip files in alphabetical order, stopping on first diff (exit 0=clean, 1=diffs, 2=error). Supports resuming from a named file - rip_harness.md: usage documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  805. Deucе
    Sun Apr 12 2026 19:53:47 GMT-0700 (PDT)
    Added Files:
    

    src/syncterm/rip_ansi_validator.py diff
    Add RIPscrip v1.54 and ANSI-BBS byte stream validator Validates .rip files against the RIPscrip Graphics Protocol v1.54 spec and CTerm ANSI-BBS emulation rules. Reports command field widths, polygon vertex counts exceeding the 255-vertex limit, and malformed ANSI escape sequences. Used by the rip-scan harness to identify RIPterm-side bugs vs SyncTERM rendering differences. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  806. Deucе
    Sun Apr 12 2026 19:48:31 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    src/conio/cterm.h diff
    Add function to detect where CR would go Important for RIP to be able to accurately detect if it's at the "beginning of a line" for ! parsing to work the same as RIPterm.
  807. Deucе
    Sun Apr 12 2026 19:48:31 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Always force cursor when setting it. Previously disabling the cursor didn't force it, so it would stay on the screen in the last state until the frame changed.
  808. Deucе
    Sun Apr 12 2026 19:48:31 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/term.c diff
    RIP mode implies LCF mode.
  809. Rob Swindell (on Debian Linux)
    Sun Apr 12 2026 02:48:40 GMT-0700 (PDT)
    Modified Files:
    

    exec/mqtt_spy.js diff
    Fix the node number display in the MQTT Spy banner i.e. "*** Synchronet MQTT Spy on Node undefined: Ctrl-C to Abort ***" broken in commit 4fe0bb1983aa734
  810. Deucе
    Sun Apr 12 2026 00:29:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/jsdebug.c diff
    jsdebug: add source line display to backtraces and list command Backtraces now show the source line text below each frame (read from the file path returned by JS_GetScriptFilename). Silently omitted when the file can't be opened (dynamic scripts, cwd changes, etc). New "l"/"list" command works like gdb's: shows 11 lines of source context centered on the current PC line, with a ">" marker on the current line. Accepts an optional line number argument, and bare "l" after a previous listing continues where it left off. You're welcome nelgin. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  811. Deucе
    Sat Apr 11 2026 23:25:47 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    src/sbbs3/terminal.cpp diff
    Fix issue #1120 When you connect only with sftp, input and output for the terminal server won't work. Make them early exit. But the REAL problem was that cols = rows = 0... The line wrapping was subtracting cols from column to get it below cols... but that doesn't work when cols is zero. Special case cols = 0 in inc_col() and rows = 0 in inc_row() This is the bit that fixes it.
  812. Rob Swindell
    Sat Apr 11 2026 16:22:29 GMT-0700 (PDT)
    Added Files:
    

    xtrn/zzt/.gitignore diff
    xtrn/zzt/DOC/ABOUT.HLP diff
    xtrn/zzt/DOC/CREATURE.HLP diff
    xtrn/zzt/DOC/EDITOR.HLP diff
    xtrn/zzt/DOC/GAME.HLP diff
    xtrn/zzt/DOC/INFO.HLP diff
    xtrn/zzt/DOC/ITEM.HLP diff
    xtrn/zzt/DOC/LANG.HLP diff
    xtrn/zzt/DOC/LANGREF.HLP diff
    xtrn/zzt/DOC/LANGTUT.HLP diff
    xtrn/zzt/DOC/LICENSE.HLP diff
    xtrn/zzt/DOC/TERRAIN.HLP diff
    xtrn/zzt/PORTING_MAP.md diff
    xtrn/zzt/README.md diff
    xtrn/zzt/ZZT.CFG diff
    xtrn/zzt/ZZT.DAT diff
    xtrn/zzt/build.js diff
    xtrn/zzt/install-xtrn.ini diff
    xtrn/zzt/package.json diff
    xtrn/zzt/src/elements.ts diff
    xtrn/zzt/src/game.ts diff
    xtrn/zzt/src/gamevars.ts diff
    xtrn/zzt/src/input.ts diff
    xtrn/zzt/src/oop.ts diff
    xtrn/zzt/src/runtime.ts diff
    xtrn/zzt/src/sounds.ts diff
    xtrn/zzt/src/synchronet.d.ts diff
    xtrn/zzt/src/txtwind.ts diff
    xtrn/zzt/src/video.ts diff
    xtrn/zzt/src/zzt.ts diff
    xtrn/zzt/tsconfig.json diff
    xtrn/zzt/web/README.md diff
    xtrn/zzt/web/experiment3.js diff
    xtrn/zzt/web/files/root/api/flweb-assets.ssjs diff
    xtrn/zzt/web/files/root/api/terminal-ui-config.ssjs diff
    xtrn/zzt/web/files/root/js/flweb.js diff
    xtrn/zzt/web/files/root/js/terminal.js diff
    xtrn/zzt/web/files/root/terminal-iframe.html diff
    xtrn/zzt/web/files/root/terminal-ui.ini diff
    xtrn/zzt/web/files/root/terminal.xjs diff
    xtrn/zzt/web/install-web-bridge.sh diff
    xtrn/zzt/zzt.js diff
    xtrn/zzt/zzt_files/CAVES.ZZT diff
    xtrn/zzt/zzt_files/CITY.ZZT diff
    xtrn/zzt/zzt_files/DUNGEONS.ZZT diff
    xtrn/zzt/zzt_files/README.txt diff
    xtrn/zzt/zzt_files/SAVE.SAV diff
    xtrn/zzt/zzt_files/TOUR.ZZT diff
    xtrn/zzt/zzt_files/TOWN.ZZT diff
    Merge branch 'zzt' into 'master' Add ZZT game port for Synchronet BBS See merge request main/sbbs!673
  813. HM Derdok
    Sat Apr 11 2026 16:22:29 GMT-0700 (PDT)
    Added Files:
    

    xtrn/zzt/.gitignore diff
    xtrn/zzt/DOC/ABOUT.HLP diff
    xtrn/zzt/DOC/CREATURE.HLP diff
    xtrn/zzt/DOC/EDITOR.HLP diff
    xtrn/zzt/DOC/GAME.HLP diff
    xtrn/zzt/DOC/INFO.HLP diff
    xtrn/zzt/DOC/ITEM.HLP diff
    xtrn/zzt/DOC/LANG.HLP diff
    xtrn/zzt/DOC/LANGREF.HLP diff
    xtrn/zzt/DOC/LANGTUT.HLP diff
    xtrn/zzt/DOC/LICENSE.HLP diff
    xtrn/zzt/DOC/TERRAIN.HLP diff
    xtrn/zzt/PORTING_MAP.md diff
    xtrn/zzt/README.md diff
    xtrn/zzt/ZZT.CFG diff
    xtrn/zzt/ZZT.DAT diff
    xtrn/zzt/build.js diff
    xtrn/zzt/install-xtrn.ini diff
    xtrn/zzt/package.json diff
    xtrn/zzt/src/elements.ts diff
    xtrn/zzt/src/game.ts diff
    xtrn/zzt/src/gamevars.ts diff
    xtrn/zzt/src/input.ts diff
    xtrn/zzt/src/oop.ts diff
    xtrn/zzt/src/runtime.ts diff
    xtrn/zzt/src/sounds.ts diff
    xtrn/zzt/src/synchronet.d.ts diff
    xtrn/zzt/src/txtwind.ts diff
    xtrn/zzt/src/video.ts diff
    xtrn/zzt/src/zzt.ts diff
    xtrn/zzt/tsconfig.json diff
    xtrn/zzt/web/README.md diff
    xtrn/zzt/web/experiment3.js diff
    xtrn/zzt/web/files/root/api/flweb-assets.ssjs diff
    xtrn/zzt/web/files/root/api/terminal-ui-config.ssjs diff
    xtrn/zzt/web/files/root/js/flweb.js diff
    xtrn/zzt/web/files/root/js/terminal.js diff
    xtrn/zzt/web/files/root/terminal-iframe.html diff
    xtrn/zzt/web/files/root/terminal-ui.ini diff
    xtrn/zzt/web/files/root/terminal.xjs diff
    xtrn/zzt/web/install-web-bridge.sh diff
    xtrn/zzt/zzt.js diff
    xtrn/zzt/zzt_files/CAVES.ZZT diff
    xtrn/zzt/zzt_files/CITY.ZZT diff
    xtrn/zzt/zzt_files/DUNGEONS.ZZT diff
    xtrn/zzt/zzt_files/README.txt diff
    xtrn/zzt/zzt_files/SAVE.SAV diff
    xtrn/zzt/zzt_files/TOUR.ZZT diff
    xtrn/zzt/zzt_files/TOWN.ZZT diff
    Add ZZT game port for Synchronet BBS
  814. Rob Swindell (on Debian Linux)
    Sat Apr 11 2026 14:01:37 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/funclib.js diff
    Replace E4X syntax in funclib.js toXML() for SM128 compatibility E4X (XMLList, XML literals) was removed in SM128, causing parse-time failure that prevents the entire funclib.js from loading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  815. Rob Swindell (on Debian Linux)
    Fri Apr 10 2026 21:07:04 GMT-0700 (PDT)
    Modified Files:
    

    exec/broker.js diff
    exec/imapservice.js diff
    exec/ircbots/ham/ham.js diff
    exec/load/bajalib.js diff
    exec/load/callsign.js diff
    exec/load/funclib.js diff
    exec/load/xjs.js diff
    webv4/lib/events/forum.js diff
    webv4/lib/events/nodelist.js diff
    webv4/lib/events/sbbsimsg.js diff
    Fix SM128 incompatibilities in exec/ and webv4/ scripts - broker.js: Fix expression closure, replace e.toSource() with JSON.stringify(e) in error logging - imapservice.js: Wrap remaining eval("function") in parens for SM128 - ircbots/ham/ham.js: Wrap eval("function") in parens - xjs.js: Replace str.toSource() with JSON.stringify(str) - bajalib.js: Replace fn.toSource() with fn.toString() for function serialization - funclib.js: Replace data.toSource() with data.toString() - callsign.js: Replace x.toSource() with JSON.stringify(x) - webv4 events (forum.js, nodelist.js, sbbsimsg.js): Change top-level const to var to prevent redeclaration errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  816. Rob Swindell (on Debian Linux)
    Fri Apr 10 2026 21:00:56 GMT-0700 (PDT)
    Modified Files:
    

    exec/imapservice.js diff
    Fix imapservice.js toSource() calls for SM128 compatibility Replace all 33 toSource() occurrences: - Deep copies: eval(x.toSource()) -> JSON.parse(JSON.stringify(x)) - Error messages: x.toSource() -> JSON.stringify(x) - Value embedding in eval'd functions: x.toSource() -> JSON.stringify(x) and wrap function expressions in parens for SM128 - NOT/OR search: replace eval+toSource function serialization with direct closure composition - Fix bug in OR: next1[0]==next1[1] -> next1[0]==next2[0] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  817. Rob Swindell (on Debian Linux)
    Fri Apr 10 2026 20:54:47 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/acmev2.js diff
    exec/load/termdesc.js diff
    exec/sbbslist.js diff
    xtrn/go-for/go-for.js diff
    xtrn/kingdom/kingdom.js diff
    xtrn/lord2/l2lib.js diff
    xtrn/war/warcommon.js diff
    Fix JavaScript errors for SM128 compatibility - acmev2.js: Replace .toSource() with JSON.stringify() (4 occurrences) - termdesc.js: Explicitly assign functions to this for load({}) scope - sbbslist.js: Remove duplicate return causing unreachable code warning - go-for.js: Change top-level const to var (redeclaration error) - kingdom.js: Change top-level const to var - warcommon.js: Change top-level const to var - l2lib.js: Wrap eval'd function expressions in parens (SM128 requires function expressions, not function statements, inside eval) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  818. Rob Swindell (on Debian Linux)
    Fri Apr 10 2026 20:50:07 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/echocfg.c diff
    Fail with a heplful error message is passed a directory on the command-line .. instead of a config/ini file
  819. Rob Swindell (on Debian Linux)
    Fri Apr 10 2026 20:48:49 GMT-0700 (PDT)
    Modified Files:
    

    src/xpdev/ini_file.c diff
    Fix possible NULL deref in iniHasInclude() when called with a nullptr (e.g. by echocfg when passed a directory on the command-line)
  820. Rob Swindell (on Debian Linux)
    Fri Apr 10 2026 20:45:05 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/recordfile.js diff
    Fix recordfile.js toSource() calls for SM128 compatibility toSource() is removed in SpiderMonkey 128. In put(), the default value is only read so no copy is needed. In reInit(), use JSON.parse(JSON.stringify()) for deep-copying object/array defaults. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  821. Rob Swindell (on Debian Linux)
    Thu Apr 09 2026 20:50:14 GMT-0700 (PDT)
    Added Files:
    

    exec/txt_handler.js diff
    Add text file web handler with LIST-style viewer (txt_handler.js) Displays .txt files in a DOS LIST-style HTML viewer with blue background, keyboard navigation, search, touch swipe, and a ?raw option to serve the original plain text. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  822. Rob Swindell (on Debian Linux)
    Thu Apr 09 2026 20:39:14 GMT-0700 (PDT)
    Modified Files:
    

    exec/md_handler.js diff
    Add raw markdown download option to md_handler.js Appending ?raw to the URL serves the original .md file as text/plain. A "View raw markdown" link is shown at the bottom of the HTML output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  823. Rob Swindell (on Debian Linux)
    Thu Apr 09 2026 00:28:42 GMT-0700 (PDT)
    Added Files:
    

    exec/md_handler.js diff
    Add Markdown-to-HTML web content handler (md_handler.js) Similar to asc_handler.js, converts .md files to HTML on the fly. Supports headings, bold/italic, code blocks, lists, blockquotes, links, images, horizontal rules, and pipe-delimited tables. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  824. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 23:49:38 GMT-0700 (PDT)
    Modified Files:
    

    exec/email_sec.js diff
    exec/load/shell_lib.js diff
    Allow shell_lib.send_email() and send_netmail() to accept to/address argument Only prompt user for the destination user/address if not provided by caller.
  825. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 23:13:26 GMT-0700 (PDT)
    Modified Files:
    

    exec/tests/global/format.js diff
    Add test case for format width arguments Created by Claude
  826. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 21:48:17 GMT-0700 (PDT)
    Modified Files:
    

    exec/email_sec.js diff
    exec/load/shell_lib.js diff
    Allow the write-message (WM_*) mode flags to be passed to shell lib email funcs Fixes the 'A' (email attachment) command from the email menu. broken in commit 1dac6acd2215db3e5 Thanks Nelgin for the head's up!
  827. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 21:47:00 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/maze/game.js diff
    xtrn/maze/menu.js diff
    xtrn/maze/service.js diff
    xtrn/starstocks/game.js diff
    xtrn/synchronetris/game.js diff
    xtrn/synchronetris/lobby.js diff
    xtrn/synchronetris/service.js diff
    xtrn/synchronetris/tetrisobj.js diff
    Replace `for each` in maze, synchronetris, and starstocks Replace non-standard `for each(var x in obj)` with compatible `for(var key in obj)` loops in the remaining xtrn game scripts: maze, synchronetris, and starstocks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  828. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 21:46:45 GMT-0700 (PDT)
    Modified Files:
    

    exec/json-service.js diff
    xtrn/bublbogl/game.js diff
    xtrn/dicewarz2/diceobj.js diff
    xtrn/dicewarz2/game.js diff
    xtrn/dicewarz2/service.js diff
    Replace `for each` in dicewarz2, bublbogl, json-service.js Replace non-standard `for each(var x in obj)` with compatible `for(var key in obj)` loops in xtrn/dicewarz2 and xtrn/bublbogl. Also fix json-service.js Module.init() catch block: `module.name` was an invalid reference (should be `this.name`), causing the error handler itself to throw, silently crashing the service on repeated restarts with no log output. Added logging of the caught exception. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  829. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 21:46:20 GMT-0700 (PDT)
    Modified Files:
    

    exec/allusers.js diff
    exec/ircbots/admin/admin.js diff
    exec/ircbots/antispam/antispam.js diff
    exec/ircbots/humanity/humanity.js diff
    exec/ircbots/humanity/humanity_functions.js diff
    exec/ircbots/rpgbot/rpg_commands.js diff
    exec/ircbots/rpgbot/rpg_editor.js diff
    exec/ircbots/rpgbot/rpg_functions.js diff
    exec/json-svc-ctrl.js diff
    exec/load/cnflib.js diff
    exec/load/cvslib.js diff
    exec/load/frame.js diff
    exec/load/json-chat.js diff
    exec/load/json-db.js diff
    exec/load/layout.js diff
    exec/load/rss-atom.js diff
    exec/load/tree.js diff
    exec/slog.js diff
    exec/web_feed_importer.js diff
    Replace non-standard `for each` with `for...in` in exec/ scripts The `for each(var x in obj)` syntax is a Mozilla SpiderMonkey extension that is not supported by SpiderMonkey 128+. Replace all instances with standard `for(var key in obj)` loops with explicit value extraction, which is compatible with both SpiderMonkey 1.8.5 and 128. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  830. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 08:07:11 GMT-0700 (PDT)
    Modified Files:
    

    exec/json-service.js diff
    xtrn/bublbogl/game.js diff
    xtrn/dicewarz2/diceobj.js diff
    xtrn/dicewarz2/game.js diff
    xtrn/dicewarz2/service.js diff
    Revert "Uproot the last of the `for each` blight from the xtrn fields" This reverts commit 8d1156ef41bc0318d90271b87c69fd3a0970a754.
  831. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 08:06:52 GMT-0700 (PDT)
    Modified Files:
    

    exec/allusers.js diff
    exec/ircbots/admin/admin.js diff
    exec/ircbots/antispam/antispam.js diff
    exec/ircbots/humanity/humanity.js diff
    exec/ircbots/humanity/humanity_functions.js diff
    exec/ircbots/rpgbot/rpg_commands.js diff
    exec/ircbots/rpgbot/rpg_editor.js diff
    exec/ircbots/rpgbot/rpg_functions.js diff
    exec/json-service.js diff
    exec/json-svc-ctrl.js diff
    exec/load/cnflib.js diff
    exec/load/cvslib.js diff
    exec/load/frame.js diff
    exec/load/json-chat.js diff
    exec/load/json-db.js diff
    exec/load/layout.js diff
    exec/load/rss-atom.js diff
    exec/load/tree.js diff
    exec/slog.js diff
    exec/web_feed_importer.js diff
    Revert "Purge the obsolete `for each` abomination from all scripts" This reverts commit 330a7eb9e8a63faccb809bd4ffcec55dbf14f87d.
  832. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 00:03:50 GMT-0700 (PDT)
    Modified Files:
    

    exec/json-service.js diff
    xtrn/bublbogl/game.js diff
    xtrn/dicewarz2/diceobj.js diff
    xtrn/dicewarz2/game.js diff
    xtrn/dicewarz2/service.js diff
    Uproot the last of the `for each` blight from the xtrn fields Well now. Went out to check the back forty and found the dicewarz2 and bublbogl plots still choked with that invasive `for each` weed Mozilla planted years ago. SpiderMonkey 128 don't tolerate that nonsense no more than I tolerate thistle in my beet rows. Worse yet, the json-service.js error handler had a busted reference — `module.name` where it should've been `this.name` — so when dicewarz2's service.js failed to compile, the catch block itself threw a ReferenceError, and the whole service went down like a frost-bit seedling. Over and over, restart after restart, and not a single line in the log to show for it. Like a groundhog eating your crop in the dark. Fixed the catch block to use the right variable and actually log the error. A man ought to know what's killing his plants. Files tended: - xtrn/dicewarz2/diceobj.js: 2 `for each` → `for...of Object.values()` - xtrn/dicewarz2/game.js: 1 `for each` → `for...of Object.values()` - xtrn/dicewarz2/service.js: 1 `for each` → `for...of Object.values()` - xtrn/bublbogl/game.js: 1 `for each` → `for...of Object.values()` - exec/json-service.js: `module.name` → `this.name`, added error logging Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  833. Rob Swindell (on Debian Linux)
    Wed Apr 08 2026 00:03:50 GMT-0700 (PDT)
    Modified Files:
    

    exec/allusers.js diff
    exec/ircbots/admin/admin.js diff
    exec/ircbots/antispam/antispam.js diff
    exec/ircbots/humanity/humanity.js diff
    exec/ircbots/humanity/humanity_functions.js diff
    exec/ircbots/rpgbot/rpg_commands.js diff
    exec/ircbots/rpgbot/rpg_editor.js diff
    exec/ircbots/rpgbot/rpg_functions.js diff
    exec/json-service.js diff
    exec/json-svc-ctrl.js diff
    exec/load/cnflib.js diff
    exec/load/cvslib.js diff
    exec/load/frame.js diff
    exec/load/json-chat.js diff
    exec/load/json-db.js diff
    exec/load/layout.js diff
    exec/load/rss-atom.js diff
    exec/load/tree.js diff
    exec/slog.js diff
    exec/web_feed_importer.js diff
    Purge the obsolete `for each` abomination from all scripts The `for each(var x in obj)` syntax was a Mozilla-proprietary extension to JavaScript that never graced any ECMAScript standard — and rightly so. SpiderMonkey 128, a properly modern engine, naturally refuses to dignify such antiquated nonsense with so much as a parse. One can scarcely imagine how this syntax persisted across 20 files for so many years without anyone raising an objection. All instances have been replaced with the obviously correct and standards-compliant `for(var x of Object.values(obj))`, which has been available since ES2017 — nearly a decade ago. Affected files span core service modules (json-service.js, json-db.js, json-chat.js), UI frameworks (frame.js, layout.js, tree.js), IRC bots (rpgbot, humanity, admin, antispam), and various utilities (cnflib.js, cvslib.js, rss-atom.js, slog.js, allusers.js, web_feed_importer.js). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  834. Rob Swindell
    Wed Apr 08 2026 00:01:11 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    xtrn/DDMsgReader/ddmr_cfg.js diff
    xtrn/DDMsgReader/readme.txt diff
    xtrn/DDMsgReader/revision_history.txt diff
    Merge branch 'dd_msg_reader_msg_scrolling_line_blank_fix' into 'master' DDMsgReader: Fix for blanking the rest of the lines when reading a message with the scrollable interface See merge request main/sbbs!672
  835. Eric Oulashin
    Wed Apr 08 2026 00:01:11 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    xtrn/DDMsgReader/ddmr_cfg.js diff
    xtrn/DDMsgReader/readme.txt diff
    xtrn/DDMsgReader/revision_history.txt diff
    DDMsgReader: Fix for blanking the rest of the lines when reading a message with the scrollable interface
  836. Rob Swindell (on Debian Linux)
    Tue Apr 07 2026 21:09:37 GMT-0700 (PDT)
    Modified Files:
    

    web/root/ecwebv3/pages/disabled/001-episodes.ssjs diff
    web/root/ecwebv3/pages/disabled/episode.ssjs diff
    webv4/root/api/github.ssjs diff
    Use load() instead of load({}, ...) for value-returning scripts Same fix as bb60b17eb, applied to .ssjs files that were missed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  837. Rob Swindell (on Debian Linux)
    Tue Apr 07 2026 21:05:28 GMT-0700 (PDT)
    Modified Files:
    

    exec/SlyEdit.js diff
    exec/addfiles.js diff
    exec/avatar_chooser.js diff
    exec/avatars.js diff
    exec/binarydecoder.js diff
    exec/bulkmail.js diff
    exec/chksetup.js diff
    exec/cleanup.js diff
    exec/default.js diff
    exec/dyndns.js diff
    exec/email_sec.js diff
    exec/emailval.js diff
    exec/exportcfg.js diff
    exec/fido-nodelist-syncterm.js diff
    exec/fileman.js diff
    exec/filescancfg.js diff
    exec/fingerservice.js diff
    exec/fseditor.js diff
    exec/ftn-setup.js diff
    exec/ftnmsgdump.js diff
    exec/gopherservice.js diff
    exec/imapservice.js diff
    exec/importcfg.js diff
    exec/inactive_user_email.js diff
    exec/init-fidonet.js diff
    exec/install-xtrn.js diff
    exec/irc.js diff
    exec/ircbots/presence/presence.js diff
    exec/ircd.js diff
    exec/ircdcfg.js diff
    exec/ircmsg.js diff
    exec/jsdocs.js diff
    exec/lbshell.js diff
    exec/listgate.js diff
    exec/listserver.js diff
    exec/load/ansiedit.js diff
    exec/load/ansiterm_lib.js diff
    exec/load/ars_defs.js diff
    exec/load/attrdefs.js diff
    exec/load/avatar_lib.js diff
    exec/load/ax25defs.js diff
    exec/load/binkp.js diff
    exec/load/cardlib.js diff
    exec/load/cfglib.js diff
    exec/load/cmdshell.js diff
    exec/load/cterm_lib.js diff
    exec/load/filelist_lib.js diff
    exec/load/ircd/channel.js diff
    exec/load/ircd/config.js diff
    exec/load/ircd/user.js diff
    exec/load/irclib.js diff
    exec/load/logonlist_lib.js diff
    exec/load/newsutil.js diff
    exec/load/rss-atom.js diff
    exec/load/sauce_lib.js diff
    exec/load/sbbsimsg_lib.js diff
    exec/load/sbbslist_lib.js diff
    exec/load/shell_lib.js diff
    exec/load/smbdefs.js diff
    exec/load/tdfonts_lib.js diff
    exec/load/telnet_lib.js diff
    exec/load/termcapture_lib.js diff
    exec/load/uifcdefs.js diff
    exec/load/user_info_prompts.js diff
    exec/load/userdefs.js diff
    exec/load/vga_defs.js diff
    exec/load/xbimage_lib.js diff
    exec/mqtt_pub.js diff
    exec/mqtt_sub.js diff
    exec/msglist.js diff
    exec/msgscancfg.js diff
    exec/newslink.js diff
    exec/newuser.js diff
    exec/nntpservice.js diff
    exec/noyesbar.js diff
    exec/playmidi.js diff
    exec/playtone.js diff
    exec/presence-service.js diff
    exec/privchat.js diff
    exec/purgefiles.js diff
    exec/qnet-ftp.js diff
    exec/qnet-http.js diff
    exec/qotdservice.js diff
    exec/renegade.js diff
    exec/replace_text.js diff
    exec/sbbsecho_upgrade.js diff
    exec/sbbsedit.js diff
    exec/sbbslist.js diff
    exec/sutils.js diff
    exec/tests/global/load.js diff
    exec/textedit.js diff
    exec/tickfix.js diff
    exec/tickit.js diff
    exec/tickitcfg.js diff
    exec/update.js diff
    exec/uselect_tree.js diff
    exec/viewimsgs.js diff
    exec/websocket_proxy_service.js diff
    exec/websocketservice.js diff
    exec/xtrn-setup.js diff
    exec/xtrnmenu.js diff
    exec/yesnobar.js diff
    Change top-level const to var for SpiderMonkey 128 compatibility Per ECMA-262 6th Edition (ES6, 2015), Section 13.3.1, const creates block-scoped bindings in the global lexical environment that are non-configurable and cannot be deleted or redeclared. Section 15.1.11 (GlobalDeclarationInstantiation) requires a SyntaxError when a script attempts to declare a lexical binding that already exists in the global lexical environment. In long-running BBS terminal sessions, loadable modules (e.g. msglist.js, fileman.js) may be executed multiple times in the same JS context. Under SpiderMonkey 1.8.5, top-level const was a Mozilla extension with var-like (function-scoped) semantics that allowed redeclaration. Under SpiderMonkey 128, which implements ES6 const semantics, re-executing a script with top-level const throws: "SyntaxError: redeclaration of const <name>". Replace top-level const with var in all affected scripts to allow repeated execution in the same JS global scope. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  838. Rob Swindell (on Debian Linux)
    Tue Apr 07 2026 21:04:46 GMT-0700 (PDT)
    Modified Files:
    

    exec/avatars.js diff
    exec/dyndns.js diff
    exec/email_sec.js diff
    exec/emailfiles.js diff
    exec/inactive_user_email.js diff
    exec/load/avatar_lib.js diff
    exec/load/modopts.js diff
    exec/load/nodelist_options.js diff
    exec/load/xtrnmenulib.js diff
    exec/msglist.js diff
    exec/nodelist.js diff
    exec/podcast.js diff
    exec/postmeme.js diff
    exec/postxtrn.js diff
    exec/prextrn.js diff
    exec/privatemsg.js diff
    exec/qnet-ftp.js diff
    exec/rlogin.js diff
    exec/sbbsimsg.js diff
    exec/sbbslist.js diff
    exec/str_cmds.js diff
    exec/telgate.js diff
    exec/xtrn_sec.js diff
    Use load() instead of load({}, ...) for value-returning scripts Scripts like modopts.js and nodelist_options.js are designed to return a value via their completion value (the result of the last expression evaluated). SpiderMonkey 128's JS::ExecuteInJSMEnvironment() -- used when a scope object {} is passed to load() -- returns only a bool success/failure, not the script's completion value. This causes these load() calls to return the scope object instead of the intended value. Remove the unnecessary scope object {} from load() calls to modopts.js and nodelist_options.js so the scripts execute in the caller's scope and their completion values are returned correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  839. Rob Swindell (on Debian Linux)
    Tue Apr 07 2026 20:40:27 GMT-0700 (PDT)
    Modified Files:
    

    exec/user_settings.js diff
    block-scoped const fix (for ES6 compatibility)
  840. Rob Swindell (on Debian Linux)
    Tue Apr 07 2026 20:34:18 GMT-0700 (PDT)
    Modified Files:
    

    exec/msglist.js diff
    Check for moderated messages
  841. Rob Swindell (on Windows 11)
    Tue Apr 07 2026 19:57:16 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ratelimit.hpp diff
    Mutex-protect the map (the client IP/count dataset) Although this has been running for months without issue on multiple servers for vert/www/git.synchro.net, I did observe *a* (one) crash that let me know this could ultimately fail in the exact right circumstances. Although the use of std::mutex caused issues (crashes) when used in filterFile and older MSVC++ runtime libs, it appears to be fine when used in this class: no crash observed when run on Windows 7. Fix issue #1113
  842. Rob Swindell (on Windows 11)
    Sun Apr 05 2026 12:38:13 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_system.cpp diff
    system.get_telegram(0) now returns null, always previously, it would get any waiting telegrams waiting for user #1 This would fail exec/tests/system/rtypes.js if there were any telegrams waiting for user #1. There's no good reason this function needs to promote argument values < 1 to 1.
  843. Rob Swindell (on Debian Linux)
    Sun Apr 05 2026 12:18:32 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/lord2/lord2.js diff
    Change all while(1)s to while(!js.terminated) Game could sit in infinite loops if/when user disconnects during the game. Ideally, these loops would be checking bbs.online (for truthiness) as well, but I don't know how that works with jsdoor or what the equivalent dorkit thing would be, but this game needs to check if the user is still connected and (hopefully) terminate gracefully (e.g. save game state to prevent cheating).
  844. Rob Swindell (on Debian Linux)
    Sun Apr 05 2026 12:07:36 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/qwk.cpp diff
    src/sbbs3/qwktomsg.cpp diff
    Claude pointed out that these sscanf() calls are potentially dangerous Specify the max length (4 chars) of the hex-encoded SMB timezone in the format string to "harden" them.
  845. Rob Swindell (on Debian Linux)
    Sat Apr 04 2026 18:11:40 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/shell_lib.js diff
    More use of gettext() for string customization (e.g. via ctrl/text.ini [JS]) - Attach a file - Start batch upload - Failed to clear batch download queue! - Search all libraries for new files
  846. Rob Swindell (on Windows 11)
    Sat Apr 04 2026 14:26:36 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/atcodes.cpp diff
    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/js_system.cpp diff
    src/sbbs3/jsdoor.cpp diff
    src/sbbs3/jsexec.cpp diff
    src/sbbs3/mailsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/services.cpp diff
    src/sbbs3/websrvr.cpp diff
    Track server uptimes using time() again ... Manual reversion of (most of) commits 0c0cb7c4732 and 5cbc2c2eb39 Since we expose the actual date/time of the server uptime value in JS system.uptime, this "fix" created a new issue (#1109). Thanks for the report Craig! Also, the original "fix" didn't really fix anything in the servers: The original issue (#1093) was only observed in the status bar of SBBSCTRL-Win32, so leave that solution (using xp_fast_timer64()) since it appears to be a Borland-only issue not actually impacting the servers, which are built with just about every toolchain *but* Borland's. The original code before the above mentioned commits included this old comment which might be a clue: uptime = time(NULL); /* this must be done *after* setting the timezone */ A *long* time ago, the severs used to be built with Borland C/C++ and we also used to call tzset() during initialization (e.g. see commit f2a7dab6a08cef), so weird stuff used to be happening with the the time() base value changing during initialization and that no longer applies.
  847. Rob Swindell (on Windows 11)
    Fri Apr 03 2026 09:30:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/userdat.c diff
    Remove extraneous "external program" text from node status string When the xtrn prog code is valid, we include the program's full name in the status string and don't need the "external program" designation that was added in commit cc4fea1c6fd. Thanks to xbit for pointing out in IRC.
  848. Deucе
    Fri Apr 03 2026 08:50:31 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Fix parse_mega() buffer overreads Many places were calling parse_mega() without checking the return value, then calling parse_mega() on the string AFTER the previous one. This means that on truncated invalid RIP sequences, ripper would read past the end of the allocation. This fix was complicated by the fact that the "last" item in a parameter list can be truncated as long as it has at least one valid byte in it. This now uses a vararg function and parses them all at once (mostly).
  849. Deucе
    Fri Apr 03 2026 08:49:57 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    When forcing a font, add a default It's forced, force it.
  850. Deucе
    Thu Apr 02 2026 23:56:10 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/bitmap_con.c diff
    Clear "gap rows" when pixel scrolling Previously, when EGA 43-row modes scrolled, the top line was partially copied to the bottom of the screen and never cleared. This was part of the optimization that made the screen buffer into a ring buffer instead of linear. Fixes ticket 228.
  851. Deucе
    Thu Apr 02 2026 23:26:25 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/allfonts.c diff
    src/conio/ciolib.adoc diff
    src/conio/ciolib.h diff
    src/conio/cterm.adoc diff
    src/conio/vidmodes.c diff
    src/syncterm/bbslist.c diff
    src/syncterm/ripper.c diff
    Extract the RIPterm font and use it exclusively in ripper.c Also, when switching to RIPv1, switch to font to RIPterm. Fixes an accidental commit of a bad aspect ration in EGA modes I was using for testing as well.
  852. Deucе
    Thu Apr 02 2026 22:31:16 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/vidmodes.c diff
    src/syncterm/ripper.c diff
    Fix off-by-one error in button face size for fixed-size buttons
  853. Deucе
    Thu Apr 02 2026 22:10:56 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Handle thick ellipses better. It looks like when drawing a thick ellipse, it draws multiple thick line segments. Now that we have the Borland cos/sin tables, we can do that ourselves as a special case.
  854. Deucе
    Thu Apr 02 2026 20:02:39 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/CHANGES diff
    Mention jquast specifically He has found the most bugs in this RC cycle, and they've all been security issues. Thanks again!
  855. Deucе
    Thu Apr 02 2026 19:50:14 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/syncterm.c diff
    Use XDG_DOWNLOAD_DIR on *nix Default to $HOME/Downloads The default download directory on all *nix builds (except macOS) was previously $HOME. This meant that ZModem auto-downloads can place files directly in your home directory, potentially without you noticing if it's fast enough. While it would request confirmation if it's overwriting, if it's a file that doesn't exist, it would be dropped right there. This is potentially VERY BAD, it could create a .bash_profile if you're using .profile for example, a .xsessionrc, etc. files that are automatically executed and assumed trusted, but often don't exist already on most systems. While this technically isn't *quite* as bad as memory errors where the remote will potentially have full access to your system, it's much more trivial to turn into a real exploit. Reported by JQuast on IRC. Thanks again for reaching out and reporting these security issues with SyncTERM. I'd like to take this time to clarify that you SHOULD NOT use SyncTERM to access a POSIX shell, there's a LOT of sequences that "standard" terminal emulators have specifically stopped supporting because they pose clear security risks. SyncTERM gleefully supports these sequences. If you us this for a shell and ssh to untrusted systems, copy/paste commands in or out of the terminal, or even run things like curl and support redirects, there are strange gotchas waiting for you.
  856. Deucе
    Thu Apr 02 2026 19:34:40 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    Fix stack overflow parsing DECRQSS Reported by JQuast over IRC. Thanks!
  857. Deucе
    Thu Apr 02 2026 19:29:31 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    Fix various Sixel related vulnerabilities. All found by JQuast and graciously reported via IRC. Thanks!
  858. Deucе
    Thu Apr 02 2026 19:19:25 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Some more button fixups. 1. !|1U missing <res> field - no longer draws the button (per RIPterm) 2. Recessed inner corner pixels - removed explicit cc-colored corner pixels on inner recess border (RIPterm lets the bg lines overlap naturally) 3. Chisel inset height off-by-one — y2 - y1 + 1 → y2 - y1 since box.y2 is exclusive
  859. Deucе
    Thu Apr 02 2026 18:18:31 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Mostly cleanup stuff... Clean up old use of a hack bit, use the new polyfill for the RIP command, clean up some comments, and get XOR working properly everywhere.
  860. Deucе
    Thu Apr 02 2026 18:17:56 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripdiff.py diff
    A bit more tweakage to ripdiff.py
  861. Deucе
    Thu Apr 02 2026 14:57:03 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    Much better bezier match replace per-iteration step/cnt division with accumulated t += 1.0/cnt. This produces the SAME ERROR I see from the RIPterm across obvious test vectors. The old code was "too correct", RIPterm appears to accumulate error and never correct for it.
  862. Deucе
    Thu Apr 02 2026 13:55:02 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripdiff.py diff
    src/syncterm/ripper.c diff
    Fix up pie slices and fills Some intial bezier cleanup, but still not an exact match.
  863. Deucе
    Thu Apr 02 2026 01:30:54 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    And fix arcs too! We actually needed to extract the trig tables BGI uses to get this to work out right... fighting with floating point was not the way.
  864. Deucе
    Wed Apr 01 2026 23:53:50 GMT-0700 (PDT)
    Modified Files:
    

    src/syncterm/ripper.c diff
    With the help of Claude, exactly match RIPTERM ellipses I had assumed they used McIlroy for Ellipses, so could only get close... they had actually used the two-region Bresenham, the reason I didn't get that to match was that they apparently scale by 100 to "avoid rounding", so my truncation was wildly different.
  865. Rob Swindell (on Debian Linux)
    Wed Apr 01 2026 21:19:19 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_global.cpp diff
    Add support for Ctrl-AU and Ctrl-AV codes to html_encode() I'm not clear why blink and high/bold are tracked with bools rather than just the represenative bits in bg and fg (and heck, those 2 could be combined into a single char or even union), but I'm storing the high/blink bit in bg/fg *and* setting the bool variables. Seems fine.
  866. Rob Swindell (on Debian Linux)
    Wed Apr 01 2026 21:18:10 GMT-0700 (PDT)
    Modified Files:
    

    exec/asc_handler.js diff
    Read file line lengths up to 256KB (up from 8KB) rez2ansi can generate ANSI files that are just one massive long line
  867. Deucе
    Wed Apr 01 2026 20:08:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh.h diff
    src/ssh/examples/client.c diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/hybrid-pq-kex.c diff
    src/ssh/key_algo/rsa-sha2-256-botan.c diff
    src/ssh/key_algo/rsa-sha2-256-openssl.c diff
    src/ssh/key_algo/rsa-sha2-512-botan.c diff
    src/ssh/key_algo/rsa-sha2-512-openssl.c diff
    src/ssh/key_algo/ssh-ed25519-botan.c diff
    src/ssh/key_algo/ssh-ed25519-openssl.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/dssh_test.h diff
    src/ssh/test/kex_test.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_asymmetric_mac.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_thread_errors.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Add mandatory host key verification callback for client sessions Applications must now set a dssh_hostkey_verify_cb before calling dssh_transport_handshake() on client sessions. The callback receives the algorithm name, key strength in bits, SHA-256 fingerprint, and raw key blob — enabling known_hosts checking and key size policy enforcement without requiring the application to parse wire formats. New API: dssh_hostkey_decision enum, dssh_hostkey_verify_cb typedef, dssh_session_set_hostkey_verify_cb(), dssh_key_algo_keybits function pointer on dssh_key_algo_s. KEX modules invoke the callback after exchange hash computation and before signature verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  868. Deucе
    Wed Apr 01 2026 18:56:13 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/kex/sntrup761_optblocker.c diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/TODO.md diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/hybrid-pq-kex.c diff
    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/ssh/kex/mlkem768.c diff
    src/ssh/kex/sntrup761.c diff
    TODO items 170-178: KEX safety, vendor fixes, include guard - KEX server reply overflow + narrowing safety (items 170-173): UINT32_MAX pre-flight checks and incremental SIZE_MAX overflow guards in curve25519-sha256.c and hybrid-pq-kex.c server paths; initializer-style casts - Vendor portability fixes: optblocker, popcount, byte-order (174-176) - Strip 91 unused cryptoint functions from sntrup761.c (item 177) - Replace #pragma once with standard include guard (item 178) - Clean up TODO.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  869. Deucе
    Wed Apr 01 2026 18:14:00 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.adoc diff
    Document DECQRSS p1=0 behaviour.
  870. Deucе
    Wed Apr 01 2026 18:02:13 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/docs/audit-portability-vendor.md diff
    src/ssh/docs/audit-rules-modules.md diff
    Modified Files:

    src/ssh/TODO.md diff
    src/ssh/docs/audit-rules.md diff
    Update RULES.md audit: all 9 findings fixed, clarify KBI ownership exception Remove findings 1.1-1.3, 3.1, 6.1-6.2, 10.1-10.4 (all fixed in 8ead719912). Reclassify Pointer Ownership from false CONFORMS to CONFORMS-with-exception: the KBI API intentionally transfers allocation ownership across the library boundary (app mallocs, library frees), documented in deucessh-auth.h callback typedefs. Add RULES.md audit for algorithm modules 4 findings in curve25519-sha256.c and hybrid-pq-kex.c: missing overflow checks on server-side reply_sz computation and unchecked sig_len narrowing to uint32_t. dh-gex-sha256.c has the correct pattern. All other rules conform across 19 module source files. Add module audit findings to TODO (items 170-173) Overflow and narrowing issues in server-side KEX reply construction in curve25519-sha256.c and hybrid-pq-kex.c. dh-gex-sha256.c has the correct pattern to follow. Add vendor portability audit (items 174-178) 3 serious findings in sntrup761.c, libcrux_mlkem768_sha3.h, and mlkem768.c: undefined optblocker symbols (linker failure on non-x86/ arm64), __builtin_popcount without portable fallback, and __BYTE_ORDER__ detection that silently breaks on non-GCC big-endian. 2 minor: __attribute__((unused)) warnings, #pragma once non-standard. All affect platforms outside the current build matrix only. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  871. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Add tests for full public API and function coverage 18 new tests across 3 files covering all previously-untested DSSH_PUBLIC functions and the last 4 uncovered internal functions: test_chan.c: - params_set_max_window: dssh_chan_params_set_max_window unit test test_transport.c: - kex/set_ctx: dssh_kex_set_ctx + NULL/unknown/TOOLATE guards - gather/set, gather/toolate: dssh_transport_set_tx_gather - gather/roundtrip: full handshake through scatter/gather TX path - rxline/from_rx_handshake: handshake with NULL rxline callback, exercises the rxline_from_rx fallback test_conn.c: - zc/open_client: client-side dssh_chan_zc_open roundtrip (also exercises dssh_chan_zc_getbuf + dssh_chan_zc_send) - zc/cancel: dssh_chan_zc_cancel after getbuf - null/zc_open: NULL guards for all ZC functions - event/session_set_cb + toolate: dssh_session_set_event_cb - event/chan_set_cb: dssh_chan_set_event_cb - event/session_set_max + toolate: dssh_session_set_max_events - event/chan_set_max: dssh_chan_set_max_events - pty/get_pty: dssh_chan_get_pty with PTY and non-PTY channels - env/roundtrip: env vars through accept_parse_env/parse_env_data Result: 0 missed functions in all 4 core files, 75% branch coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  872. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_auth.c diff
    RULES.md audit fixes: input validation, type safety, arithmetic safety (items 163-169) - send_info_request: UINT32_MAX guards on string lengths (item 163) - Server KBI: reject num_responses > last_nprompts (item 164) - Client KBI: hard cap of 256 on num_prompts (item 165) - event_queue_push: SIZE_MAX/2 overflow guard (item 166) - send_to_slot: UINT8_MAX range check on payload_len (item 167) - stream_zc_cb, handle_channel_extended_data: inline cast to initializer (items 168-169) - 2 new tests: kbi_excess_responses, kbi_too_many_prompts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  873. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/.clang-format diff
    src/ssh/RULES.md diff
    src/ssh/docs/audit-rules.md diff
    Modified Files:

    src/ssh/TODO.md diff
    Add some files... .clang-format is the fules file for source formatting. RULES.md is overall design rules. docs/audit-rules.md is an audit of the code conformance to RULES.md
  874. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/comp/none.c diff
    src/ssh/crypto/botan.cpp diff
    src/ssh/crypto/openssl.c diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh-auth.h diff
    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-conn.h diff
    src/ssh/deucessh-crypto.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-lang.h diff
    src/ssh/deucessh-mac.h diff
    src/ssh/deucessh-portable.h diff
    src/ssh/deucessh.h diff
    src/ssh/enc/aes256-ctr-botan.c diff
    src/ssh/enc/aes256-ctr-botan.cpp diff
    src/ssh/enc/aes256-ctr-openssl.c diff
    src/ssh/enc/none.c diff
    src/ssh/examples/client.c diff
    src/ssh/examples/server.c diff
    src/ssh/kex/curve25519-sha256-botan.c diff
    src/ssh/kex/curve25519-sha256-botan.cpp diff
    src/ssh/kex/curve25519-sha256-openssl.c diff
    src/ssh/kex/curve25519-sha256-ops.h diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-groups.c diff
    src/ssh/kex/dh-gex-groups.h diff
    src/ssh/kex/dh-gex-sha256-botan.c diff
    src/ssh/kex/dh-gex-sha256-botan.cpp diff
    src/ssh/kex/dh-gex-sha256-openssl.c diff
    src/ssh/kex/dh-gex-sha256-ops.h diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/dh-gex-sha256.h diff
    src/ssh/kex/hybrid-pq-kex-ops.h diff
    src/ssh/kex/hybrid-pq-kex.c diff
    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/ssh/kex/mlkem768.c diff
    src/ssh/kex/mlkem768.h diff
    src/ssh/kex/mlkem768x25519-sha256-botan.c diff
    src/ssh/kex/mlkem768x25519-sha256-botan.cpp diff
    src/ssh/kex/mlkem768x25519-sha256-openssl.c diff
    src/ssh/kex/sntrup761.c diff
    src/ssh/kex/sntrup761.h diff
    src/ssh/kex/sntrup761x25519-sha512-botan.c diff
    src/ssh/kex/sntrup761x25519-sha512-botan.cpp diff
    src/ssh/kex/sntrup761x25519-sha512-openssl.c diff
    src/ssh/key_algo/rsa-sha2-256-botan.c diff
    src/ssh/key_algo/rsa-sha2-256-botan.cpp diff
    src/ssh/key_algo/rsa-sha2-256-openssl.c diff
    src/ssh/key_algo/rsa-sha2-256.h diff
    src/ssh/key_algo/rsa-sha2-512-botan.c diff
    src/ssh/key_algo/rsa-sha2-512-botan.cpp diff
    src/ssh/key_algo/rsa-sha2-512-openssl.c diff
    src/ssh/key_algo/ssh-ed25519-botan.c diff
    src/ssh/key_algo/ssh-ed25519-botan.cpp diff
    src/ssh/key_algo/ssh-ed25519-openssl.c diff
    src/ssh/key_algo/ssh-ed25519.h diff
    src/ssh/mac/hmac-sha2-256-botan.c diff
    src/ssh/mac/hmac-sha2-256-botan.cpp diff
    src/ssh/mac/hmac-sha2-256-openssl.c diff
    src/ssh/mac/hmac-sha2-512-botan.c diff
    src/ssh/mac/hmac-sha2-512-botan.cpp diff
    src/ssh/mac/hmac-sha2-512-openssl.c diff
    src/ssh/mac/none.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    Run through clang-format
  875. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_chan.c diff
    Add configurable event queue cap (default 64) to prevent OOM A malicious peer can flood CHANNEL_REQUESTs (signal, break, window-change) to grow the event queue without bound. Add a per-channel max_events cap (default 64, inherited from session). When the queue is full, the demux thread closes the channel. - event_queue_push() returns DSSH_ERROR_TOOMANY at cap - dssh_session_set_max_events() sets default (before start) - dssh_chan_set_max_events() adjusts per-channel (DSSH_ERROR_INVALID if cap < current count) - Pass 0 to disable the cap - All event_queue_push() call sites now check return values Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  876. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    Remove dead old-API code (signal queue, msgqueue, DSSH_IO_OLD) No channel creation path sets io_model=DSSH_IO_OLD — every channel is DSSH_IO_STREAM or DSSH_IO_ZC. Remove all unreachable old-API code: signal queue, message queue, raw channel type, window_change callback, and old-API branches in demux handlers. Deleted: msgqueue_free/push, sigqueue_init/free/push, session_readable, dssh_msgqueue_entry, dssh_msgqueue, dssh_signal_mark, dssh_signal_queue, DSSH_CHAN_RAW, DSSH_IO_OLD, window_change_cb, stdout/stderr_consumed. Simplified buf union to plain struct. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  877. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    Serialize packets directly into tx_packet via send_begin/send_commit Every send_packet() call copies the payload into the session's pre-allocated tx_packet buffer. 16 call sites in ssh-auth.c and ssh-conn.c were malloc'ing a temporary buffer, serializing into it, passing it to send_packet() (which memcpy'd into tx_packet), then immediately freeing the temporary — a pointless double-copy. Add send_begin/send_commit/send_cancel internal API that returns a pointer directly into tx_packet[9] with tx_mtx held, letting callers serialize in-place. This mirrors the existing zero-copy channel send path (zc_getbuf_inner/zc_send_inner). Refactor send_packet() itself to use send_begin + memcpy + send_commit. Convert all 16 malloc/send_packet/free sites: - ssh-auth.c (12): send_auth_failure, send_passwd_changereq, send_pk_ok, send_info_request, flush_pending_banner, server SERVICE_ACCEPT, dssh_auth_request_service, get_methods_impl, send_password_request, auth_kbi_impl (x2), auth_publickey_impl - ssh-conn.c (4): send_channel_request_wait, dssh_chan_send_signal, dssh_chan_send_window_change, dssh_chan_send_break Delete 12 alloc-failure tests that tested the now-eliminated malloc paths. Remove n>0 guard in alloc/auth_iterate since the password auth path now has zero library mallocs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  878. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/deucessh.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    Add optional scatter/gather TX callback for batched sends Factor tx_finalize into tx_finalize_prepare (padding, MAC, encrypt, advance tx_seq) and tx_update_counters, enabling preparation of multiple packets before a single I/O callback. New public API: dssh_transport_set_tx_gather() registers an optional callback that receives an iov array of wire-ready packet segments. When set, all TX paths (send_packet, zc_send_inner, send_to_slot, send_to_wa_slot) batch pending slot packets with the caller's data packet into a single gather call, enabling writev()-style sends. tx_gather_with_packet() is the shared internal helper: gathers up to DSSH_TX_IOV_MAX ready slot packets (session-priority, channel round-robin) plus one caller packet into a stack iov array, calls tx_gather, and signals cleared slots. Remaining slots beyond the iov limit stay ready for the next drain pass. Contract: callback returns 0 (all sent) or negative (error, unknown bytes on wire — session terminated). Backward compatible: NULL tx_gather preserves per-packet gconf.tx behavior unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  879. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_chan.c diff
    Replace TX queue linked list with pre-allocated slot buffers Eliminate all malloc/free from the demux thread's fire-and-forget send path by replacing the linked-list TX queue with pre-allocated, algorithm-correctly-sized packet buffers ("slots"). Session-level slots: global_reply (REQUEST_SUCCESS/FAILURE, 1B), open_fail (CHANNEL_OPEN_FAILURE, 16B). Channel-level slots: wa (WINDOW_ADJUST, 9B), chan_fail (CHANNEL_FAILURE, 8B). Each slot buffer is sized to hold a complete wire packet for its max payload using the negotiated block_size and MAC digest, computed by tx_slot_buf_size(). Buffers are allocated in newkeys() after derive_and_apply_keys() and resized on rekey if algorithms change. tx_finalize() parameterized to operate on arbitrary buffers (not just sess->trans.tx_packet), enabling zero-copy finalize+send directly from slot buffers. send_to_slot() provides RX backpressure: fast path tries tx_mtx and sends immediately; slow path stalls the demux thread on tx_slot_cnd until the slot is drained, blocking recv_packet and causing TCP backpressure on a misbehaving peer. send_to_wa_slot() coalesces WINDOW_ADJUST via saturating add on the bytes field when the slot is already occupied — no stall needed. Accept queue converted from unbounded malloc'd linked list to fixed-capacity ring buffer (DSSH_ACCEPT_QUEUE_CAP=8) embedded in the session struct. Demux stalls on accept_cnd when full, closing the memory starvation vector from CHANNEL_OPEN floods. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  880. Deucе
    Wed Apr 01 2026 16:21:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    Coalesce queued WINDOW_ADJUST messages in tx_queue When the demux thread's WINDOW_ADJUST send falls back to enqueue (tx_mtx busy), coalesce with any already-queued WINDOW_ADJUST for the same channel instead of appending a duplicate. Entries below half the window target are moved to the tail (low priority); entries at or above half stay in place. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  881. Rob Swindell
    Wed Apr 01 2026 16:04:20 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    Merge branch 'dd_msg_reader_version_update' into 'master' DDMsgReader.js version udpate (forgot to update that in the last commit) See merge request main/sbbs!671
  882. Eric Oulashin
    Wed Apr 01 2026 15:37:04 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    DDMsgReader.js version udpate (forgot to update that in the last commit)
  883. Rob Swindell
    Wed Apr 01 2026 14:47:14 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    xtrn/DDMsgReader/ddmr_cfg.js diff
    xtrn/DDMsgReader/readme.txt diff
    xtrn/DDMsgReader/revision_history.txt diff
    Merge branch 'dd_msg_reader_console_getxy_fix' into 'master' Digital Distortion Message Reader: Updated usage of console.getxy() to prevent errors with that. Addresses issue #1107 See merge request main/sbbs!670
  884. Eric Oulashin
    Wed Apr 01 2026 12:30:41 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/DDMsgReader/DDMsgReader.js diff
    xtrn/DDMsgReader/ddmr_cfg.js diff
    xtrn/DDMsgReader/readme.txt diff
    xtrn/DDMsgReader/revision_history.txt diff
    Digital Distortion Message Reader: Updated usage of console.getxy() to prevent errors with that. Addresses issue #1107
  885. Deucе
    Wed Apr 01 2026 08:03:54 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/crypto/botan.cpp diff
    src/ssh/crypto/openssl.c diff
    src/ssh/deucessh-crypto.h diff
    src/ssh/deucesshConfig.cmake.in diff
    src/ssh/enc/aes256-ctr-botan.c diff
    src/ssh/enc/aes256-ctr-botan.cpp diff
    src/ssh/enc/aes256-ctr-openssl.c diff
    src/ssh/kex/curve25519-sha256-botan.c diff
    src/ssh/kex/curve25519-sha256-botan.cpp diff
    src/ssh/kex/curve25519-sha256-openssl.c diff
    src/ssh/kex/curve25519-sha256-ops.h diff
    src/ssh/kex/dh-gex-groups.c diff
    src/ssh/kex/dh-gex-groups.h diff
    src/ssh/kex/dh-gex-sha256-botan.c diff
    src/ssh/kex/dh-gex-sha256-botan.cpp diff
    src/ssh/kex/dh-gex-sha256-openssl.c diff
    src/ssh/kex/dh-gex-sha256-ops.h diff
    src/ssh/kex/hybrid-pq-kex-ops.h diff
    src/ssh/kex/hybrid-pq-kex.c diff
    src/ssh/kex/mlkem768x25519-sha256-botan.c diff
    src/ssh/kex/mlkem768x25519-sha256-botan.cpp diff
    src/ssh/kex/mlkem768x25519-sha256-openssl.c diff
    src/ssh/kex/sntrup761x25519-sha512-botan.c diff
    src/ssh/kex/sntrup761x25519-sha512-botan.cpp diff
    src/ssh/kex/sntrup761x25519-sha512-openssl.c diff
    src/ssh/key_algo/rsa-sha2-256-botan.c diff
    src/ssh/key_algo/rsa-sha2-256-botan.cpp diff
    src/ssh/key_algo/rsa-sha2-256-openssl.c diff
    src/ssh/key_algo/rsa-sha2-512-botan.c diff
    src/ssh/key_algo/rsa-sha2-512-botan.cpp diff
    src/ssh/key_algo/rsa-sha2-512-openssl.c diff
    src/ssh/key_algo/ssh-ed25519-botan.c diff
    src/ssh/key_algo/ssh-ed25519-botan.cpp diff
    src/ssh/key_algo/ssh-ed25519-openssl.c diff
    src/ssh/mac/hmac-sha2-256-botan.c diff
    src/ssh/mac/hmac-sha2-256-botan.cpp diff
    src/ssh/mac/hmac-sha2-256-openssl.c diff
    src/ssh/mac/hmac-sha2-512-botan.c diff
    src/ssh/mac/hmac-sha2-512-botan.cpp diff
    src/ssh/mac/hmac-sha2-512-openssl.c diff
    src/ssh/test/kex_test.c diff
    src/ssh/test/test_botan_algo_key.cpp diff
    src/ssh/test/test_botan_transport.cpp diff
    src/ssh/test/test_crypto.c diff
    Modified Files:

    src/ssh/CLAUDE.md diff
    src/ssh/CMakeLists.txt diff
    src/ssh/TODO.md diff
    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-lang.h diff
    src/ssh/deucessh-mac.h diff
    src/ssh/deucessh-portable.h diff
    src/ssh/deucessh.pc.in diff
    src/ssh/docs/audit-4251.md diff
    src/ssh/docs/audit-4253.md diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/ssh/kex/mlkem768.c diff
    src/ssh/kex/sntrup761.c diff
    src/ssh/kex/sntrup761.h diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/test_algo_enc.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_algo_mac.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_transport.c diff
    Removed Files:

    src/ssh/kex/mlkem768x25519-sha256.c diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    Add Botan3 crypto backend with native C++ API, deduplicate KEX modules Add a second crypto backend using Botan3's native C++ API alongside the existing OpenSSL backend. Selected at build time via -DDEUCESSH_CRYPTO_BACKEND=Botan (default remains OpenSSL). CXX is enabled conditionally only when Botan is selected — OpenSSL-only builds no longer require a C++ compiler. Backend-agnostic crypto layer: - New deucessh-crypto.h public API: dssh_hash_*, dssh_random, dssh_cleanse, dssh_crypto_memcmp, dssh_base64_encode - crypto/openssl.c and crypto/botan.cpp implement the same interface - All algorithm modules use only the public crypto API and module headers; no backend headers leak into production builds Algorithm module deduplication: - KEX: protocol logic (curve25519-sha256.c, dh-gex-sha256.c, hybrid-pq-kex.c) split from crypto operations via ops vtables (curve25519-sha256-ops.h, dh-gex-sha256-ops.h, hybrid-pq-kex-ops.h); dhgex_handler_impl() and hybrid_pq_handler_impl() further split into separate static client/server helpers with single goto-cleanup labels - Each backend provides only the crypto ops (*-openssl.c, *-botan.cpp) - DH-GEX groups extracted to kex/dh-gex-groups.c (shared between backends) - Botan modules use native C++ API (not FFI): Botan::system_rng(), Botan::X25519_PrivateKey, Botan::RSA_PrivateKey, Botan::BigInt, Botan::Cipher_Mode, Botan::MessageAuthenticationCode, etc. - Each Botan module split into .cpp (crypto impl with extern "C" wrappers and try/catch) + .c (struct allocation and registration) Renamed files for consistency: - enc/aes256-ctr.c -> enc/aes256-ctr-openssl.c - mac/hmac-sha2-{256,512}.c -> mac/hmac-sha2-{256,512}-openssl.c - key_algo/{ssh-ed25519,rsa-sha2-256,rsa-sha2-512}.c -> *-openssl.c Other changes: - derive_key(): replace hardcoded uint8_t tmp[64] with malloc(md_len) for forward-compatibility with any hash digest size - mlkem768.c: replace #undef htole64/le64toh/le32toh system macro overrides with libcrux-local lcx_htole64/lcx_le64toh/lcx_le32toh Pre-existing bug fixes (in OpenSSL code that predates this commit): - Password callback buffer overflow (missing upper bound check, 6 sites) - mpint parse uint32_t overflow (4 + len > bufsz wraps on large len) - DH-GEX server reply buffer overflow (unchecked sum of 5 fields) - RSA verify missing error codes at n-parse failure - dssh_hash_final NULL output check Testing: - New test/test_crypto.c for backend-agnostic crypto API - New test/test_botan_algo_key.cpp (18 Botan-specific tests) - New test/test_botan_transport.cpp (12 Botan-specific tests) - Backend-specific tests properly guarded with DSSH_CRYPTO_OPENSSL / DSSH_CRYPTO_BOTAN - OpenSSL: 3490/3490 tests pass - Botan: 3491/3491 tests pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  886. Rob Swindell (on Windows 11)
    Tue Mar 31 2026 23:39:30 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_global.cpp diff
    Add "support" for (ignore) Ctrl-AE (iCE colors) sequences in html_encode() Maybe we'll want to add an option to support these, but at least for now this fixes the html-encoding of any display files that include this sequence.
  887. Rob Swindell (on Debian Linux)
    Tue Mar 31 2026 23:06:07 GMT-0700 (PDT)
    Modified Files:
    

    text/menu/random_sync_rezfox.c80.msg diff
    Re-committing after converting with ans2asc -delay 100 ... again Not sure what happened last time, but apparently I committed a 0-length file. Also, the LINEDELAY @-code would not work on this file since it didn't include line-feeds (!) so I'm not sure how that worked when I tested it before committing last time. This time, I'm just using the interval delay feature of ans2msg. No SAUCE record on the output of this one, shouldn't be needed as we have the conditional newline Ctrl-A sequences in there now.
  888. Rob Swindell (on Windows 11)
    Tue Mar 31 2026 21:09:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/js_console.cpp diff
    Better argument validation and error reporting from console.gotoxy() e.g. console.gotoxy() Error: Insufficient Arguments (0 provided, a minimum of 1 expected) e.g. console.gotoxy(false) Error: console.gotoxy: invalid argument type (expected object or number-pair) e.g. console.gotoxy(0) Error: Insufficient Arguments (1 provided, a minimum of 2 expected) e.g. console.gotoxy({}) Error: console.gotoxy: object argument 'x' property is an unexpected 'null' or 'undefined' value See issue #1107 for details
  889. Deucе
    Tue Mar 31 2026 11:12:09 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/cterm.c diff
    Fix DECRQSS handling. When an "invalid" sequence or setting is selected, it should not be echoed back. Also, many classes of invalid were not getting any response.
  890. Deucе
    Mon Mar 30 2026 16:12:43 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/.gitignore diff
    src/ssh/LICENSE.md diff
    src/ssh/docs/api-design-4254.md diff
    src/ssh/docs/audit-4250.md diff
    src/ssh/docs/audit-4251.md diff
    src/ssh/docs/audit-4252.md diff
    src/ssh/docs/audit-4253.md diff
    src/ssh/docs/audit-4254.md diff
    src/ssh/docs/audit-design.md diff
    src/ssh/docs/audit-dsohowto.md diff
    src/ssh/docs/audit-hardening.md diff
    src/ssh/docs/design-channel-io-api.md diff
    src/ssh/examples/client.c diff
    src/ssh/examples/server.c diff
    src/ssh/standards/draft-ietf-sshm-mlkem-hybrid-kex.txt diff
    src/ssh/standards/draft-ietf-sshm-ntruprime-ssh.txt diff
    src/ssh/standards/rfc4250.txt diff
    src/ssh/standards/rfc4251.txt diff
    src/ssh/standards/rfc4252.txt diff
    src/ssh/standards/rfc4253.txt diff
    src/ssh/standards/rfc4254.txt diff
    src/ssh/standards/rfc4256.txt diff
    src/ssh/standards/rfc4335.txt diff
    src/ssh/standards/rfc4344.txt diff
    src/ssh/standards/rfc4419.txt diff
    src/ssh/standards/rfc4716.txt diff
    src/ssh/standards/rfc5647.txt diff
    src/ssh/standards/rfc5656.txt diff
    src/ssh/standards/rfc6668.txt diff
    src/ssh/standards/rfc8160.txt diff
    src/ssh/standards/rfc8270.txt diff
    src/ssh/standards/rfc8308.txt diff
    src/ssh/standards/rfc8332.txt diff
    src/ssh/standards/rfc8709.txt diff
    src/ssh/standards/rfc8731.txt diff
    Modified Files:

    src/ssh/CLAUDE.md diff
    Removed Files:

    src/ssh/all.c diff
    Reorganize project: add .gitignore, LICENSE, move docs/examples/standards - Add .gitignore for build dirs, coverage artifacts, CTest, .claude/ - Add BSD-2-Clause LICENSE.md (Stephen Hurd) with vendored code attribution - Move audit and design docs to docs/ - Move client.c and server.c to examples/ - Rename RFCs/ to standards/ (covers RFCs and drafts) - Remove all.c from tracking (incomplete unity build experiment) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  891. Deucе
    Mon Mar 30 2026 15:16:57 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/test/test_asymmetric_mac.c diff
    Modified Files:

    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_thread_errors.c diff
    src/ssh/test/test_transport.c diff
    Add DSSH_TEST_MAC variant axis and fork-based asymmetric MAC test Gap 1: hmac-sha2-256 was never exercised in integration tests because hmac-sha2-512 always won negotiation. Add DSSH_TEST_MAC=hmac256 env var with test_register_mac_algos() helper to control MAC preference order. Four hmac256 variants added per test suite (default, rsa, dhgex, sntrup) covering the full stack with the 32-byte MAC. Gap 2: asymmetric c2s/s2c MAC negotiation was untestable because both sides share the same global registry. Add test_asymmetric_mac.c using fork() after socketpair() so client and server have separate registries with opposite MAC preference orders. Full handshake, auth, channel echo roundtrip exercises the per-direction key derivation fix from the previous commit. Proper session_teardown() (terminate + shutdown + cleanup) prevents demux thread hangs; child gets alarm(30) safety net. Test suite: 3489 tests (up from 2624), 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  892. Deucе
    Mon Mar 30 2026 14:48:38 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/key_algo/rsa-sha2-512.c diff
    src/ssh/mac/hmac-sha2-512.c diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_algo_mac.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_thread_errors.c diff
    src/ssh/test/test_transport.c diff
    Add rsa-sha2-512 and hmac-sha2-512 algorithm modules (RFC 8332/6668) New modules: rsa-sha2-512 (RSASSA-PKCS1-v1_5 + SHA-512 host key) and hmac-sha2-512 (64-byte digest/key HMAC). Both use modern OpenSSL 3.0+ provider APIs with no deprecated interfaces. Fix pre-existing bug in derive_and_apply_keys(): key sizes, block sizes, and MAC digest sizes were read from the c2s algorithm only and applied to both directions. When c2s and s2c negotiate different-sized algorithms (now possible with hmac-sha2-512 vs hmac-sha2-256), this caused heap buffer over-reads on the s2c integrity key. Split all shared variables into per-direction variants and use sess->trans.client to select the correct digest size for rx MAC verification buffers. Test suite expanded from 8 to 12 KEX/key variants (adds rsa512 across all 4 KEX methods). Includes RFC 4231 HMAC-SHA-512 test vectors, registration tests, and alloc failure tests. 2624 tests, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  893. Deucе
    Mon Mar 30 2026 14:16:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/test/test_selftest.c diff
    Fix 12 more selftest write-before-window races (closes item 106) Same root cause as item 104: client-side dssh_chan_write called before the server's WINDOW_ADJUST was processed, so remote_window was still 0 and the non-blocking write returned 0. Under -j16 contention this manifested as ASSERT failures in 3 variants (dssh_self, dssh_self_rsa, dssh_self_mlkem) and a secondary Bus error from use-after-free when the server echo thread's stack overwrote the test's popped stack frame. Added dssh_chan_poll(DSSH_POLL_WRITE) before the first client-side write in 12 test functions. 50 consecutive clean runs under -j16 after fix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  894. Deucе
    Mon Mar 30 2026 13:15:28 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/dssh_test.h diff
    src/ssh/test/test_conn.c diff
    Fix test_conn 60s timeout bugs; add per-test timing to test framework Two reject tests (test_open_exec_rejected, test_setup_exec_rejected_by_callback) took 30s each because dssh_chan_accept loops back after rejection to wait for the next CHANNEL_OPEN. In tests, no more opens arrive, so the 30s accept_timeout fires. Fix: terminate the server session after joining the client thread to unblock the server's accept. test_accept_timeout_negative replaced its 50ms thrd_sleep with an atomic flag so the main thread proceeds as soon as the accept thread enters dssh_chan_accept. Reduced intentional-timeout safety margins to 1s (from 3-5s) in test_eof_half_close, test_accept_zc, and the four alloc sweep server threads. Added wall-clock timing to DSSH_TEST_MAIN: tests taking >100ms show duration in parentheses after PASS/FAIL/SKIP. Total dssh_conn_default time: ~5s (was ~78s). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  895. Deucе
    Mon Mar 30 2026 12:16:41 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/test/test_selftest.c diff
    Fix selftest echo write race: poll for DSSH_POLL_WRITE before first write dssh_chan_write is non-blocking by design — returns 0 when remote_window is 0. Both sides open channels with initial_window=0 then independently send WINDOW_ADJUST after setup. Under -j16 contention (especially RSA keygen), the server's WINDOW_ADJUST hadn't been processed by the client's demux thread before the test wrote, causing ~40% failure rate in dssh_self_rsa and dssh_self_mlkem_rsa variants. Add dssh_chan_poll(DSSH_POLL_WRITE) before the first write in test_self_exec_echo and test_self_shell_echo, matching the pattern already used by test_self_shell_large_data. Make server_echo_thread (both instances) use a poll-then-retry write loop to avoid silently dropping data on partial writes. Also log item 106: intermittent dssh_self_dhgex failure observed once under heavy -j16 contention, not yet reproduced. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  896. Deucе
    Mon Mar 30 2026 11:57:51 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    Fix send_channel_request_wait race: CLOSE clobbering successful response When the server accepted a channel request (CHANNEL_SUCCESS) and then immediately closed the channel (CHANNEL_CLOSE), the client's demux thread could process both messages before the client thread woke up. The post-loop check `if (sess->terminate || ch->close_received)` then returned DSSH_ERROR_TERMINATED even though request_responded was true, discarding the successful response and causing dssh_chan_open to return NULL. Fix: capture `responded` under buf_mtx; only return TERMINATED when the loop exited without getting a response. If the server explicitly answered, honor that answer regardless of close_received. Observed as test_self_exec_exit_code failing under -j16, predominantly with RSA variants where keygen CPU contention widens the scheduling window between the two demux dispatches. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  897. Deucе
    Mon Mar 30 2026 11:30:29 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/test/test_selftest.c diff
    Fix selftest race: cleanup while server echo thread still sending Two bugs caused segfaults in dssh_self_rsa under ctest -j16: 1. Server thread handle (thrd_t st) was a stack local lost when an ASSERT failed mid-test, so dssh_test_after_each cleanup could not join it -- the server thread kept running while the session was freed. 2. g_active_ctx pointed to a stack-local selftest_ctx whose frame was popped on test return. Cleanup's deeper function calls (terminate, join, session_cleanup) grew the stack into the old frame, corrupting the ctx data and causing a NULL deref in dssh_session_stop. Fix: add server_thread/server_thread_active fields to selftest_ctx; add selftest_start_thread() helper; restructure selftest_cleanup() to snapshot all ctx fields into a local copy before any function calls, then terminate both sessions, join the server thread, and finally cleanup sessions. All 27 test functions updated. Also adds TODO items 104-105 for two distinct test failures observed under -j16 that need separate investigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  898. Deucе
    Mon Mar 30 2026 10:38:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/audit-4254.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Fix malformed message parse failures silently dropping required replies Audited all SSH message types that require a response: GLOBAL_REQUEST (want_reply), CHANNEL_REQUEST (want_reply), and CHANNEL_OPEN (always requires CONFIRMATION or FAILURE). Four parse-failure paths silently dropped the required reply because want_reply was never extracted from the truncated payload. Each path now sends the appropriate failure reply (REQUEST_FAILURE, CHANNEL_FAILURE, or CHANNEL_OPEN_FAILURE) then disconnects with PROTOCOL_ERROR. The disconnect is necessary because a speculative reply when want_reply was actually false would corrupt the reply ordering (RFC 4254 s4/s5.4 match replies by order, not content). CHANNEL_OPEN_FAILURE carries the peer's channel ID so it's matched by ID, but the session is still terminated since truncated messages indicate a broken peer. Fixes: - ssh-trans.c recv_packet(): GLOBAL_REQUEST truncated name-length/name - ssh-conn.c handle_channel_request(): CHANNEL_REQUEST parse failure - ssh-conn.c chan_accept_setup_loop(): CHANNEL_REQUEST parse failure - ssh-conn.c demux_channel_open(): CHANNEL_OPEN parse failure (sends OPEN_FAILURE when sender-channel extractable, disconnect-only when not) Updated audit-4254.md sections 4-1, 5.1-4, 5.4-3. Closes TODO item 102. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  899. Deucе
    Mon Mar 30 2026 09:33:39 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh.c diff
    I got 99 problems but a callback setter ain't one Enforce the "must set before dssh_session_start()" contract at runtime: all 8 session-level callback/config setters now return int and check sess->demux_running, returning DSSH_ERROR_TOOLATE after start instead of silently racing the demux thread. NULL cb remains allowed (means "no callback"). Updated headers (deucessh.h, deucessh-conn.h), README.md, and TODO.md (item 99 done; items 95/96/101 moved to Closed). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  900. Deucе
    Mon Mar 30 2026 08:47:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-trans.c diff
    Make rx_line callback optional with built-in default The rx_line parameter to dssh_transport_set_callbacks() may now be NULL. A built-in default (rxline_from_rx) reads one byte at a time via the rx callback with strict CR-LF validation: bare CR or bare LF returns DSSH_ERROR_PARSE. Also adds TODO item 103 for a pre-existing selftest race under parallel load (cleanup while server echo thread still sending). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  901. Deucе
    Mon Mar 30 2026 08:35:36 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    Rewrite README.md for new dssh_chan_* API The old README documented the removed dssh_session_read/write/poll/close and dssh_channel_read/write/poll/close APIs. Rewrite from scratch to document the current unified dssh_chan_* API: params builder, stream I/O with stream parameter, event model (signalfd-style), zero-copy API, server accept with callbacks, channel getters, and all error codes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  902. Deucе
    Mon Mar 30 2026 08:21:04 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/audit-design.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/test_conn.c diff
    Fix audit item 14: accept loops on terminal request reject dssh_chan_accept() now loops internally when a terminal request callback rejects — closes the rejected channel (with proper CLOSE handshake), then waits for the next CHANNEL_OPEN. Matches the design spec (lines 945-946) and eliminates the last deviation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  903. Deucе
    Mon Mar 30 2026 07:58:41 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/audit-design.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/test_conn.c diff
    Fix audit item 13: server accept supports ZC mode dssh_chan_accept() now checks result.zc_cb after the setup loop: non-NULL selects DSSH_IO_ZC with the app's callback (no ring buffers); NULL keeps DSSH_IO_STREAM with the internal adapter. Added test_accept_zc: client opens stream subsystem, server accepts with ZC callback, bidirectional data roundtrip verified. Corrected all 24 stale line-number references in audit-design.md (off-by-ones, wrong ranges, and two major misrefs: mode dedup pointed at set_command, event_cb propagation cited the wrong site). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  904. Deucе
    Mon Mar 30 2026 07:40:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/audit-design.md diff
    Re-audit design conformance; add TODO item 102 Fresh re-audit of audit-design.md against current implementation. Two open deviations: - 13: server accept ignores result.zc_cb (no ZC mode on server) - 14: accept returns NULL on reject instead of looping TODO 102: malformed GLOBAL_REQUEST with want_reply gets no response (recv_packet breaks out of switch on parse failure, skipping the REQUEST_FAILURE reply required by RFC 4254 s4). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  905. Deucе
    Mon Mar 30 2026 06:53:42 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/audit-design.md diff
    src/ssh/design-channel-io-api.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/dssh_test.h diff
    src/ssh/test/mock_io.c diff
    src/ssh/test/test_algo_enc.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_algo_mac.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_thread_errors.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Fix audit items 6, 11 + test reliability under contention Item 6: remote_window converted to atomic_uint_least32_t with CAS saturating add/sub helpers. zc_send_inner no longer acquires buf_mtx for the window deduction. Item 11: design doc event positions corrected to "bytes of unread stdout/stderr at poll time"; poll freeze path recomputes from .used. Test fixes: - 20 test_server_send_fail_* tests: close both pipes before thrd_join to prevent hang when server send wins the race and loops to recv - 3 dclient server threads: close_s2c_write (not full close) to avoid yanking the read fd from under the client thread - Selftest/conn accept+poll timeouts: 5s -> 30s, poll loops 100-200ms -> 1000ms to survive -j16 contention with PQ/DH-GEX crypto - EINTR retry on all socket send/recv in test_selftest.c and mock_io.c - After-each cleanup hook in test framework: selftest registers g_active_ctx so leaked demux/accept threads from ASSERT bail-outs get cleaned up before the next test runs in the same process Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  906. Deucе
    Mon Mar 30 2026 00:00:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/audit-design.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/test_chan.c diff
    Fix audit items 4-5, 7-10 + event queue init + ZC window fix Audit conformance fixes against design-channel-io-api.md: - Item 4: term field changed from char[64] to char* with strdup (no truncation, matches RFC 4254 unbounded string) - Item 5: add dssh_chan_get_pty() returning const dssh_chan_params* - Item 7: add cb_mtx per-channel mutex protecting callback pointers; init/destroy in all channel lifecycle paths - Item 8: in_zc_rx guard added to shutwr, close, send_signal, send_window_change, send_break (was only on zc_getbuf/zc_send) - Item 9: ZC callback WINDOW_ADJUST now sent (ZC mode only; stream mode uses maybe_replenish_window after app reads) - Item 10: dssh_session_set_event_cb stores in session struct, propagated to channels at open/accept time Additional fixes found during testing: - Event queue initialized before channel registration in all three open functions (dssh_chan_open, dssh_chan_zc_open, dssh_chan_accept) to prevent SIGFPE when demux dispatches EOF/CLOSE during reject - ZC WINDOW_ADJUST restricted to DSSH_IO_ZC (was firing for stream mode too, breaking window accounting in demux truncation tests) Remaining deliberate deviations documented in audit-design.md: - Item 6: remote_window uses buf_mtx not atomic (correct, optimization) - Item 11: design doc inconsistency in event position semantics - Item 12: accept-loops-on-reject deferred (needs demux sync work) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  907. Deucе
    Mon Mar 30 2026 00:00:02 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/audit-design.md diff
    Modified Files:

    src/ssh/ssh-conn.c diff
    src/ssh/test/test_selftest.c diff
    Fix audit items 1-3: initial_window=0, WINDOW_ADJUST, buffer timing - open_session_channel sends initial_window=0 (was INITIAL_WINDOW_SIZE) - accept_channel_init sends initial_window=0 in CONFIRMATION - Split init_session_channel into init_channel_sync (phase 1: sync primitives only) and init_channel_buffers (phase 2: ring buffers) - dssh_chan_open: allocates buffers AFTER terminal request succeeds, then sends WINDOW_ADJUST to open the data window - dssh_chan_zc_open: no ring buffers (ZC mode), sends WINDOW_ADJUST - dssh_chan_accept: sends WINDOW_ADJUST after setup loop completes - Tests updated: server threads poll for DSSH_POLL_WRITE before first write (WINDOW_ADJUST may not have been processed yet) Added audit-design.md with full conformance audit (12 items). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  908. Deucе
    Mon Mar 30 2026 00:00:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_conn.c diff
    Implement ZC core; rewire stream API on top of ZC internals Factored send_packet_inner into tx_finalize (DSSH_PRIVATE) which handles padding, MAC, encrypt, send, and counters given a payload already in tx_packet[9]. send_packet_inner now copies payload then calls tx_finalize. drain_tx_queue promoted to DSSH_PRIVATE. ZC core in ssh-conn.c: - zc_getbuf_inner: acquires tx_mtx, waits for rekey, drains tx_queue, checks remote_window/remote_max_packet under buf_mtx, returns pointer into tx_packet data area past the channel header - zc_send_inner: fills channel header (msg_type, channel_id, data_type_code, length) at tx_packet[9], calls tx_finalize, deducts from remote_window, releases tx_mtx - zc_cancel_inner: releases tx_mtx without sending Stream write (dssh_chan_write) rewired: calls zc_getbuf_inner, memcpy, zc_send_inner. Eliminates the per-packet malloc that send_data used. DSSH_ERROR_NOMORE (window full) mapped to 0 bytes sent. Demux RX data path rewired: handle_channel_data/extended_data call the channel's zc_cb for new-model channels (releasing buf_mtx first, setting _Thread_local in_zc_rx guard). Stream channels use stream_zc_cb which copies into ring buffer under buf_mtx. Public ZC API: dssh_chan_zc_open, dssh_chan_zc_getbuf, dssh_chan_zc_send, dssh_chan_zc_cancel. All validate ch, check in_zc_rx guard, delegate to inner functions. Event callback setters: dssh_chan_set_event_cb, dssh_session_set_event_cb. Deleted send_data/send_extended_data (old malloc-based send path). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  909. Deucе
    Mon Mar 30 2026 00:00:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/client.c diff
    src/ssh/deucessh-conn.h diff
    src/ssh/server.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    Implement channel I/O redesign (TODO item 101, stream API) Replaces the old split dssh_session_*/dssh_channel_* channel API with a unified dssh_chan_* API per the design in design-channel-io-api.md. Transport layer: - tx_packet/rx_packet gain 4-byte seq prefix, making MAC input contiguous; tx_mac_scratch and rx_mac_scratch eliminated (2 fewer mallocs per session, 2 fewer memcpy per packet) New public API (deucessh-conn.h): - dssh_chan_params builder (init/free + 9 setters) with mode dedup - dssh_chan_open(sess, params) -- full setup: env, pty-req, shell/exec/subsystem - dssh_chan_read/write(ch, stream, ...) -- stream param replaces _ext variants - dssh_chan_poll(ch, events, timeout) -- adds DSSH_POLL_EVENT - dssh_chan_read_event(ch, event) -- 7 event types (signalfd model) - dssh_chan_close(ch, int64_t exit_code) -- negative = no exit-status - dssh_chan_shutwr(ch) -- half-close (EOF) - dssh_chan_send_signal/send_window_change/send_break - dssh_chan_accept(sess, cbs, timeout) -- callback-driven server setup - dssh_chan_get_type/get_command/get_subsystem/has_pty -- getters Demux dispatch: - New-model channels (io_model != DSSH_IO_OLD) get events pushed to an event queue instead of the old signal queue + window-change callback - EOF and CLOSE generate DSSH_EVENT_EOF / DSSH_EVENT_CLOSE events - Old-model channel paths preserved for backward compat during transition Deleted from public API: - dssh_session_open_shell/open_exec, dssh_channel_open_subsystem - dssh_session_read/read_ext/write/write_ext/poll/close - dssh_channel_read/write/poll/close - dssh_session_read_signal/send_signal/send_window_change - dssh_session_accept/reject/accept_channel, dssh_channel_accept_raw - dssh_parse_pty_req_data/env_data/exec_data/subsystem_data - struct dssh_pty_req, struct dssh_server_session_cbs, DSSH_POLL_SIGNAL Parse helpers converted to static (renamed, NULL checks removed). client.c and server.c migrated to new API. All test files updated; 1758 CTest runs pass. Zero-copy API (dssh_chan_zc_*) and event callbacks (dssh_chan_set_event_cb) deferred -- typedefs present, functions not yet implemented. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  910. Deucе
    Mon Mar 30 2026 00:00:02 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/deucessh-lang.h diff
    Modified Files:

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_enc.c diff
    src/ssh/test/test_mac.c diff
    API cleanup: items 93, 97, 100 + internal type hygiene Item 93: Create deucessh-lang.h public header for language registration. Languages are external-only (apps parse language tags); moved struct, typedef, _Static_assert, and dssh_transport_register_lang() from ssh-trans.h to new public header following deucessh-comp.h pattern. Item 97: Document dssh_parse_uint32/dssh_serialize_uint32 return values in deucessh.h and README.md. Item 100: Replace dssh_session/dssh_channel typedefs with struct pointers in all internal code (ssh-internal.h, ssh.c, ssh-trans.c, ssh-auth.c, ssh-conn.c). Public headers keep typedefs for the external API. Remove deucessh-conn.h include from ssh-trans.c; add dssh_session_stop() forward declaration to ssh-internal.h. Additional cleanup from the audit: - Remove dead dssh_transport_state/dssh_transport_global_config pointer typedefs from ssh-trans.h (defined but never used). - Replace callback typedefs in internal struct fields with raw function pointer types (ssh-internal.h, ssh-trans.h). Drop deucessh.h include from ssh-trans.h and ssh-internal.h; add forward declaration of struct dssh_session_s in ssh-trans.h. - Fix TOCTOU race on 5 callback invocation sites: snapshot function pointer into a local before NULL check to prevent use-after-clear if a setter races with the demux thread. ssh-auth.c handle_banner() already had this pattern; now terminate_cb, debug_cb, unimplemented_cb, global_request_cb, and window_change_cb all do the same. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  911. Deucе
    Mon Mar 30 2026 00:00:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    Consolidate TODO items 67, 75, 95 into 101 (channel I/O redesign) Item 67 (setup mailbox blocks demux): resolved by dssh_chan_accept() running on app's thread. Demux queues, never blocks on callbacks. Item 75 (msgqueue per-message malloc): resolved by eliminating the message queue entirely. ZC: callback into rx_packet. Stream: ring buffers. initial_window=0 bounds the 0-byte message flood. Item 95 (unify channel I/O API): subsumed by the full redesign in design-channel-io-api.md. All three are now part of item 101 with cross-references. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  912. Rob Swindell (on Windows 11)
    Sun Mar 29 2026 19:03:48 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/userdat.c diff
    del_lastuser() truncates the user index (name.dat) file as well as data file Fix issue #1100, reported in IRC by plt
  913. Rob Swindell (on Windows 11)
    Sun Mar 29 2026 18:47:44 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/main.cpp diff
    src/sbbs3/userdat.c diff
    Use the user data and index file-path-getter helper functions No functional change
  914. Deucе
    Sun Mar 29 2026 10:20:56 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/RFCs/draft-ietf-sshm-mlkem-hybrid-kex.txt diff
    src/ssh/RFCs/draft-ietf-sshm-ntruprime-ssh.txt diff
    src/ssh/RFCs/rfc4250.txt diff
    src/ssh/RFCs/rfc4251.txt diff
    src/ssh/RFCs/rfc4252.txt diff
    src/ssh/RFCs/rfc4253.txt diff
    src/ssh/RFCs/rfc4254.txt diff
    src/ssh/RFCs/rfc4256.txt diff
    src/ssh/RFCs/rfc4335.txt diff
    src/ssh/RFCs/rfc4344.txt diff
    src/ssh/RFCs/rfc4419.txt diff
    src/ssh/RFCs/rfc4716.txt diff
    src/ssh/RFCs/rfc5647.txt diff
    src/ssh/RFCs/rfc5656.txt diff
    src/ssh/RFCs/rfc6668.txt diff
    src/ssh/RFCs/rfc8160.txt diff
    src/ssh/RFCs/rfc8270.txt diff
    src/ssh/RFCs/rfc8308.txt diff
    src/ssh/RFCs/rfc8332.txt diff
    src/ssh/RFCs/rfc8709.txt diff
    src/ssh/RFCs/rfc8731.txt diff
    Modified Files:

    src/ssh/CLAUDE.md diff
    Add RFCs/ directory with all implemented and referenced specs 19 RFCs + 2 IETF drafts for offline reference: Core: 4250-4254 Implemented: 4256 (KBI), 4335 (break), 4419 (DH-GEX), 6668 (SHA-2 MAC), 8160 (IUTF8), 8308 (ext negotiation), 8332 (RSA-SHA2), 8709 (Ed25519), 8731 (curve25519), draft-ietf-sshm-ntruprime-ssh (sntrup761x25519-sha512), draft-ietf-sshm-mlkem-hybrid-kex (mlkem768x25519-sha256) Reference: 4344 (encryption modes), 4716 (key file format), 5647 (AES-GCM), 5656 (EC integration), 8270 (DH modulus sizes) Update CLAUDE.md with complete RFC list and RFCs/ directory note. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  915. Deucе
    Sun Mar 29 2026 10:20:42 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/design-channel-io-api.md diff
    Server-side accept API: callback-driven setup, shared params struct Complete server-side channel API design: - dssh_chan_accept() blocks until incoming channel setup completes. Library drives the setup state machine; app provides per-request callbacks (pty_req, env, shell, exec, subsystem). - Library populates dssh_chan_params internally during accept using the same builder functions the client uses. Every callback receives the accumulated params struct showing all setup state so far. Channel owns the params after accept; getters read from it. - Same struct round-trips through the wire — in selftests, client's builder-constructed params and server's wire-populated params can be compared directly for round-trip verification. - NULL callback semantics: only pty_req auto-accepts (benign). Everything else auto-rejects: env (RFC s6.4 security hazard — uncontrolled env vars like LD_PRELOAD), shell, exec, subsystem. App must explicitly declare what it accepts. No accidental open-everything servers, no unfiltered env vars. - Terminal request callbacks receive pre-filled dssh_chan_accept_result for I/O model selection (stream vs ZC, max_window). ZC requires the callback (only way to provide zc_cb). - pty_req/env reject = non-fatal (CHANNEL_FAILURE, continue setup). Terminal request reject = close channel, keep waiting. - Lifecycle enforcement: env before terminal request (RFC s6.4), one terminal request per channel (RFC s6.5), second pty-req = disconnect (OpenSSH convention), post-setup only window-change/ break/signal. - Getters work for both client-opened and server-accepted channels. Returned pointers valid for channel lifetime. - Accept copies session-level event callback default; post-setup events arrive through normal event queue/callback. - Server initiating channels uses dssh_chan_open (side-neutral API). - Parse helpers eliminated — library populates params internally. - All open items resolved (client and server). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  916. Deucе
    Sun Mar 29 2026 10:20:29 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/design-channel-io-api.md diff
    Channel I/O design: ZC/stream split, locking, events, params builder Squash of design iteration commits into one coherent update. - dssh_chan_ prefix; zero-copy API uses dssh_chan_zc_ - Stream API (open/read/write/poll) built on ZC internals - ZC API (zc_open/zc_getbuf/zc_send/zc_cancel): zero-copy TX (app writes directly into tx_packet) and RX (callback gets pointer into rx_packet). Zero mallocs, zero copies both directions. - All functions except open take dssh_channel only (channel carries session). No mismatched sess/ch pairs. - Stream parameter (0=stdout, 1=stderr) replaces _ext variants - Channel type as enum in params struct, not separate open functions - Params builder: init/set_*/free, all strings copied in. Type, max_window, pty (orthogonal to type), modes, env all in struct. Zero terminal modes by default (library can't know terminal state). Consumed at open time, library keeps no references. - Events separate from data (signalfd model). poll(DSSH_POLL_EVENT) + read_event(), or event callback with full event struct. Poll freezes positions; one event per cycle; uncollected discarded. - dssh_chan_close(ch, int64_t exit_code): negative = no exit-status. Preserves full uint32 wire range. - dssh_chan_shutwr(ch): half-close (EOF), shutdown(SHUT_WR) semantics - TX locking: zc_getbuf acquires tx_mtx, zc_send releases. App must not block between them. tx_mac_scratch eliminated via 4-byte seq prefix in tx_packet. - RX locking: ZC callback runs with no library mutex. remote_window and state flags are atomic. - RX callback cannot TX (deadlock: rekey needs demux thread). Enforced via _Thread_local bool in_zc_rx. - Callback protection: cb_mtx per channel. Session-level defaults copied to channel at creation time (no open-time race). - Stream built on ZC: internal zc_cb copies to ring buffer; public ZC functions validate, internal versions skip checks. - No void returns for fallible functions; infallible ops (free) void. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  917. Rob Swindell (on Windows 11)
    Sun Mar 29 2026 00:51:10 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/jsexec.cpp diff
    In MSVC, at least, environ is a macro for _environ which is a function ... so this change would crashes on the call to js_init(). Revert to the use of 'env' as the argument name (as before commit bae7c4dc) and hope this change works for macOS build too.
  918. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 23:12:01 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/jsexec.cpp diff
    Go back to using "non-standard third parameter to main for environment" ... but only for non-macOS builds, since apparently there was an issue (see commit bae7c4dc6d). This revert fixes the following MSVC build warning warning C4273: '__p__environ': inconsistent dll linkage This *may* fix an issue that plt was reporting where it appeared the SBBSCTRL environment variable was getting clobbered by running 'jsexec addfiles.js' for imports of a lot of files resulting in subsequent errors (running jsexec) with finding main.ini since the SBBSCTRL environment (pointing to the correct location of main.ini) was missing or empty in the process or shell. A similar error was reported using SBBSCTRL->User->Edit or SBBSCTRL->File->Run ... , but only after running jsexec (addfiles.js). The root cause was not determined, so this is just a guess that there could be some relation to this change.
  919. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 22:51:28 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/execnet.cpp diff
    src/sbbs3/ftpsrvr.cpp diff
    src/sbbs3/main.cpp diff
    src/sbbs3/sbbs.h diff
    src/sbbs3/telgate.cpp diff
    Rename resolve_ip() to resolve_ipv4() and use in the FTP server's PASV response Eliminates MSVC warning about use of deprecated function: gethostbyname() Also, I noticed that the ftp_startup.pasv_addr wasn't being treated as network byte order here, so that was a bug. I guess no sysops were explicitly setting their FTP server's public IP address for use in IPv4-passive data transfers.
  920. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 22:51:28 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/ctrl/FtpCfgDlgUnit.cpp diff
    Fix byte-reversal of the Passive->IPv4 Address (on both read and write) This configuration property has been network-byte order (big endian) for a long time now, but this dialog didn't get updated (?) to read and write it as such.
  921. Deucе
    Sat Mar 28 2026 21:46:19 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/design-channel-io-api.md diff
    Revise channel I/O design: dssh_chan_ API, RFC 4254 analysis, OpenSSH audit Major revision of design-channel-io-api.md based on deep-diving the RFC and reading the OpenSSH source. Key changes: - Document RFC 4254's underspecification: no ordering, multiplicity, or lifecycle constraints on channel requests; conventions are the de facto spec, not the protocol - Audit OpenSSH server (LARVAL state, initial_window=0, strict request gating) and client (non-zero window, no LARVAL, trusts server) - Both sides use initial_window=0 — safer than OpenSSH client behavior - Rename to dssh_chan_ prefix; zero-copy API uses dssh_chan_zc_ - Channel type (shell/exec/subsystem) as parameter with union, not separate open functions; NULL params means defaults - dssh_chan_close() takes int64_t exit_code: negative means no exit-status, preserving full uint32 wire range (documents OpenSSH's exit-status truncation bug) - dssh_chan_shutwr() for half-close (EOF) with unambiguous naming - Signal, window-change, AND break delivered as stream-position callbacks during dssh_chan_read() — window-change is SIGWINCH - Zero-copy send: app writes directly into tx_packet buffer - Document current allocation costs and path to zero-malloc I/O Add TODO item 101: eliminate per-packet malloc in channel send path via scatter write into tx_packet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  922. Rob Swindell (on Debian Linux)
    Sat Mar 28 2026 20:58:51 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/uedit/uedit.c diff
    Add Mouse, Delete/Backspace swap toggles Rename Pause to Screen Pause
  923. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 20:54:17 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/useredit/MainFormUnit.cpp diff
    src/sbbs3/useredit/MainFormUnit.dfm diff
    src/sbbs3/useredit/MainFormUnit.h diff
    Add terminal columns, mouse and delete/backspace swap settings Renamed "Pause" to "Screen Pause"
  924. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 20:10:40 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/logon.cpp diff
    Don't store the NO_EXASCII autoterm-set-flag in user.misc Just because the terminal was auto-detected as "DUMB" (non-ANSI) doesn't mean the user wants only US-ASCII char from that point forward (obviously). This should fix issue #1106.
  925. Deucе
    Sat Mar 28 2026 16:46:52 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/design-channel-io-api.md diff
    Revise channel I/O design: two models, deferred window, linear accumulation Replace unified read/write proposal with two distinct I/O models: - Stream API (dssh_channel_read/write) for session channels (shell/exec) - Zero-copy callback for subsystem channels (sftp, etc.) Key design decisions: - Max packet size per-session (set once, used in all channel opens) - Max window size per-channel (deferred to channel request/accept) - Channel accept as finalization point (server callback, client function) - Linear accumulation buffer for subsystem callbacks (no ring, no wrap) - Flow control via natural window exhaustion during accumulation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  926. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 14:03:22 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/user_info_prompts.js diff
    Make a few strings easier to replace with shorter text.ini keys: bad_user_address bad_user_phone bad_user_birth
  927. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 13:37:05 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/useredit/MainFormUnit.cpp diff
    src/sbbs3/useredit/MainFormUnit.dfm diff
    Add UTF-8, PETSCII, and ICE Color terminal toggles for "symmetry"
  928. Rob Swindell (on Windows 11)
    Sat Mar 28 2026 13:37:03 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/useredit/MainFormUnit.cpp diff
    src/sbbs3/useredit/MainFormUnit.dfm diff
    Remove the deprecated 'Hot Keys' setting/toggle
  929. Rob Swindell (on Debian Linux)
    Sat Mar 28 2026 13:21:34 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/uedit/uedit.c diff
    Add UTF-8, PETSCII, and ICE Color toggles for HM Derdoc to experiment with an test his (or Claude's?) theories about how user's stored terminal settings work in combination with terminal auto-detect. As you can observe, user's UTF8 flag does *not* persisent when the account is configured for auto-terminal detection and the user reconnects and logs-in with a non-UTF8 terminal. The analysis in issue #1106 description appears wrong in at least this respect.
  930. Rob Swindell (on Debian Linux)
    Sat Mar 28 2026 13:12:02 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/uedit/uedit.c diff
    Remove the deprecated "Hot Keys" setting/toggle
  931. Deucе
    Sat Mar 28 2026 12:54:37 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/design-channel-io-api.md diff
    Modified Files:

    src/ssh/TODO.md diff
    Add channel I/O API redesign proposal, consolidate TODO items 95+98 Unifies dssh_session_read/write/poll and dssh_channel_read/write/poll under a single dssh_channel_* family with consistent int64_t returns, peek support on both channel types, and close API options to resolve. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  932. Deucе
    Sat Mar 28 2026 12:45:43 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/test_conn.c diff
    Close items 92, 94: fix channel_read peek, add chan_type checks Item 92: dssh_channel_read(sess, ch, NULL, 0) now reaches msgqueue_pop() for peek (returns next message size without consuming). Guard maybe_replenish_window on buf != NULL. Item 94: all 11 channel I/O functions (7 session, 4 raw) now check chan_type and return DSSH_ERROR_INVALID on mismatch. New item 98: re-evaluate peek semantics for session channels. 5 new tests, 6 existing tests updated to set chan_type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  933. Deucе
    Sat Mar 28 2026 12:30:05 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    src/ssh/test/dssh_test_internal.h diff
    Move session lifecycle to ssh-trans.c, demote 9 functions to static Move dssh_session_init, dssh_session_terminate, dssh_session_is_terminated, dssh_session_cleanup from ssh.c to ssh-trans.c so they can call transport_init/transport_cleanup directly. 9 ssh-trans.c functions demoted from DSSH_PRIVATE: - transport_init, transport_cleanup, find_kex → static - version_exchange, kexinit, kex, newkeys, rekey, rekey_needed → DSSH_TESTABLE (static in production, visible to tests) Removes 11 declarations from ssh-trans.h, cleans up dssh_test_internal.h. TODO items 98-99 added (callback setter UB, typedef layering). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  934. Deucе
    Sat Mar 28 2026 12:14:30 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_selftest.c diff
    Add test for dssh_session_set_terminate_cb() The setter had zero test coverage (and was previously unreachable due to the missing dssh_ prefix). New test verifies the callback fires exactly once on terminate and that the single-fire guarantee holds. Also adds the NULL-session guard to test_null_session_api. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  935. Deucе
    Sat Mar 28 2026 12:10:41 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh.c diff
    Fix session_set_terminate_cb missing dssh_ prefix Item 90 rename missed this function — the definition used the internal name but deucessh.h declared dssh_session_set_terminate_cb. Demo apps (EXCLUDE_FROM_ALL) hid the linker error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  936. Deucе
    Sat Mar 28 2026 12:01:52 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    Update README and TODO for recent API changes README: document version string, termination, algorithm queries, reject/raw-accept APIs, window-change, remove stale test table. TODO: add items 92-97 for API definition gaps found during review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  937. Deucе
    Sat Mar 28 2026 11:41:41 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh.c diff
    src/ssh/test/test_chan.c diff
    Close items 91, 28: remove dead typedefs, merge read helpers Item 91: delete all 7 unused dssh_* type aliases (dssh_byte, dssh_boolean, dssh_uint32_t, dssh_uint64_t, dssh_string, dssh_mpint, dssh_namelist) and their underlying structs from deucessh.h. Update dssh_parse_uint32() and dssh_serialize_uint32() signatures to use uint32_t directly. Item 28: merge session_stdout_readable()/session_stderr_readable() into session_readable(ch, ext). Merge dssh_session_read()/dssh_session_read_ext() into session_read_impl(); public functions are now thin wrappers. Write pair left as-is -- send_data() and send_extended_data() build different wire messages (structural, not accidental duplication). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  938. Deucе
    Sat Mar 28 2026 11:33:40 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/all.c diff
    Modified Files:

    src/ssh/CLAUDE.md diff
    src/ssh/CMakeLists.txt diff
    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/audit-4251.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_chan.c diff
    Removed Files:

    src/ssh/deucessh-arch.h diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-arch.h diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-chan.h diff
    src/ssh/test/test_arch.c diff
    Close item 31: eliminate ssh-arch and fold ssh-chan into ssh-conn ssh-arch.c/h had only two functions left after dead code removal; moved dssh_parse_uint32/dssh_serialize_uint32 into ssh.c and inlined deucessh-arch.h content into deucessh.h. ssh-chan.c/h contained purely internal buffer primitives with no public API; moved all 19 functions into ssh-conn.c as DSSH_TESTABLE and struct definitions into ssh-internal.h. Six files deleted, test_arch.c tests absorbed into test_chan.c. Added item 91 (redundant dssh_* typedefs). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  939. Deucе
    Sat Mar 28 2026 11:16:41 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-chan.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_algo_enc.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_algo_mac.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_thread_errors.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Close item 90: consistent symbol prefix convention dssh_ prefix reserved for DSSH_PUBLIC symbols, dssh_test_ for symbols inside #ifdef DSSH_TESTING, no prefix for internal symbols (DSSH_PRIVATE, DSSH_TESTABLE). ~50 functions renamed across 9 library files and 11 test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  940. Deucе
    Sat Mar 28 2026 10:42:31 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-mac.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/test_transport.c diff
    Close items 16, 18: type-safe algorithm lists, DEFINE_REGISTER macro Item 16: _Static_assert on next field offset for all 6 algorithm structs (dssh_kex_s, dssh_key_algo_s, dssh_enc_s, dssh_mac_s, dssh_comp_s, dssh_language_s) and test_algo_node. FREE_LIST macro now takes a type parameter and uses typed ->next access instead of memcpy cast. Item 18: six identical dssh_transport_register_*() functions replaced with DEFINE_REGISTER(func_name, param_type, head, tail, entries) macro (~140 lines -> ~25 lines). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  941. Deucе
    Sat Mar 28 2026 10:14:58 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    Close item 76: non-blocking demux sends via tx queue send_packet() held tx_mtx for its entire duration including the blocking gconf.tx() I/O callback. On a congested link, the demux thread blocked on tx_mtx for fire-and-forget protocol responses, stalling all incoming packet processing. The naive split (prepare under lock, I/O outside) doesn't work: SSH MACs use implicit sequence numbers, so wire order must match assignment order. Any split requires a second ordering mechanism that re-serializes I/O. Instead, add a send queue: the demux thread uses mtx_trylock on tx_mtx -- fast path sends immediately, slow path enqueues the payload (linked list under independent tx_queue_mtx). send_packet() drains the queue before each send, preserving sequence-number ordering. Extracted send_packet_inner() for the core build/MAC/encrypt/I/O logic. Three demux call sites changed to send_or_queue(): CHANNEL_FAILURE, OPEN_FAILURE, and GLOBAL_REQUEST reply. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  942. Deucе
    Sat Mar 28 2026 09:25:20 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    Item 75: document ring-buffer analysis and unsolved sizing problem Expand the msgqueue memory amplification item with detailed analysis of the ring-buffer replacement approach and the fundamental sizing problem (0-byte messages bypass window accounting). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  943. Deucе
    Sat Mar 28 2026 09:01:29 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/client.c diff
    src/ssh/deucessh.h diff
    src/ssh/server.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh.c diff
    Close item 83: terminate callback + single-fire set_terminate Add dssh_terminate_cb and dssh_session_set_terminate_cb() so the application can close sockets or signal its event loop when the session terminates, unblocking I/O callbacks that would otherwise cause dssh_session_cleanup() to hang on thrd_join. Make dssh_session_set_terminate() single-fire via atomic_exchange -- previously it unconditionally re-broadcast all condvars on every call. The callback fires exactly once, before condvar broadcasts, from whichever thread triggers termination. Update I/O callback documentation to state that callbacks MUST return promptly when dssh_session_is_terminated() is true. Update client.c and server.c to use shutdown(fd, SHUT_RDWR) in the terminate callback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  944. Deucе
    Sat Mar 28 2026 08:37:46 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_conn.c diff
    Close items 26, 30: decompose demux_dispatch/accept_channel, eliminate SER macros demux_dispatch() (~240 lines) split into 4 helpers: handle_channel_data, handle_channel_extended_data, handle_channel_request, and dssh_test_parse_channel_request (DSSH_TESTABLE shared parser). Switch body reduced to ~15 lines. dssh_session_accept_channel() (~230 lines) split into accept_channel_init and accept_setup_loop, both using the shared CHANNEL_REQUEST parser. Main function reduced to ~50 lines. PTY_SER/SIG_SER local macros replaced with direct DSSH_PUT_U32 calls (11 sites). 6 new parser unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  945. Deucе
    Sat Mar 28 2026 08:14:38 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/dh-gex-sha256.h diff
    src/ssh/kex/mlkem768x25519-sha256.c diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    src/ssh/server.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_transport.c diff
    Built-in RFC 3526 default provider for DH-GEX, generic dssh_kex_set_ctx() API DH-GEX previously leaked algo-specific details (struct dssh_dh_gex_provider, dssh_dh_gex_set_provider()) into the public API, breaking the register-and- forget model every other algorithm uses. Now DH-GEX works out of the box: - Add RFC 3526 groups 14-18 (2048-8192-bit) to the DH-GEX module with a built-in default_select_group() that picks the best fit for the client's requested min/preferred/max range - Add void *ctx field to dssh_kex_s (mirrors dssh_key_algo_s pattern) - Add dssh_kex_set_ctx() public API for optional override (global, pre-init, same gate as dssh_key_algo_set_ctx()) - Remove per-session dssh_dh_gex_set_provider() and kex_ctx from transport state; struct dssh_dh_gex_provider moves to kex/dh-gex-sha256.h only - Remove 65 lines of DH-GEX boilerplate from server.c demo - Add TODO item 84: investigate DH-GEX group size vs cipher strength mismatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  946. Deucе
    Sat Mar 28 2026 07:39:52 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_transport.c diff
    Close items 19, 20, 22: decompose kexinit/newkeys, clean up derive_key kexinit() (~330 lines) split into build_kexinit_packet, receive_peer_kexinit, dssh_test_parse_peer_kexinit (DSSH_TESTABLE pure parser), and negotiate_algorithms. Eliminates KEXINIT_SER_NL macro and if(0){kexinit_fail:} goto pattern. newkeys() (~280 lines) split into dssh_test_encode_k_wire (DSSH_TESTABLE pure K wire encoder) and derive_and_apply_keys. derive_key() refactored: chained || OpenSSL calls replaced with sequential checks; 3 duplicated cleanup blocks unified via goto. 11 new unit tests: 6 for parse_peer_kexinit (valid, control char, name too long, truncated, too short, first_kex_follows), 5 for encode_k_wire (mpint no pad, sign pad, empty, string, string empty). Previously-SKIP kexinit/peer_trunc_namelist now implemented. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  947. Deucе
    Sat Mar 28 2026 06:56:35 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-auth.c diff
    Close items 10, 11: decompose auth_server_impl, eliminate SER macros Extract 4 per-method handlers (handle_auth_none, handle_auth_password, handle_auth_kbi, handle_auth_publickey) plus password_dispatch helper. auth_server_impl reduced from ~575 to ~80 lines of dispatch using AUTH_HANDLER_CONTINUE/AUTH_HANDLER_SUCCESS return macros. Replace all 5 local SER/SD_SER/MSG_SER/KBI_SER macros with direct DSSH_PUT_U32() calls. Extract build_userauth_request() prefix builder for 5 client call sites, eliminating ~60 lines of duplicated serialization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  948. Deucе
    Sat Mar 28 2026 06:21:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-trans.c diff
    Close item 17: replace cascading cleanup with goto in transport_init/newkeys transport_init: 7 allocation failure points (4 buffers + 3 sync primitives) each duplicated cleanup of all prior resources. Replaced with goto init_cleanup; bool flags track which sync primitives need destroying. newkeys: 6 key buffer mallocs had cascading cleanse_free chains; replaced with NULL-initialized pointers and goto keys_cleanup (reusing the existing label). Also simplified the post-derive_key error block from 8 lines to a single goto. Eliminates ~50 lines of duplicated cleanup code across 8 error paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  949. Deucе
    Sat Mar 28 2026 06:07:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/test_selftest.c diff
    Close item 69: buffer in-flight data during self-initiated rekey The kexinit wait loop silently discarded non-KEXINIT messages received between sending our KEXINIT and receiving the peer's. RFC 4253 s7.1 restricts message types on the SENDER only; the peer may have valid connection-layer messages in flight. Added a rekey message queue that buffers these messages and replays them through recv_packet() after rekey completes. Also fixed a latent bug where recv_packet's default case set rekey_pending during an active rekey, which would have caused nested rekey attempts with enough in-flight packets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  950. Deucе
    Sat Mar 28 2026 05:32:47 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/test_selftest.c diff
    Close items 65, 66: session-wide inactivity timeout for unbounded waits Add dssh_session_set_timeout() and DSSH_ERROR_TIMEOUT. Default 75s (standard BSD TCP connect timeout). Converts 4 unbounded cnd_wait() sites to cnd_timedwait(): open_session_channel, send_channel_request_wait, setup_recv return DSSH_ERROR_TIMEOUT; send_packet rekey wait terminates the session (rekey failure is fatal). Shared dssh_deadline_from_ms() extracted to ssh-internal.h. 4 new tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  951. Deucе
    Fri Mar 27 2026 20:57:29 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_arch.c diff
    Add DSSH_GET_U32/DSSH_PUT_U32 internal macros, convert all ~130 call sites Unchecked big-endian uint32 read/write macros for internal library use (ssh-trans.c, ssh-auth.c, ssh-conn.c). Every call site individually audited: buffer size guards preserved where they were the sole protection, removed where already covered by prior bounds checks or exactly-sized allocations. Modules (kex/, key_algo/) continue using the public dssh_parse_uint32/dssh_serialize_uint32 functions. Eliminates 322 unreachable error-handling branches, raising overall branch coverage from 81.5% to 84.9%. Adds serialize_pos_past_bufsz test to bring ssh-arch.c back to 100% branch coverage. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  952. Deucе
    Fri Mar 27 2026 20:18:42 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-arch.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/dssh_test_alloc.c diff
    src/ssh/test/dssh_test_alloc.h diff
    src/ssh/test/test_arch.c diff
    src/ssh/test/test_conn.c diff
    Add alloc failure sweeps for channel open, conditional demux exclusion Two new alloc sweep tests (open_exec, open_shell) iterate malloc failure points during channel open, verifying NULL return without crash or hang. New dssh_test_alloc_exclude_new_threads() flag lets tests opt demux threads out of alloc injection. The flag is conditional -- demux only self-excludes when the test explicitly sets it, so future server-side demux alloc tests remain possible. Cleared by dssh_test_alloc_reset(). Reorder ssh-arch.c NULL guards (val/pos first, buf last) and add explicit NULL tests for dssh_parse_uint32 and dssh_serialize_uint32 to reach 100% branch coverage on ssh-arch.c. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  953. Deucе
    Fri Mar 27 2026 19:55:43 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    Close item 87, add 30 NULL-parameter coverage tests (ssh-conn.c 69%->76%) Item 87 (shutdown path tolerance) already handled by dssh_thrd_check wrapper -- terminate atomic prevents recursion, every lock/broadcast in set_terminate() checks its return and skips on failure. Moved to Closed. Reorder NULL guard || chains in ssh-conn.c (16 functions) and ssh-auth.c (5 functions): sess == NULL now evaluates last so all branches are reachable with fake non-NULL sentinels (no handshake needed). Split parse function NULL checks into separate if statements. 25 new tests in test_conn.c + 5 in test_auth.c covering every branch in every public API NULL guard. ssh-conn.c branch coverage 69.08% -> 76.01% (-65 missed branches), overall 79.79% -> 81.49%. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  954. Deucе
    Fri Mar 27 2026 19:23:13 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    Speed up test suite 2x: cancellable watchdog, RSA key caching, CTest consolidation Four optimizations reduce full CTest wall time from ~25s to ~12.5s at -j16: 1. Cancellable watchdog in test_alloc.c: the conn_iterate test's watchdog thread slept 3s unconditionally on every iteration (7 iterations = 21s). Replace with condvar-based wait that cancels immediately when conn threads finish. alloc/conn_iterate: 21s -> 0.02s. 2. RSA key caching in test_algo_key.c: 30 tests that just need a key present now load from DSSH_TEST_RSA_KEY (set by CTest) instead of generating a fresh 2048-bit RSA key each time. Tests that specifically test key generation keep calling generate_key directly. dssh_unit_algo_key: 20s -> 8s. 3. CTest consolidation: transport, conn, selftest, and thread_error tests now run all tests per executable in one process (dssh_add_variant_tests macro) instead of one CTest entry per test function. Reduces CTest entries from ~4487 to ~1551 and eliminates fork/exec scheduling overhead. Auth tests stay individual for sntrup parallelism. 4. Sleep reduction in test_conn.c and test_selftest.c: demux-processing sleeps reduced from 100-200ms to 10-20ms (demux processes packets in microseconds). Race-widener and I/O error detection sleeps kept at original values. Saves ~2s per in-process conn run. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  955. Deucе
    Fri Mar 27 2026 18:16:24 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/ssh/kex/sntrup761.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_transport.c diff
    Fix all GCC13 -Wconversion/-Wpedantic warnings for clean -Werror builds Add explicit narrowing casts throughout sntrup761.c (popcount helpers, XOR-swap loops, field element arithmetic), libcrux_mlkem768_sha3.h (Barrett reduction), ssh-arch.c and test helpers (serialize shifts). Fix missing openssl/rsa.h include for EVP_PKEY_CTX_set_rsa_padding declaration. Replace ISO C-forbidden object-to-function-pointer casts in tests with memcpy. Both GCC13 and Clang now build clean with -Werror -Wconversion; 4487/4487 tests pass on both compilers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  956. Deucе
    Fri Mar 27 2026 18:03:59 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/test/test_thread_errors.c diff
    Modified Files:

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/dssh_test_ossl.h diff
    Check all C11 threading return values via dssh_thrd_check wrapper (items 85, 86) 133 unchecked mtx_lock/mtx_unlock/cnd_wait/cnd_timedwait/cnd_broadcast/ cnd_signal calls across ssh-conn.c (113), ssh-trans.c (10), and ssh.c (13) now go through dssh_thrd_check(), which calls set_terminate() on failure. Library code ignores the return; the wrapper handles termination internally. set_terminate() checks returns to skip blocks whose lock was not acquired (best-effort wakeup, no recursion since terminate is set first). Test injection uses a separate countdown (dssh_test_thrd_fail_after) from the OpenSSL countdown. 6 new injection wrappers, 6 new tests (48 CTest entries across 8 algo variants). Demux thread excluded from countdown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  957. Deucе
    Fri Mar 27 2026 14:09:15 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-arch.h diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-chan.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_arch.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    Fix channel close race, remove 14 dead functions (items 62, 79, 89) Item 62/79: dssh_session_close() and dssh_channel_close() freed the channel while the demux thread held buf_mtx, causing use-after-free. Added atomic_bool closing to channel struct; close functions set it before unregistering, then acquire/release buf_mtx to synchronize. demux_dispatch() checks closing after each unlock-relock window (window-change callback, send CHANNEL_FAILURE) and bails out. Added test_close_during_wc_cb regression test (8 CTest variants). Item 89: Removed 14 DSSH_PRIVATE functions with no library callers (test-only): parse/serialize for byte, boolean, uint64, string, mpint, namelist (ssh-arch.c) and msgqueue_peek_size (ssh-chan.c). Cleaned up declarations in ssh-arch.h and ssh-chan.h, removed dead test cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  958. Deucе
    Fri Mar 27 2026 12:12:34 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    Fix 5 data races: atomic rekey counters, atomic algo pointers, set_ctx gate (items 32, 53, 57, 60, 61) Item 53: split bytes_since_rekey into tx/rx halves; make tx counters atomic (atomic_uint_fast32_t / atomic_uint_fast64_t) so rekey_needed() reads them lock-free from the recv thread without acquiring tx_mtx (which send_packet holds across I/O). rx counters remain non-atomic under rx_mtx. Item 57: make all 10 *_selected pointer fields _Atomic in dssh_transport_state_s so algorithm query functions perform implicit atomic loads, eliminating UB during rekey. Item 60: dssh_key_algo_set_ctx() now refuses with DSSH_ERROR_TOOLATE after first dssh_session_init() (same gconf.used gate as registration). Items 61, 32: documented dssh_dh_gex_set_provider() and callback setters as must-call-before-start with thrd_create happens-before guarantee explanation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  959. Deucе
    Fri Mar 27 2026 11:15:15 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh-auth.h diff
    src/ssh/server.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    Fix 2 bugs: accept_channel data race, auth username buffer overflow (items 52, 8) Item 52: dssh_session_accept_channel() setup-to-normal transition now holds buf_mtx. chan_type, buffer union, window_max, and callbacks are initialized atomically; setup_mode set to false last. Prevents demux thread from seeing partially initialized channel state. Item 8: dssh_auth_server() username_out_len is now in/out — input is buffer capacity, output is bytes written. Prevents overflow when caller buffer is smaller than the internal 255-byte cap. All callers updated. New test auth/server/small_username_buffer verifies truncation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  960. Deucе
    Fri Mar 27 2026 10:30:47 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Fix 3 bugs: recv rekey termination, bytebuf window drift, close item 81 (items 15, 74, 81) Item 15: recv_packet_raw() terminated the session on DSSH_ERROR_REKEY_NEEDED, asymmetric with send_packet() which correctly exempts it. Fixed to match. Item 74: demux_dispatch() ignored dssh_bytebuf_write() return value, deducting full dlen from local_window even on partial writes. Window accounting drifted, eventually starving the channel. Also capped maybe_replenish_window() by free buffer space so WINDOW_ADJUST never grants more than the buffers can absorb. Item 81: Closed as not-a-bug. The single demux thread serializes packet processing; the accept queue only grows while the app controls drain rate via dssh_session_accept(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  961. Deucе
    Fri Mar 27 2026 09:59:13 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    Fix 4 bugs: data races, lost wakeup, duplicate defines, magic numbers (items 14-56) - rekey_in_progress: bool -> atomic_bool (item 54, demux reads without tx_mtx) - set_terminate() lost wakeup: mtx_trylock around rekey_cnd broadcast (item 55, avoids self-deadlock when called from send_packet; residual noted in items 66/85) - conn_initialized: bool -> atomic_bool (item 56, cross-thread read in set_terminate) - Deduplicate DSSH_CHAN_SESSION/DSSH_CHAN_RAW (item 35) and SSH_OPEN_ADMINISTRATIVELY_PROHIBITED (item 36) from ssh-internal.h - Replace magic numbers 80/81/82 with SSH_MSG_GLOBAL_REQUEST/SUCCESS/FAILURE (item 14) - Close item 44 (SSH_MSG_UNIMPLEMENTED callback already implemented) - Also fixes 2 pre-existing test failures (dhgex self-deadlock in set_terminate) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  962. Deucе
    Fri Mar 27 2026 09:20:28 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-auth.h diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_auth.c diff
    Fix 5 bugs: data races, double-start, auth disconnect, cnd_broadcast (items 58-73) - Move channel flag pre-checks (open/eof_sent/close_received) into dssh_conn_send_data() and dssh_conn_send_extended_data() under buf_mtx, fixing data races in write paths (items 58, 59) - Change dssh_session_start() double-start guard from demux_running to conn_initialized; clear flag in dssh_session_stop() (item 68) - Add DSSH_AUTH_DISCONNECT callback return value so server auth callbacks can reject and disconnect clients (item 70) - Replace all cnd_signal(poll_cnd) with cnd_broadcast to wake all waiters when multiple threads poll the same channel (item 73) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  963. Deucе
    Fri Mar 27 2026 08:31:54 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/mlkem768x25519-sha256.c diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_auth.c diff
    Fix 6 bugs: resource leaks, NULL guards, silent hang, banner drain (items 71-82) - Item 71: dssh_session_accept_channel() and dssh_channel_accept_raw() leaked the inc parameter on early-return error paths; added free(inc) to all error returns after the NULL-arg check - Item 72: dssh_transport_init() leaked tx_mtx when rx_mtx init failed; split combined mtx_init || into two checks with proper cleanup - Item 77: DH-GEX dhgex_handler() leaked BIGNUM p on malformed GEX_GROUP size-check failures; added BN_free(p) before two early returns - Item 78: sntrup761x25519 and mlkem768x25519 KEX handlers called ka->verify/pubkey/sign without NULL guards; added the same checks that curve25519 and dh-gex already had - Item 80: Setup mailbox malloc failure in demux_dispatch() silently dropped the message, leaving setup_recv() blocked forever; added setup_error flag so setup_recv() returns DSSH_ERROR_ALLOC - Item 82: Auth banner handling only drained one SSH_MSG_USERAUTH_BANNER; changed if to while in get_methods_impl() and auth_server_impl() KBI path per RFC 4252 s5.4 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  964. Deucе
    Fri Mar 27 2026 07:51:59 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/server.c diff
    Excercise dssh_transport_set_version() API as well
  965. Deucе
    Fri Mar 27 2026 07:15:55 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-arch.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    Remove branch coverage wallpaper Claude was supposed to put these guards in where the test was actually invariant. The ssh-arch.c one was like that, but pretty much all of the others were just current code behaviour, not invariants. In short, Claude can't be trusted to do this work, it sees it as a handy back-door way to get 100% branch coverage without writing tests, which makes it worthless.
  966. Deucе
    Fri Mar 27 2026 07:09:08 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    Add NULL parameter validation to 13 public API functions Prevents NULL-pointer crashes in dssh_parse_uint32, dssh_serialize_uint32, dssh_auth_get_methods, dssh_parse_env/exec/subsystem_data, dssh_session_read/read_ext/write/write_ext, dssh_channel_read/write, and dssh_session_read_signal. Closes TODO items 6, 45, 46, 49, 50; items 47 and 48 were already safe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  967. Deucе
    Fri Mar 27 2026 06:05:43 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_conn.c diff
    Fix 3 window-accounting data races (items 51, 63, 84) local_window was modified without buf_mtx and before send_packet succeeded, causing flow-control drift and permanent channel stalls. session_write/write_ext used a stale remote_window snapshot from a separate lock acquisition, causing spurious DSSH_ERROR_TOOLONG. - send_window_adjust: update local_window under buf_mtx, only after send_packet succeeds - send_data/send_extended_data: add size_t *sentp parameter for clamp-under-lock mode (NULL = exact-or-fail) - session_write/write_ext: pass bufsz directly, let inner function clamp atomically (eliminates double-lock gap) - channel_write: remove racy unlocked pre-check of remote_window Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  968. Deucе
    Fri Mar 27 2026 05:30:04 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    Add 34 TODO items from thread safety and design audits Thread safety audit (items 51-61): data races in local_window, setup-to-normal transition, rekey counters, rekey_in_progress, conn_initialized, algorithm queries, channel write pre-checks, global registry set_ctx, and dh-gex set_provider. Design/liveness audit (items 62-84): channel close use-after-free, window adjust failure stall, poll/accept timeout stacking, unbounded waits in open/request/setup/rekey, setup mailbox head-of-line blocking, session_start double-call, rekey data loss, auth attempt counter, inc leak, transport_init mutex leak, signal vs broadcast, bytebuf truncation, msgqueue amplification, I/O under tx_mtx, DH-GEX BIGNUM leak, PQ KEX NULL check, window-change callback use-after-free, setup malloc hang, accept queue DoS, auth banner loop, cleanup hang, and double-lock stale window. Also adds previously unnumbered items 45-50 (NULL checks). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  969. Deucе
    Fri Mar 27 2026 04:54:42 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/client.c diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh-auth.h diff
    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-conn.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-mac.h diff
    src/ssh/deucessh-portable.h diff
    src/ssh/deucessh.h diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/ssh/kex/sntrup761.h diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    src/ssh/mac/hmac-sha2-256.c diff
    src/ssh/server.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-chan.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/dssh_test.h diff
    src/ssh/test/dssh_test_alloc.c diff
    src/ssh/test/dssh_test_alloc.h diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/dssh_test_ossl.h diff
    src/ssh/test/mock_alloc.c diff
    src/ssh/test/mock_alloc.h diff
    src/ssh/test/mock_io.c diff
    src/ssh/test/mock_io.h diff
    src/ssh/test/test_algo_enc.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_algo_mac.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_arch.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_enc.c diff
    src/ssh/test/test_enc.h diff
    src/ssh/test/test_mac.c diff
    src/ssh/test/test_mac.h diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Replace non-ASCII characters with ASCII equivalents in all sources Em/en dashes, arrows, math operators, Greek letters, sub/superscripts, floor/ceil brackets, and accented letters replaced across all 50 .c/.h files including vendored sntrup761 and libcrux headers for strict C17 source character set conformance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  970. Deucе
    Fri Mar 27 2026 04:35:57 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-auth.h diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Fix error code accuracy: add REJECTED codes, fix ~50 misuses Two new error codes: DSSH_ERROR_AUTH_REJECTED (-12) for USERAUTH_FAILURE, DSSH_ERROR_REJECTED (-13) for CHANNEL_OPEN_FAILURE/CHANNEL_FAILURE. Fixes ~50 sites where error codes were misleading or wrong-category: - Auth rejection: INIT -> AUTH_REJECTED (3 sites in ssh-auth.c) - Channel rejection: INIT -> REJECTED (2 sites in ssh-conn.c) - Unexpected message type: INIT -> PARSE (3 sites in ssh-auth.c) - NULL-argument checks: INIT -> INVALID (~36 sites across all 3 files) - Wrong-state writes: INIT -> TERMINATED (3 sites in ssh-conn.c) - Channel ID exhaustion: ALLOC -> TOOMANY (1 site in ssh-conn.c) - Packet too short: TOOLONG -> PARSE with split condition (ssh-trans.c) - Negotiation failure: INIT -> INVALID (1 site in ssh-trans.c) - Empty registration name: TOOLONG -> INVALID with split (6 funcs) Closes TODO items 9 and 44. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  971. Deucе
    Fri Mar 27 2026 04:10:14 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/TODO.md diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/rsa-sha2-256.h diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/key_algo/ssh-ed25519.h diff
    src/ssh/ssh-chan.h diff
    src/ssh/ssh-trans.h diff
    Clean up public header hygiene: remove OpenSSL, fix duplicates - Replace pem_password_cb with library-owned dssh_pem_password_cb typedef so consumers don't need OpenSSL on their include path - Remove 7 duplicate transport function declarations from ssh-trans.h (deucessh.h is authoritative) - Remove unnecessary <threads.h> include from ssh-chan.h - Use dssh_transport_extra_line_cb typedef in global config struct Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  972. Deucе
    Fri Mar 27 2026 03:56:52 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/ssh-arch.h diff
    Modified Files:

    src/ssh/deucessh-arch.h diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/test_arch.c diff
    Strip ssh-arch public API to 2 functions, remove dead code Only dssh_parse_uint32 and dssh_serialize_uint32 are used by algorithm modules; all other arch functions are library-internal. - deucessh-arch.h: remove _Generic macros (unused), remove 19/21 function declarations, remove openssl/bn.h include (TODO items 4+5) - ssh-arch.h: new internal header declaring 12 DSSH_PRIVATE functions - ssh-arch.c: add DSSH_PUBLIC/DSSH_PRIVATE annotations, delete all 7 dssh_serialized_*_length functions (zero production callers) - test_arch.c: remove 7 tests for deleted functions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  973. Deucе
    Fri Mar 27 2026 00:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh.c diff
    Fix 3 bugs: data race, memory leak, terminate hang - dssh_session_write/write_ext: read remote_window and remote_max_packet under buf_mtx to eliminate data race with demux WINDOW_ADJUST thread - dssh_session_accept_channel fail path: free setup_payload before freeing channel struct to prevent leak when demux delivered a message - dssh_session_set_terminate and demux cleanup: remove chan_type != 0 guard so setup-mode channels get poll_cnd signaled on termination (buf_mtx/poll_cnd are initialized before register_channel) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  974. Deucе
    Fri Mar 27 2026 00:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-arch.h diff
    src/ssh/deucessh-auth.h diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/test_arch.c diff
    Remove dead code, fix stale comments and wrong docs - Remove dssh_bytearray type, functions, _Generic entries, tests (TODO 1) - Remove dssh_parse_namelist_next and dssh_namelist_s.next field (TODO 3) - Remove unused dssh_transport_packet_s struct (TODO 38) - Fix stale comment in open_session_channel: register-then-send (TODO 25) - Fix dssh_auth_server() doc: username is copied, not borrowed (TODO 40) - Add comment explaining msg type 60 aliasing per RFC 4252/4256 (TODO 42) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  975. Deucе
    Fri Mar 27 2026 00:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-trans.c diff
    Fix serialize overflow checks that can wrap size_t on 32-bit Convert all *pos + N > bufsz bounds checks to subtraction form (*pos > bufsz || N > bufsz - *pos) to prevent size_t wraparound. Also fix flush_pending_banner() strlen-to-uint32_t truncation and serialize_namelist_from_str() silent truncation to UINT32_MAX. Closes TODO items 2, 7, 21. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  976. Deucе
    Fri Mar 27 2026 00:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/test_alloc.c diff
    Fix 5 security bugs: stack overflows, OOB read, use-after-free, truncation - send_auth_failure(): replace msg[256] stack buffer with malloc (methods string from app callback was unbounded) - auth_server_impl() SERVICE_ACCEPT: replace accept[64] stack buffer with malloc (service name length is attacker-controlled) - Peer KEXINIT parsing: add minimum length check before setting ppos (short packet caused unsigned wraparound in pk_len - ppos) - find_channel(): hand-over-hand locking (channel_mtx then buf_mtx) to prevent use-after-free when channel is closed during demux - CHANNEL_DATA/EXTENDED_DATA: reject malformed packets where declared length exceeds payload instead of silently truncating Also: document lock ordering at declarations and cascade sites, update alloc test countdowns for new mallocs, add TODO for non-ASCII cleanup in source comments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  977. Rob Swindell
    Thu Mar 26 2026 20:13:19 GMT-0700 (PDT)
    Added Files:
    

    xtrn/avatar_chat/.github/dependabot.yml diff
    xtrn/avatar_chat/src/domain/bitmap.ts diff
    Modified Files:

    xtrn/avatar_chat/README.md diff
    xtrn/avatar_chat/avatar_chat.ini.example diff
    xtrn/avatar_chat/avatar_chat.js diff
    xtrn/avatar_chat/src/app/avatar-chat-app.ts diff
    xtrn/avatar_chat/src/domain/chat-model.ts diff
    xtrn/avatar_chat/src/domain/ui.ts diff
    xtrn/avatar_chat/src/io/config.ts diff
    xtrn/avatar_chat/src/synchro/compat.d.ts diff
    xtrn/avatar_chat/web/lib/events/avatarchat.js diff
    xtrn/avatar_chat/web/pages/avatarchat.xjs diff
    xtrn/avatar_chat/web/root/api/avatarchat.ssjs diff
    Merge branch 'avatar_chat' into 'master' Send/View ANSI art in chat, add MOTD support, Refine web visual layout, bug fixes See merge request main/sbbs!669
  978. HM Derdok
    Thu Mar 26 2026 20:13:19 GMT-0700 (PDT)
    Added Files:
    

    xtrn/avatar_chat/.github/dependabot.yml diff
    xtrn/avatar_chat/src/domain/bitmap.ts diff
    Modified Files:

    xtrn/avatar_chat/README.md diff
    xtrn/avatar_chat/avatar_chat.ini.example diff
    xtrn/avatar_chat/avatar_chat.js diff
    xtrn/avatar_chat/src/app/avatar-chat-app.ts diff
    xtrn/avatar_chat/src/domain/chat-model.ts diff
    xtrn/avatar_chat/src/domain/ui.ts diff
    xtrn/avatar_chat/src/io/config.ts diff
    xtrn/avatar_chat/src/synchro/compat.d.ts diff
    xtrn/avatar_chat/web/lib/events/avatarchat.js diff
    xtrn/avatar_chat/web/pages/avatarchat.xjs diff
    xtrn/avatar_chat/web/root/api/avatarchat.ssjs diff
    Send/View ANSI art in chat, add MOTD support, Refine web visual layout, bug fixes
  979. Deucе
    Thu Mar 26 2026 15:32:29 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    Rewrite TODO.md: 47 open items from core library audit Walk-through of ssh-arch.c, ssh-auth.c, ssh-trans.c, ssh-conn.c, ssh-chan.c, ssh.c, and all public/internal headers. Covers bugs (stack overflows, OOB reads, UAF races, memory leaks), missing visibility annotations, OpenSSL exposure in public headers, decomposition opportunities, duplicate definitions, magic numbers, and documentation inaccuracies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  980. Deucе
    Thu Mar 26 2026 14:15:23 GMT-0700 (PDT)
    Removed Files:
    

    src/ssh/NOTES.md diff
    Remove NOTES.md; rejected DEBUG-as-diagnostics idea Piggybacking library diagnostics on SSH_MSG_DEBUG conflates peer-originated wire messages with local events. If needed, a dedicated log callback is the cleaner approach. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  981. Deucе
    Thu Mar 26 2026 14:13:32 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/NOTES.md diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_transport.c diff
    Fix trailing-comma bug in build_namelist; add truncation tests dssh_test_build_namelist() wrote a comma before checking whether the next algorithm name would fit, producing malformed name-lists like "alpha," when the buffer was too small. Fix by checking comma + name length together before writing the separator. Clean up completed items from NOTES.md (internal API exposure, magic number audit). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  982. Deucе
    Thu Mar 26 2026 13:41:32 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/NOTES.md diff
    src/ssh/README.md diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/mlkem768x25519-sha256.c diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Replace magic numbers with named constants; refactor key_algo API Replace bare numeric literals throughout the library with DSSH_-prefixed macros: DSSH_VERSION_STRING_MAX, DSSH_KEXINIT_COOKIE_SIZE, DSSH_KEXINIT_NAMELIST_COUNT, DSSH_ALGO_NAME_MAX, DSSH_DISCONNECT_DESC_MAX, DSSH_ASCII_DEL, DSSH_MPINT_SIGN_BIT, DSSH_NAMELIST_BUF_SIZE, and DSSH_REQ_DATA_BUF_SIZE. Replace inline string literals in dispatch comparisons with file-scope static const char arrays (str_signal, str_session, method_password, etc.) using DSSH_STRLEN() to keep lengths in sync with content. Move MAC verification buffers from hardcoded [64] stack arrays to session-level allocations sized to the negotiated digest_size, eliminating the arbitrary size constant. Refactor dssh_key_algo_pubkey to return a const pointer to a cached blob in cbdata (computed once, reused), and dssh_key_algo_sign to malloc its output (caller frees). This eliminates DSSH_HOST_KEY_BUF_SIZE and DSSH_SIGNATURE_BUF_SIZE entirely — no caller-side buffer size guessing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  983. Deucе
    Thu Mar 26 2026 10:58:54 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/dssh_test.h diff
    src/ssh/test/mock_io.c diff
    src/ssh/test/mock_io.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Fix 6 thread-timing issues in test infrastructure 1. Double-close race: mock_io_pipe fds now _Atomic int; close helpers use atomic_exchange for exactly one close() per fd. 2. Add mock_io_close_s2c_write() for symmetry with c2s_write. 3. Crafted server threads in test_auth.c now close pipes on all error paths (prevents peer hangs on early failure). 4. All 122 bare thrd_create() calls checked: new ASSERT_THRD_CREATE macro in dssh_test.h; helper functions use inline checks. 5. mock_io_drain uses recv(MSG_DONTWAIT) instead of toggling O_NONBLOCK via fcntl (per-call, no fd-flag side effects). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  984. Deucе
    Thu Mar 26 2026 10:26:53 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/NOTES.md diff
    src/ssh/TODO.md diff
    src/ssh/client.c diff
    src/ssh/deucessh-auth.h diff
    src/ssh/ssh-auth.c diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_arch.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    Named auth constants, banner-before-success fix, close TODO 12, tests Replace magic 0/1 returns in dssh_auth_get_methods() with DSSH_AUTH_NONE_ACCEPTED / DSSH_AUTH_METHODS_AVAILABLE. Handle SSH_MSG_USERAUTH_BANNER arriving before SUCCESS in get_methods_impl. NULL-safe methods buffer. Close TODO item 12 (flaky test fixed). Tests: KBI server-side coverage (test_auth.c), new arch and conn parse tests. 4365 CTest runs, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  985. Deucе
    Thu Mar 26 2026 06:52:18 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/deucessh.h diff
    src/ssh/ssh.c diff
    Add dssh_cleanse() for secure memory scrubbing Public API wrapping OPENSSL_cleanse so applications can scrub password buffers without linking OpenSSL themselves. NULL-safe. README documents usage and the realloc caveat. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  986. Deucе
    Thu Mar 26 2026 06:14:18 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/server.c diff
    Update README for server-side KBI and banner APIs; enhance game README: add RFC 4256, document dssh_auth_set_banner(), dssh_auth_server_kbi_cb, DSSH_AUTH_KBI_PROMPT. Update server quick-start with all algorithms, keyboard-interactive, and banner. Remove none enc/mac from server example. Server: password change on failed password auth. Adventure game expanded with 4 rooms (Gate/Garden/Tower/Parapet), N/S/E/W/U/D navigation, detailed look/examine for all objects. None auth limited to one attempt with 90% failure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  987. Deucе
    Thu Mar 26 2026 05:47:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/deucessh-auth.h diff
    src/ssh/server.c diff
    src/ssh/ssh-auth.c diff
    Server-side keyboard-interactive auth (RFC 4256) New callback: dssh_auth_server_kbi_cb — called in a loop, first with NULL responses (provide initial prompts), then with client answers. Returns DSSH_AUTH_KBI_PROMPT to send more prompts, DSSH_AUTH_SUCCESS/FAILURE/PARTIAL to finish. Library handles INFO_REQUEST/INFO_RESPONSE wire protocol and frees prompt arrays. New constant: DSSH_AUTH_KBI_PROMPT (3) — avoids collision with DSSH_AUTH_SUCCESS (0) that caused immediate auth bypass. Banner flush added before INFO_REQUEST so KBI callbacks can set banners that display before the next prompt. Example server: adventure game authentication via KBI. Four rooms (Gate, Garden, Tower, Parapet), riddle puzzle, raven hint, key collection. Solve to authenticate. N/S/E/W/U/D navigation, look, take, use, read, talk, answer, inventory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  988. Deucе
    Thu Mar 26 2026 05:10:00 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-auth.h diff
    src/ssh/server.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh.c diff
    Add dssh_auth_set_banner() API; sarcastic example server New public API: dssh_auth_set_banner(sess, message, language) queues a banner to be sent before the next auth response. Callbacks can set new banners dynamically. NULL message cancels. Empty message rejected with DSSH_ERROR_INVALID per RFC 4252 s5.4. Banners flushed in send_auth_success and send_auth_failure, and at the top of the auth loop (for the initial pre-auth banner). Pending banner freed on session cleanup. Example server enhancements: - Welcome banner before auth - Sarcastic per-callback banners showing username/password/key info - 75% random auth rejection with 16 quips - publickey auth support with same rejection odds - Debug, unimplemented, and global request callbacks registered - Banner logging via set_banner() helper - All session callbacks now wired up TODO: note auth/client/pw_changereq_send_fail timing flake. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  989. Deucе
    Thu Mar 26 2026 04:29:26 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/server.c diff
    src/ssh/ssh-conn.c diff
    Fix setup mailbox race: demux could drop channel requests The demux thread delivered setup-mode messages (pty-req, shell, etc.) via a single-slot mailbox without waiting for the accept loop to consume the previous message. When multiple CHANNEL_REQUESTs arrived in rapid succession (e.g. auth-agent-req, pty-req, shell), later messages overwrote earlier ones — typically dropping pty-req. Fix: demux now waits on poll_cnd while setup_ready is true before writing the next message. setup_recv signals poll_cnd after consuming so the demux can proceed. Verified with OpenSSH 9.9 which sends auth-agent-req + pty-req + shell back-to-back. Server: add debug REQ trace line in channel request callback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  990. Deucе
    Thu Mar 26 2026 04:09:44 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/server.c diff
    Example server: fork per connection, 60s session timeout Server now forks on accept and handles multiple concurrent connections. SIGALRM enforces a 60-second session timeout via dssh_session_terminate. SIGCHLD reaps children. Host keys generated once before the accept loop. Shell command parser: ping, quit, exit (+ undocumented diediedie which SIGTERMs the parent to shut down the server). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  991. Deucе
    Thu Mar 26 2026 04:03:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/client.c diff
    src/ssh/server.c diff
    Update example client and server Server: register all KEX methods (mlkem768 first, sntrup761 second), both host key types with ephemeral key generation, remove none enc/mac. Add shell command parser with ping, quit/exit, help. Echo translates CR to CRLF for PTY clients. Client: remove none enc/mac, register mlkem768 and sntrup761. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  992. Deucе
    Thu Mar 26 2026 03:43:20 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/README.md diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    Update README and source for post-quantum KEX methods Add mlkem768x25519-sha256 and sntrup761x25519-sha512 to the algorithm table, draft references, and registration example. Fix draft references to use current WG drafts (draft-ietf-sshm-ntruprime-ssh, draft-ietf-sshm-mlkem-hybrid-kex) instead of superseded individual submissions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  993. Deucе
    Thu Mar 26 2026 03:39:01 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    Update CLAUDE.md for post-quantum KEX modules Test count updated to ~4277 CTest runs across 8 algorithm variants. Document post-quantum KEX design: K_ENCODING_STRING flag, vendor crypto (sntrup761/SUPERCOP, libcrux/mlkem768), conditional ssh-internal.h includes for test injection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  994. Deucе
    Thu Mar 26 2026 03:33:37 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/kex/libcrux_mlkem768_sha3.h diff
    src/ssh/kex/mlkem768.c diff
    src/ssh/kex/mlkem768.h diff
    src/ssh/kex/mlkem768x25519-sha256.c diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/client.c diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    Implement mlkem768x25519-sha256 post-quantum hybrid KEX Adds mlkem768x25519-sha256 key exchange combining ML-KEM-768 (FIPS 203) with X25519, hashed with SHA-256. Supported in OpenSSH since 9.9; verified interop against OpenSSH 9.9. New files: - kex/libcrux_mlkem768_sha3.h: ML-KEM-768 implementation from libcrux (Cryspen, MIT license). Self-contained with its own SHA-3/SHAKE. 23 -Wconversion casts fixed, stdbool.h added, KRML_HOST_EXIT changed from fatal_f to abort. - kex/mlkem768.h, kex/mlkem768.c: thin wrappers providing a byte-array API with RAND_bytes for randomness. Public key validation via libcrux validate_public_key. Error propagation on RAND_bytes failure. - kex/mlkem768x25519-sha256.c: KEX handler module following the sntrup761x25519-sha512 pattern. SHA-256 hash, string-encoded K. Test matrix expanded from 6 to 8 variants (mlkem, mlkem_rsa). 4277 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  995. Deucе
    Thu Mar 26 2026 03:05:03 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/kex/sntrup761.c diff
    src/ssh/kex/sntrup761.h diff
    src/ssh/kex/sntrup761x25519-sha512.c diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/client.c diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/test/CMakeLists.txt diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    Implement sntrup761x25519-sha512 post-quantum hybrid KEX Adds sntrup761x25519-sha512 key exchange per draft-josefsson-ntruprime-ssh-02. Combines Streamlined NTRU Prime 761 KEM with X25519, hashed with SHA-512. Default KEX in OpenSSH since 9.0; verified interop against OpenSSH 9.9. New files: - kex/sntrup761.h, kex/sntrup761.c: public-domain SUPERCOP reference implementation adapted for OpenSSL (RAND_bytes, EVP_Digest). Error propagation added to randombytes, crypto_hash_sha512, and all internal callers (Hash_prefix, Short_random, Small_random, KeyGen, ZKeyGen, Hide, HashConfirm, HashSession). - kex/sntrup761x25519-sha512.c: KEX handler module with client and server paths, exchange hash (SHA-512), shared secret computation. Transport layer: - DSSH_KEX_FLAG_K_ENCODING_STRING flag: hybrid PQ KEX encodes K as string (fixed-length, no sign padding) instead of mpint. - ssh-trans.c newkeys: conditional K encoding based on flag. Test infrastructure: - EVP_Digest ossl injection wrapper (dssh_test_EVP_Digest) for sntrup761's one-shot SHA-512 calls. - Test matrix expanded from 4 to 6 variants (sntrup, sntrup_rsa). - Alloc test iteration limits raised for sntrup (100000 vs 500). - Proper 1190-byte Q_C construction in alloc kex server/client tests. - CTest COST properties on alloc tests for scheduling priority. - Handshake thread socket-close-on-failure across all test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  996. Deucе
    Thu Mar 26 2026 00:46:43 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/deucessh-conn.h diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    Final hardening: timing, scrubbing, threads, NULL, lifetime 1. Constant-time MAC: memcmp → CRYPTO_memcmp (timing side-channel) 2. Sensitive data scrubbing: cleanse_free() helper; OPENSSL_cleanse on shared_secret, session_id, exchange_hash, derived keys (27 sites), passwords, stack MAC/tmp buffers before free/return 3. Thread safety: buf_mtx in send_data, send_extended_data, send_eof, send_close, maybe_replenish_window to prevent data races with demux thread on remote_window/eof/close flags 4. NULL checks: all ~40 DSSH_PUBLIC functions validate pointer parameters; parse helpers allow NULL data with data_len==0 5. Lifetime docs: channel handle rules in deucessh-conn.h 6. Zero-size write: bufsz==0 returns 0 (no empty DATA message) 7. Callback validation: set_callbacks rejects NULL tx/rx/rx_line Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  997. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    Fix channel ID collision on uint32_t wrap alloc_channel_id() now scans the active channel table to skip IDs already in use. Returns DSSH_ERROR_ALLOC if all 2^32 IDs are exhausted (the application has made some terrible mistakes). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  998. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    Remove TODO item 12: chan_type==0 window stall is correct behavior Dropping data and not replenishing the window when the channel type isn't yet determined is correct backpressure — the peer backs off until the channel is ready. The SIGFPE crash was the real bug, fixed by the capacity==0 guard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  999. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-chan.c diff
    Guard bytebuf_read against division by zero on capacity==0 Same pattern as bytebuf_write fix — early return if the ring buffer has zero capacity. All other division/modulo operations in the library use compile-time constants or function returns that are clamped to minimum values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1000. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/deucessh-conn.h diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/test/dssh_test_internal.h diff
    Eliminate void functions that swallow errors - handle_banner: void → int; callers propagate non-parse errors - maybe_replenish_window: void → int; read callers propagate - demux_dispatch, demux_open_confirmation, demux_channel_open: void → int; demux thread terminates session on non-parse errors, tolerates DSSH_ERROR_PARSE (malformed peer data) - dssh_session_reject: void → int (public API change) - bytebuf_write: guard capacity==0 to prevent SIGFPE (% 0) - TODO: document chan_type==0 data delivery race (item 12) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1001. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    CLAUDE.md: document return-value checking convention Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1002. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    CLAUDE.md: document overflow and return-check conventions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1003. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    Check return value of every dssh_serialize/parse call Every call to dssh_serialize_uint32, dssh_parse_uint32, and other serialize/parse functions now has its return value checked. Functions use a single ret/pv variable declared at function scope, reused for each call. Local SER/HASH_U32 macros reduce boilerplate in serialization-heavy functions. serialize_namelist_from_str changed from void to int. Removed #ifndef DSSH_TESTING guards around parse checks in KEX modules — return values are always checked regardless of build mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1004. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-trans.c diff
    Fix three arithmetic issues found by exhaustive audit - send_packet: guard 5+payload_len and 4+packet_length against size_t overflow before use in padding calc and buffer sizing - KEXINIT name-list parser: fix infinite loop when nlen==UINT32_MAX (j<=nlen with j++ wraps to 0 and never terminates) - TODO: document channel ID collision risk on uint32_t wrap Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1005. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    Guard all arithmetic against overflow and underflow Every size computation before malloc is now checked against SIZE_MAX to prevent wrapping on platforms with small size_t. Cumulative counters (bytes_since_rekey, bytebuf total, msgqueue total_bytes/count) use saturating adds. Channel capacity doubling checks SIZE_MAX/2 and SIZE_MAX/sizeof(*). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1006. Deucе
    Wed Mar 25 2026 23:05:49 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    Document type safety and cast conventions in CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1007. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/enc/aes256-ctr.c diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    Range-check all narrowing casts; DSSH_STRLEN macro Every runtime size_t → uint32_t cast now has an explicit range check before the narrowing. Casts backed by provable invariants (received packet lengths, fixed-size buffers, BN_num_bytes chain) are documented and left as single-use inline casts. Values used more than once after narrowing get an initializer variable. DSSH_STRLEN(lit) macro replaces (uint32_t)(sizeof(lit) - 1). EVP_EncryptUpdate bufsz gets INT_MAX guard. send_packet arithmetic cast replaced with range-checked initializer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1008. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/ssh/audit-hardening.md diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    Enable -Wconversion: range-checked narrowing throughout All implicit narrowing conversions replaced with range-checked intermediate variables. Library code checks both lower and upper bounds before every narrowing assignment, with overflow guards before arithmetic. Test code uses explicit casts where safe. Hardening audit now 34 of 34 OpenSSF flags. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1009. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/ssh/audit-hardening.md diff
    Implement OpenSSF compiler hardening flags (33 of 34) All flags from the OpenSSF Compiler Options Hardening Guide are now feature-probed at configure time via check_c_compiler_flag and check_linker_flag, supporting back to GCC 8 / Clang 7. Compile-time: -Wformat=2, -Wimplicit-fallthrough, -Werror=format-security, -Werror=implicit, -Werror=incompatible-pointer-types, -Werror=int-conversion, -D_FORTIFY_SOURCE=3, -fstrict-flex-arrays=3, -fstack-clash-protection, -fstack-protector-strong, -ftrivial-auto-var-init=zero, -fno-delete-null-pointer-checks, -fno-strict-overflow, -fno-strict-aliasing GCC-only: -Wtrampolines, -Wbidi-chars=any, -fzero-init-padding-bits=all Architecture: -fcf-protection=full (x86_64), -mbranch-protection=standard (aarch64) Linker: -Wl,-z,nodlopen, -Wl,-z,noexecstack, -Wl,--as-needed, -Wl,--no-copy-dt-needed-entries Deferred: -Wconversion (requires code changes for signedness). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1010. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/audit-hardening.md diff
    Add OpenSSF compiler hardening audit (audit-hardening.md) Audit against the OpenSSF Compiler Options Hardening Guide for C. Identifies missing runtime protection flags (FORTIFY_SOURCE, stack protectors, trivial auto var init), format/conversion warnings, architecture-specific CFI, and additional linker hardening flags. All recommended flags are compatible with the existing C17 codebase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1011. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/ssh/README.md diff
    src/ssh/api-design-4254.md diff
    src/ssh/audit-4251.md diff
    src/ssh/audit-4253.md diff
    src/ssh/audit-dsohowto.md diff
    src/ssh/client.c diff
    src/ssh/comp/none.c diff
    src/ssh/comp/none.h diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh-auth.h diff
    src/ssh/enc/aes256-ctr.c diff
    src/ssh/enc/aes256-ctr.h diff
    src/ssh/enc/none.c diff
    src/ssh/enc/none.h diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/curve25519-sha256.h diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/dh-gex-sha256.h diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/rsa-sha2-256.h diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/key_algo/ssh-ed25519.h diff
    src/ssh/mac/hmac-sha2-256.c diff
    src/ssh/mac/hmac-sha2-256.h diff
    src/ssh/mac/none.c diff
    src/ssh/mac/none.h diff
    src/ssh/server.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_algo_enc.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_algo_mac.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_enc.h diff
    src/ssh/test/test_mac.h diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    src/ssh/test/test_transport_errors.c diff
    DSO best practices: linker hardening, sw_ver rodata, dssh_ prefix Applied all recommendations from audit-dsohowto.md: - Added ELF shared library flags: -Wl,-z,relro,-z,now (Full RELRO), --hash-style=gnu, -Bsymbolic-functions, -O2 (string merging), -fno-semantic-interposition - Changed sw_ver from const char * const (pointer + relocation) to const char [] (embedded in rodata, zero relocations) - Renamed all unprefixed public symbols to use dssh_ prefix: register_*() -> dssh_register_*(), ssh_ed25519_*() -> dssh_ed25519_*(), rsa_sha2_256_*() -> dssh_rsa_sha2_256_*() Updated all documentation, headers, source, tests, and examples. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1012. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/audit-dsohowto.md diff
    Modified Files:

    src/ssh/deucessh-conn.h diff
    src/ssh/deucessh-kex.h diff
    src/ssh/ssh-chan.h diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.h diff
    Reorder struct fields by descending size; DSO best practices audit Reordered 7 structs to minimize padding: dssh_pty_req, dssh_kex_context, dssh_kex_s, dssh_incoming_open, dssh_transport_global_config, dssh_transport_state_s, dssh_channel_s, dssh_session_s. Fields sorted: uint64_t > pointers/ size_t > C11 sync types > uint32_t > bool > char arrays. Added audit-dsohowto.md documenting conformance with Drepper's "How To Write Shared Libraries" best practices. Library has excellent export control and data layout; identified missing linker flags and unprefixed symbol names for pre-1.0 cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1013. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    src/ssh/NOTES.md diff
    src/ssh/README.md diff
    src/ssh/TODO.md diff
    Update documentation for module decoupling and API changes - README.md: custom module examples now use public headers instead of ssh-trans.h; KEX example uses dssh_kex_context; updated test counts and file listings for new public module headers - CLAUDE.md: updated test counts (2150 CTest runs), architecture section lists module headers, coverage command paths updated - NOTES.md: item 2 marked complete - TODO.md: SIGPIPE fix moved to Fixed section Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1014. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/enc/aes256-ctr.c diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    Modernize OpenSSL API: remove all deprecated 3.0 usage - EVP_PKEY_CTX_new_id() -> EVP_PKEY_CTX_new_from_name() - EVP_PKEY_id() -> EVP_PKEY_is_a() - EVP_PKEY_new_raw_public_key() -> EVP_PKEY_new_raw_public_key_ex() - EVP_aes_256_ctr()/EVP_aes_256_cbc() -> EVP_CIPHER_fetch() All OpenSSL usage now follows 3.0+ best practices with no deprecated function calls. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1015. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/dssh_test.h diff
    Fix flaky rekey SIGPIPE: ignore SIGPIPE in test harness DH-GEX rekey selftests intermittently died from SIGPIPE when the demux thread's send() raced with the peer closing its socketpair end. MSG_NOSIGNAL on the mock I/O's send() only covers that specific call — the library's internal send path through the tx callback could still deliver SIGPIPE. Fix: signal(SIGPIPE, SIG_IGN) at test program startup, same as any network-facing application. send() returns EPIPE instead, which the library handles as a normal I/O error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1016. Deucе
    Wed Mar 25 2026 23:05:48 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/deucessh-comp.h diff
    src/ssh/deucessh-enc.h diff
    src/ssh/deucessh-key-algo.h diff
    src/ssh/deucessh-mac.h diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/comp/none.c diff
    src/ssh/deucessh-kex.h diff
    src/ssh/enc/aes256-ctr.c diff
    src/ssh/enc/none.c diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/mac/hmac-sha2-256.c diff
    src/ssh/mac/none.c diff
    src/ssh/ssh-trans.h diff
    Public headers for all module types (kex, key_algo, enc, mac, comp) Algorithm modules no longer include the private ssh-trans.h header. Each module type now has its own public header with struct definitions, function pointer typedefs, flags, and registration declarations: deucessh-kex.h — KEX context, handler, kex_s, register_kex deucessh-key-algo.h — key_algo_s, sign/verify/pubkey, register deucessh-enc.h — enc_s, init/crypt/cleanup, register deucessh-mac.h — mac_s, init/generate/cleanup, register deucessh-comp.h — comp_s, compress/uncompress, register Third-party algorithm modules can now be written using only public headers. ssh-trans.h includes the public headers for internal use but is no longer required by modules. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1017. Rob Swindell (on Debian Linux)
    Wed Mar 25 2026 21:03:11 GMT-0700 (PDT)
    Modified Files:
    

    exec/login.js diff
    Add module options: login_prompt and password_prompt Make it easier for sysops that want to, to change these prompts via modopts
  1018. Deucе
    Wed Mar 25 2026 15:51:39 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/test/CMakeLists.txt diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/TODO.md diff
    Move test suite CMake config into test/CMakeLists.txt Reduces the root CMakeLists.txt from 700 to 162 lines. All test infrastructure, executables, and CTest registration now lives in test/CMakeLists.txt. No functional changes — same 2149 tests, same build targets. Also logged flaky rekey_preserves_channels_dhgex test in TODO.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1019. Deucе
    Wed Mar 25 2026 15:34:14 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    src/ssh/CMakeLists.txt diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_alloc.c diff
    Branch coverage tests: 9 of 12 files at 100%, overall 92.5% New targeted tests for ssh.c, dh-gex-sha256.c, curve25519-sha256.c, ssh-ed25519.c, rsa-sha2-256.c, and aes256-ctr.c — all now at 100% branch coverage. Added ossl injection redirects for BN_rand, EVP_PKEY_CTX_set_rsa_padding, and EVP_CIPHER_CTX_set_padding. Exposed kex handlers as DSSH_TESTABLE for direct unit testing. Split all layer/integration tests into individual CTest processes (one per test × env variant) to eliminate shared global state contamination. 2149 CTest entries, same ~23s wall time with -j8. Updated CLAUDE.md to clarify that DSSH_TESTING defense-in-depth guards are only for impossible states in DeuceSSH's own code; external function failures must always be tested. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1020. Deucе
    Wed Mar 25 2026 13:01:11 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/jsexec.cpp diff
    Rename jsexec global function list Avoid doing wacky things vs. the one in main.cpp when building a JSDOCS Synchronet executable. Prevents jsexec-only functions from appearing in jsobjs.html, and ensure write_raw() *is* documented (doesn't get documented for jsexec)
  1021. Deucе
    Wed Mar 25 2026 12:51:22 GMT-0700 (PDT)
    Modified Files:
    

    exec/jsdocs.js diff
    src/sbbs3/jsdoor.cpp diff
    src/sbbs3/jsexec.cpp diff
    Generate useful docs for jsexec and jsdoor Still not documented are the global chdir(), putenv() and assertEq() methods. Also, the script needs to have some way to stop using a hardcoded relative path. However, at least there's these now: https://nix.synchro.net/jsexecobjs.html https://nix.synchro.net/jsdoorobjs.html
  1022. Deucе
    Wed Mar 25 2026 10:27:25 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    Claude is terrible at knowing what directory it's in.
  1023. Deucе
    Wed Mar 25 2026 09:05:59 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/uncrustify.cfg diff
    Modified Files:

    src/ssh/client.c diff
    src/ssh/comp/none.c diff
    src/ssh/comp/none.h diff
    src/ssh/deucessh-algorithms.h diff
    src/ssh/deucessh-arch.h diff
    src/ssh/deucessh-auth.h diff
    src/ssh/deucessh-conn.h diff
    src/ssh/deucessh-portable.h diff
    src/ssh/deucessh.h diff
    src/ssh/enc/aes256-ctr.c diff
    src/ssh/enc/aes256-ctr.h diff
    src/ssh/enc/none.c diff
    src/ssh/enc/none.h diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/curve25519-sha256.h diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/kex/dh-gex-sha256.h diff
    src/ssh/mac/hmac-sha2-256.c diff
    src/ssh/mac/hmac-sha2-256.h diff
    src/ssh/mac/none.c diff
    src/ssh/mac/none.h diff
    src/ssh/server.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-chan.c diff
    src/ssh/ssh-chan.h diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh-trans.h diff
    src/ssh/ssh.c diff
    Uncrustify Claude likes to cram statements onto a single line. This is the simplest way to deal with that. Still need a huge cleanup pass on the Claude-generated code, but we need to finish writing the tests first. This should make it readable until then.
  1024. Deucе
    Wed Mar 25 2026 08:55:24 GMT-0700 (PDT)
    Modified Files:
    

    src/conio/Common.gmake diff
    Require Wayland 1.19.91 at a minimum. This is where WL_MARSHAL_FLAG_DESTROY was introduced, and we require that macro.
  1025. Deucе
    Wed Mar 25 2026 08:55:13 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    Move fixed into fixed
  1026. Deucе
    Wed Mar 25 2026 00:20:05 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/mock_io.c diff
    src/ssh/test/mock_io.h diff
    src/ssh/test/test_auth.c diff
    ssh-auth.c coverage: fix test infra bugs, 12 new tests (86% → 95%) Fixed two infrastructure bugs that were silently breaking many tests: 1. direct_server_test() closed both ends of c2s pipe before calling auth_server_impl(), so the server could never read injected messages. Fix: close only the write end (new mock_io_close_c2s_write()). 2. dclient tests where the client callback succeeded would hang because the server thread exited without closing s2c, leaving the client blocked on recv. Fix: close_s2c flag on dclient_server_arg. New tests: bad_prefix_in_loop, changereq_alloc, pk_ok_alloc, pk_sig_unknown_algo, pk_verify_alloc, get_methods_malloc, get_methods_trunc_payload, get_methods_ctrl_char, changereq_lang_overflow, get_methods_unexpected, get_methods_msg_alloc, changereq_prompt_overflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1027. Deucе
    Wed Mar 25 2026 00:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-auth.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_auth.c diff
    ssh-auth.c: DSSH_TESTABLE + direct server parse tests (17 new) Make auth_server_impl, get_methods_impl, auth_password_impl, auth_kbi_impl, and auth_publickey_impl DSSH_TESTABLE for direct testing from the main thread without needing a server thread. Add direct_server_test() helper: injects SERVICE_REQUEST + crafted USERAUTH_REQUEST into c2s, closes c2s, calls auth_server_impl directly. No threads, deterministic coverage. 17 server-side parse/state tests: - Password: no boolean, no pw_len, pw overflow, change no new_pw, change new_pw overflow - Publickey: no has_sig, no algo_len, algo overflow, no pk_len, pk overflow, sig no sig_len, sig overflow - Wrong first message type (not SERVICE_REQUEST) - Short/overflow SERVICE_REQUEST payload - Long username (>255, truncation ternary) - NULL username_out_len parameter Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1028. Deucе
    Wed Mar 25 2026 00:00:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_auth.c diff
    ssh-auth.c coverage: server send-fail + edge cases (13 new tests) Server send-failure tests for password-change and publickey-with-sig: - Password change: success/changereq/failure send failures - Publickey with bad signature: verify-fail send failure - Publickey accepted: success send failure - Publickey rejected after valid sig: failure send failure Defensive/edge-case tests: - Tiny/short SERVICE_REQUEST (payload <= 5 bytes) - PASSWD_CHANGEREQ with no language field / truncated lang data - KBI with empty response (response_lens[i] == 0) ssh-auth.c missed branches: 55 → 30 (83.3% → 90.6%). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1029. Deucе
    Wed Mar 25 2026 00:00:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-auth.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_auth.c diff
    ssh-auth.c: direct parse_userauth_prefix tests (7 new tests) Make parse_userauth_prefix DSSH_TESTABLE and add 7 direct unit tests that call it from the main thread. This bypasses a coverage counter issue where threaded server tests' branch hits don't register in merged profdata. Tests cover all 6 truncation branches in parse_userauth_prefix: - Empty payload (no username length) - Truncated username data - No service length field - Truncated service data - No method length field - Truncated method data - Valid parse (positive test) ssh-auth.c missed branches: 64 → 55. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1030. Deucе
    Wed Mar 25 2026 00:00:02 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_auth.c diff
    ssh-auth.c coverage: defensive, client, and edge-case tests (23 new) Defensive/edge-case tests (11): - Server with NULL username_out parameters - get_methods with zero-size and small buffer - FAILURE response with DEL char in method names - 4/8/9-byte method names that don't match none/password/publickey - Password change callback returning NULL prompt - Publickey auth with algo name > 64 bytes (truncation) - Banner with no language field - get_methods small buffer (copylen truncation) Client-side failure tests (12): - SERVICE_REQUEST and get_methods send failures - Password CHANGEREQ: callback error and send failure - KBI initial send and recv failures - Publickey: no key, pubkey fail, sign fail, send fail, recv fail - Publickey with BANNER before auth response ssh-auth.c missed branches: 71 → 62 (77.9% → 80.6%). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1031. Rob Swindell (on Windows 11)
    Tue Mar 24 2026 21:46:59 GMT-0700 (PDT)
    Modified Files:
    

    src/sbbs3/scfg/scfgsub.c diff
    When creating a new sub, don't copy fidonet areatag or usenet newsgroup name These fields shouldn't be duplicated. Fix issue #1105
  1032. Rob Swindell
    Tue Mar 24 2026 21:10:20 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/ddfilelister/ddfilelister.js diff
    xtrn/ddfilelister/ddfl_cfg.js diff
    xtrn/ddfilelister/readme.txt diff
    xtrn/ddfilelister/revision_history.txt diff
    Merge branch 'dd_file_lister_cfg_load_errors_only_for_sysop' into 'master' DD File Lister: If the configuration or theme configuration files can't be loaded, only show an error if the user is the sysop (don't bug other users about that). See merge request main/sbbs!668
  1033. Eric Oulashin
    Tue Mar 24 2026 13:09:13 GMT-0700 (PDT)
    Modified Files:
    

    xtrn/ddfilelister/ddfilelister.js diff
    xtrn/ddfilelister/ddfl_cfg.js diff
    xtrn/ddfilelister/readme.txt diff
    xtrn/ddfilelister/revision_history.txt diff
    DD File Lister: If the configuration or theme configuration files can't be loaded, only show an error if the user is the sysop (don't bug other users about that).
  1034. Deucе
    Tue Mar 24 2026 20:50:01 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_auth.c diff
    ssh-auth.c coverage: server send-failure tests (12 new tests) Use pipe-close technique: after client sends the auth request, close the s2c pipe so the server's response send_packet fails. Each test covers a specific send path in auth_server_impl: - SERVICE_ACCEPT send failure - none auth: success/failure send - password auth: success/failure/no-callback/changereq send - publickey: no-callback/probe-ok/probe-rejected/unknown-algo send - unknown method: failure send ssh-auth.c missed branches: 93 → 71 (71.8% → 77.9%). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1035. Deucе
    Tue Mar 24 2026 20:32:09 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/binkp.js diff
    Bump revision. This should have been done many times before now. :(
  1036. Deucе
    Tue Mar 24 2026 16:33:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Add deterministic tests for profiling-unstable branches Some branches flip between "covered" and "missed" across coverage runs due to non-deterministic thread scheduling in two-threaded iterate tests. Add targeted single-threaded tests that exercise each branch deterministically. Negotiation failure tests (test_transport.c): - negotiate/no_common_kex: null gconf.kex_head → kex_selected NULL - negotiate/no_common_comp_s2c: crafted peer KEXINIT with bogus comp_s2c → comp_s2c_selected NULL (line 1138) Version parse tests (test_transport.c): - is_20 "1.99" chain: short buffer, bad minor digit, missing dash cover all 6 sub-branches of the || chain at line 78 - version/rx_non_ascii: inject version line with byte > 127 derive_key ossl injection tests (test_transport.c): - 7 tests targeting each EVP call in the derive_key chain plus the extension loop, covering lines 1207-1230 hmac-sha2-256 targeted tests (test_transport.c): - reinit failure, fetch failure, mac_init failure covering generate() and init() error paths Connection state tests (test_conn.c): - session_poll and accept nsec overflow (tv_nsec >= 1e9) - stderr signal mark truncation (to_mark < avail) alloc/kex_server fix (test_alloc.c): - Build correct wire packets for curve25519 (was always building DH-GEX packets). Covers server-side alloc failures for both KEX. CLAUDE.md: document two-build-directory conventions, test counts, ossl/alloc thread-local injection infrastructure. 11 of 20 profiling-unstable branches now deterministically stable. Remaining 9: 5 auth state machine (need crafted packet infra), 4 coverage tool merge artifacts from parallel ctest. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1037. Deucе
    Tue Mar 24 2026 14:53:30 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_transport.c diff
    curve25519-sha256.c: 100% branch coverage Fix alloc/kex_server iterate to build correct wire packets for curve25519 (was always building DH-GEX packets regardless of KEX type). Remove dhgex-only skip from alloc/kex_server iterate. Add Q_S overrun test (client parse: qs_len=32 but payload truncated) and Q_C overrun test (server: qc_len=32 but init payload truncated). Update CLAUDE.md with two-build-directory conventions, current test counts, and dssh_test_ossl/alloc documentation. Both KEX files now at 100% branch coverage: - curve25519-sha256.c: 190/190 branches (was 150/190) - dh-gex-sha256.c: 246/246 branches (confirmed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1038. Deucе
    Tue Mar 24 2026 13:55:03 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_transport.c diff
    curve25519-sha256.c coverage: 80% → 97% (40 → 5 missed branches) Source cleanup: - Fold int ok = EVP_DigestInit_ex(...) to eliminate dead ok && branch - Guard dead dssh_parse_uint32 < 4 checks in Q_S and sig parse chains with #ifndef DSSH_TESTING (same pattern as dh-gex and ssh-arch.c) - Make compute_exchange_hash_c25519, x25519_exchange, and encode_shared_secret DSSH_TESTABLE for direct unit testing ossl/kex_client and alloc/kex_client iterate tests now run for ALL KEX types (removed dhgex-only skip), covering curve25519 client-side ossl and alloc failure paths. Curve25519 server targeted tests (6 tests): - ka NULL / NULL pubkey / NULL sign function pointers - recv failure (no packets) - wrong msg_type for ECDH_INIT - bad Q_C length (16 instead of 32) Curve25519 helper tests (3 tests): - encode_shared_secret with leading-zero raw bytes - x25519_exchange alloc failure - encode_shared_secret alloc failure (both malloc sites) Curve25519 client parse tests (7 tests via bad-server thread): - recv ECDH_REPLY failure - truncated K_S (too short / length overrun) - truncated Q_S (too short) - bad Q_S length (16 instead of 32) - truncated sig (too short / length overrun) 5 remaining branches: 2 need targeted truncated-data tests, 3 are server alloc failures likely covered by iterate (profiling noise). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  1039. Deucе
    Tue Mar 24 2026 13:35:37 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/test/dssh_test_alloc.c diff
    src/ssh/test/dssh_test_alloc.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_transport.c diff
    dh-gex-sha256.c: 100% branch coverage Thread-local alloc injection: add dssh_test_alloc_exclude_thread() matching the ossl pattern, so server threads can opt out of library malloc failure injection during two-threaded KEX tests. alloc/kex_server iterate: single-threaded server KEX with library alloc injection via dssh_test_alloc_fail_after(). Covers malloc failures in serialize_bn_mpint, shared_secret, reply buffer, and exchange_hash on the server path. alloc/kex_client iterate: two-threaded KEX with server excluded from alloc injection. Covers client-side malloc failures. Client ka guard tests: two-threaded KEX with client's key_algo_selected set to NULL or stub with NULL verify. Client parse tests (7 tests via bad-server threads): - recv GROUP failure (server closes before sending) - GEX_GROUP empty / missing g - GEX_REPLY wrong msg_type - GEX_REPLY too short for K_S / K_S overrun - GEX_REPLY f=0 (invalid DH value) - GEX_REPLY too short for sig / sig overrun Server ka==NULL targeted test. Source cleanup: break client-side K_S and sig parse chains out of || expressions, guard dead dssh_parse_uint32 checks with #ifndef DSSH_TESTING (same pattern as parse_bn_mpint line 60). Result: dh-gex-sha256.c 246/246 branches covered (100.00%). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  1040. Deucе
    Tue Mar 24 2026 12:09:41 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/dssh_test_ossl.c diff
    src/ssh/test/dssh_test_ossl.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_transport.c diff
    DH-GEX coverage: thread-local ossl filter, client iterate, server tests Add per-thread ossl injection filter: _Thread_local ossl_this_thread defaults to true (all threads participate, backward compatible). dssh_test_ossl_exclude_thread() lets a thread opt out so its ossl calls pass straight through without incrementing the counter. This enables two-threaded KEX tests where only one side is injected. ossl/kex_client iterate: two-threaded DH-GEX with the server thread excluded from injection. Covers all client-side ossl failure paths (BN_CTX_new, BN_new, BN_rand, BN_mod_exp, EVP_Digest*, verify). DH-GEX server targeted tests (10 tests in test_transport.c): - NULL pubkey/sign function pointers - recv failure (no packets / partial packets) - wrong msg_type for GEX_REQUEST and GEX_INIT - short GEX_REQUEST payload - NULL provider / provider returning error - invalid e value (e=0) DH-GEX helper tests (3 tests in test_transport.c): - serialize_bn_mpint malloc failure via alloc injection - serialize_bn_mpint with BN value 0 (bn_bytes == 0 branch) - compute_exchange_hash alloc iterate (serialize_bn_mpint mres failures covering all 5 ok && (mres == 0) False branches) Source cleanup in dh-gex-sha256.c: - parse_bn_mpint: wrap dead dssh_parse_uint32 check in #ifndef DSSH_TESTING (matching ssh-arch.c pattern) - compute_exchange_hash: fold int ok = EVP_DigestInit_ex(...) to eliminate dead ok && short-circuit on first use - compute_exchange_hash made DSSH_TESTABLE for direct testing DH-GEX branch coverage: 78.52% → 90.80% (55 → 23 missed). Overall: 83.56% → 85.71% (414 → 359 missed). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  1041. Deucе
    Tue Mar 24 2026 10:41:24 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-trans.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Add defensive guard coverage tests (32 new tests) Test all defense-in-depth branches that were previously uncovered: Registration guards (test_transport.c, 9 tests): - kex/comp/lang toolong: name > 64 chars → DSSH_ERROR_TOOLONG - kex/key_algo/enc/mac/comp/lang toomany: entries == SIZE_MAX Blocksize + cleanup guards (test_transport.c, 2 tests): - tx_block_size/rx_block_size clamp blocksize < 8 to minimum 8 (made DSSH_TESTABLE for direct testing) - transport_cleanup with NULL cleanup function pointers Key algo guards (test_algo_key.c, 6 tests): - ed25519/rsa haskey(NULL) → false - ed25519/rsa cleanup(NULL) → no-op - ed25519/rsa get_pub_str with bufsz too small → DSSH_ERROR_TOOLONG Connection state guards (test_conn.c, 16 tests): - session_write/write_ext with !open and close_received - channel_write with !open and close_received - channel_read on empty raw queue - channel_poll with eof_received and close_received - session_poll with close_received on READ and READEXT - stdout/stderr signal mark already consumed - channel_poll/session_poll with timeout_ms=-1 (infinite wait, data already ready) - session_read_ext on empty stderr buffer Total missed branches: 443 → 414 (82.41% → 83.56%). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  1042. Deucе
    Tue Mar 24 2026 03:00:59 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_alloc.c diff
    Break sequential allocation chains into per-call checks Sequential OpenSSL/allocation calls that all executed regardless of which one failed produced identical call counts for consecutive N values, triggering false plateau detection in iterate tests. The ossl/kex_server test was exiting after only 3 failure points instead of exercising all ~37. Break all grouped allocation chains into per-call checks with early return on failure: - dh-gex: BN_bin2bn(p)+BN_bin2bn(g), BN_CTX_new+3×BN_new (client+server) - curve25519: EVP_PKEY_new_raw_public_key+EVP_PKEY_CTX_new - rsa pubkey: malloc(e_buf)+malloc(n_buf) - ssh-trans newkeys: 6-alloc key derivation chain - ssh-trans init: 4-alloc packet buffer chain - ssh-auth KBI: 5-alloc prompt array chain Also fix alloc/session_init test where break-after-success fell through to error path (masked by the false plateau). DH-GEX branch coverage: 57.81% → 78.52% (-53 missed branches). Overall: 509 → 449 missed branches (79.79% → 82.17%). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1043. Deucе
    Tue Mar 24 2026 02:36:36 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_alloc.c diff
    Add isolated KEX and key_algo ossl failure injection tests New tests that iterate OpenSSL failures over just the target code, without running full two-threaded handshakes: - ossl/key_verify: iterate verify() alone with pre-generated sig+pubkey blobs. Covers EVP_DigestVerifyInit, EVP_DigestVerify, EVP_PKEY_new_raw_public_key (ed25519) or BN_bin2bn, OSSL_PARAM_BLD_*, EVP_PKEY_fromdata (RSA). - ossl/key_pubkey: iterate pubkey() alone. Covers EVP_PKEY_get_raw_public_key (ed25519) or EVP_PKEY_get_bn_param (RSA). - ossl/kex_server: server-side KEX handler with packet replay. One-time two-threaded setup (version_exchange + kexinit, ~10ms), then single-threaded iterate of dssh_transport_kex() with pre-built client packets injected via mock_io_inject(). For curve25519: ECDH_INIT(Q_C) with random 32-byte key. For dh-gex: GEX_REQUEST(2048,4096,8192) + GEX_INIT(e=2). Performance: <1ms per iteration vs ~500ms for full handshake iterate. DH-GEX+RSA variant: 220ms total vs 22s previously. Infrastructure: build_plaintext_packet() helper builds SSH wire packets matching send_packet's plaintext format. ve_ki_thread() runs version_exchange + kexinit for one-time setup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1044. Deucе
    Tue Mar 24 2026 02:16:51 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/dssh_test_alloc.c diff
    src/ssh/test/mock_alloc.c diff
    src/ssh/test/test_alloc.c diff
    Use explicit atomic operations in all test allocator counters dssh_test_alloc.c: replace implicit atomic_int operations with explicit atomic_store/atomic_load/atomic_fetch_add. Extract shared should_fail() helper matching the ossl pattern. mock_alloc.c: convert plain int counters to atomic_int with explicit operations. The process-wide --wrap allocator is shared between server and client threads in some tests. test_alloc.c: fix session_init iterate to return TEST_FAIL with descriptive message when hard limit is exhausted (was silently returning TEST_PASS). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1045. Deucе
    Tue Mar 24 2026 02:07:55 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    Update CLAUDE.md: ctest -j8, test counts, socketpair mock I/O Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1046. Deucе
    Tue Mar 24 2026 02:04:58 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Test the last "practically untestable" branches All four turned out to be testable: - aes256_ctr/ctx_member_null: cbd non-NULL but cbd->ctx is NULL (second half of OR condition at line 46) - aes256_ctr/encrypt_update_fail: arm ossl countdown AFTER init succeeds so EVP_EncryptUpdate fails mid-operation - hmac_sha2_256/cleanup_null: call cleanup with NULL ctx - hmac_sha2_256/generate_failure: arm ossl countdown AFTER init succeeds so EVP_MAC operations fail mid-generate - test_window_add_overflow: set local_window near UINT32_MAX, send_window_adjust with enough to overflow — clamps to UINT32_MAX Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1047. Deucе
    Tue Mar 24 2026 01:50:27 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Add all remaining easy coverage tests Transport tests: - first_name: basic, single entry, small buffer (clamp path) - register/two_kex, two_comp, two_lang (tail->next assignment) - kexinit/peer_trunc_namelist (SKIP — needs bridge infrastructure) Auth client tests: - get_methods FAILURE: truncated, methods_len > payload, control char - get_methods unexpected msg type - password CHANGEREQ: no callback, truncated prompt header, truncated prompt data, truncated lang - password unexpected msg type - SERVICE_ACCEPT unexpected msg type Conn tests: - send_eof already sent (direct call via DSSH_TESTABLE) - send_close already sent - maybe_replenish_window after EOF (no-op path) - maybe_replenish_window with low window (triggers WINDOW_ADJUST) - window underflow to zero via demux Infrastructure: - Expose send_eof, send_close, send_window_adjust, maybe_replenish_window, first_name as DSSH_TESTABLE - Add pipe-close to crafted-response server threads to prevent client read() hangs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1048. Deucе
    Tue Mar 24 2026 01:19:58 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-conn.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Add remaining unit tests for formerly-guarded paths Complete test coverage for all removed #ifndef DSSH_TESTING guards: - guard/ed25519_haskey_wrong_type: RSA key in ed25519 ctx → false - guard/rsa_haskey_wrong_type: ed25519 key in RSA ctx → false - guard/remote_languages_cleanup: populate and free remote_languages - test_send_ext_data_toolong: send_extended_data with len > window and len > max_packet (DSSH_TESTABLE) - test_demux_chan_type_zero: demux_dispatch on channel with chan_type==0 returns early (DSSH_TESTABLE) Expose dssh_conn_send_data, dssh_conn_send_extended_data, and demux_dispatch as DSSH_TESTABLE for direct testing. Expose serialize_bn_mpint as DSSH_TESTABLE (previous commit). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1049. Deucе
    Tue Mar 24 2026 01:09:51 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_transport.c diff
    Add unit tests for formerly-guarded paths Tests for code paths that were previously hidden behind #ifndef DSSH_TESTING guards: - guard/rekey_time_zero: rekey_needed returns false when rekey_time==0 - guard/blocksize_lt8: enc module with blocksize=1 (clamped to 8) - guard/ed25519_sign_small_buf: sign with 4-byte buffer → TOOLONG - guard/ed25519_pubkey_small_buf: pubkey with 4-byte buffer → TOOLONG - guard/rsa_sign_small_buf: sign with 4-byte buffer → TOOLONG - guard/rsa_pubkey_small_buf: pubkey with 4-byte buffer → TOOLONG - guard/bn_mpint_small_buf: serialize_bn_mpint with 4-byte buffer → TOOLONG Expose serialize_bn_mpint as DSSH_TESTABLE for direct testing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1050. Deucе
    Tue Mar 24 2026 00:59:39 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    Remove all 31 #ifndef DSSH_TESTING dead-code guards Every guarded check is now live code that can be reached and tested: - Buffer size checks in sign/pubkey (ed25519, rsa-sha2-256) - EVP_PKEY_id type validation in haskey (ed25519, rsa-sha2-256) - serialize_bn_mpint buffer overflow check (dh-gex) - KEX ka/verify/pubkey/sign NULL checks (curve25519, dh-gex) - send_extended_data len > window/max_packet check (ssh-conn) - demux_dispatch chan_type == 0 check (ssh-conn) - Channel cleanup ch != NULL check (ssh-conn) - rekey_time == 0 check (ssh-trans) - enc->blocksize < 8 checks (ssh-trans) - kex_selected/handler NULL check (ssh-trans) - All cleanup != NULL checks in newkeys/transport_cleanup (ssh-trans) - Namelist overflow checks in KEXINIT building (ssh-trans) - remote_languages cleanup (ssh-trans) Only one legitimate guard remains: dssh_parse_string() in ssh-arch.c checks a dssh_parse_uint32() contract invariant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1051. Deucе
    Tue Mar 24 2026 00:45:31 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    TODO: audit #ifndef DSSH_TESTING guards for testable paths Many dead-code guards wrap error checks that are trivially testable by exposing the function via DSSH_TESTABLE and calling it directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1052. Deucе
    Tue Mar 24 2026 00:43:46 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/ssh-conn.c diff
    Delete dead x11 type_len==2 check (bug 6) The check `type_len == 2 && memcmp(ctype, "x11", 3)` compared 3 bytes against a 2-byte string — could never match. The x11 rejection is fully handled by the type_len==3 check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1053. Deucе
    Tue Mar 24 2026 00:39:50 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_transport.c diff
    Add DEBUG/GLOBAL_REQUEST/banner/get_methods/OPEN_CONFIRMATION edge case tests Transport tests: - debug/msg_len_exceeds_payload: DEBUG with msg_len > actual data (covers msg_len clamp to 0 on line 754) - global_request/name_exceeds: GLOBAL_REQUEST with name_len > payload (covers early break on line 781) Auth tests: - banner_truncated: three BANNER variants sent from server before auth response — no msg_len header, msg_len > payload, valid msg with truncated lang (covers lines 18, 22, 33-34) - get_methods_none_accepted: server accepts "none" auth, client get_methods receives SUCCESS with empty methods (covers lines 567-570) Conn tests: - truncated_open_confirmation: OPEN_CONFIRMATION < 17 bytes (line 641) - open_conf_unknown_channel: OPEN_CONFIRMATION for nonexistent channel (line 646) - channel_success_no_request: CHANNEL_SUCCESS/FAILURE when no request pending (exercises the break path at line 612) TODO.md: add bugs 5 (void* banner_cb), 6 (dead x11 type_len==2 check), 7 (KEX pubkey error check guards — already fixed). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1054. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/mock_io.c diff
    src/ssh/test/mock_io.h diff
    src/ssh/test/test_conn.c diff
    Rewrite mock I/O to socketpair(); add 19 conn edge case tests Replace circular buffer + condvar mock I/O with Unix socketpair(). Blocking read/write with natural close-unblocks-peer behavior eliminates timed waits and condvar signaling complexity. Fix conn_cleanup to close pipes before dssh_session_stop() — with socketpair I/O, condvar broadcasts cannot unblock a blocking read(); only closing the peer fd does. Fixes hangs in test_session_stop and test_session_start_twice. New test coverage (88 conn tests, up from 69): - Session poll WRITE readiness and timeout=0 for all event types - Session write/write_ext after EOF, window=0, max_packet clamping - Raw channel write after close, write TOOLONG - Raw channel poll WRITE and timeout=0 - dssh_channel_accept_raw path - Reject with long description (truncation) - Signal interleave clamping (stdout and stderr readable limits) - dssh_session_read_signal when no signal pending - Accept with negative timeout (blocking, unblocked by terminate) - Demux parse errors: short payload, truncated CHANNEL_OPEN, truncated CHANNEL_REQUEST ssh-conn.c branch coverage: 64.71% → 72.79% (544 branches) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1055. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_conn.c diff
    Add 4 conn demux edge case tests - Extended data (stderr) after EOF — discarded per eof_received guard - Truncated CHANNEL_EXTENDED_DATA (payload_len < 13) - Channel request with want_reply=true — unknown request gets CHANNEL_FAILURE response - CHANNEL_DATA with dlen > payload — dlen clamped, window saturates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1056. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CLAUDE.md diff
    Update CLAUDE.md build instructions to use cmake -S . -B build Prevents cmake from discovering the parent src/CMakeLists.txt which has a broken project() call that causes install-export errors. Also update test count to ~600. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1057. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/ssh/test/test_dhgex_provider.h diff
    Cache test host keys to avoid repeated RSA keygen test_generate_host_key() now checks DSSH_TEST_ED25519_KEY and DSSH_TEST_RSA_KEY environment variables. If set to a file path: - Load the key from that file if it exists - Otherwise generate and save it for next time If the env var is not set, generates a fresh key every time (preserving original behavior for manual runs). CMakeLists.txt sets both env vars to build-directory paths for all CTest configurations, so keys are generated once and reused across the 23 test runs. Test suite time: ~130s -> ~42s (3x speedup). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1058. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/test/dssh_test_alloc.c diff
    src/ssh/test/dssh_test_alloc.h diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/TODO.md diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/test/test_alloc.c diff
    Add library-only test allocator; fix curve25519 double-free; iterative handshake test New test infrastructure: dssh_test_alloc (macro-based allocator) - ssh-internal.h redirects malloc/calloc/realloc to dssh_test_malloc etc. via macros under DSSH_TESTING - Only affects library code (OpenSSL doesn't include ssh-internal.h) - Enables safe allocation failure injection during handshakes without crashing OpenSSL's internal state Bug fix: curve25519 double-free of shared_secret - When exchange_hash malloc failed after shared_secret was stored in sess->trans, the error path freed ss_copy but left shared_secret pointing to it. dssh_transport_cleanup then freed it again. - Found by valgrind under the new iterative handshake alloc test. - Fixed: NULL out shared_secret on the error path (both client and server sides). New test: alloc/handshake_iterate - Iterates N from 0..50, failing the Nth library malloc during a two-threaded handshake. Uses a barrier to arm the allocator after thread creation. Covers kexinit, peer_kexinit, newkeys key derivation, and shared secret allocation failure paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1059. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_auth.c diff
    src/ssh/test/test_conn.c diff
    Add 5 auth password-change and conn demux edge case tests Auth server password change flow: - passwd_change_cb returns FAILURE (USERAUTH_FAILURE sent) - no passwd_change_cb set when change=true (falls through to FAILURE) Connection demux edge cases: - WINDOW_ADJUST from peer (covers WINDOW_ADJUST case + window_add) - CHANNEL_DATA after EOF (data discarded per eof_received guard) - Truncated CHANNEL_DATA (payload_len < 9, silently dropped) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1060. Deucе
    Mon Mar 23 2026 20:49:12 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_conn.c diff
    Add 4 connection auto-reject channel type tests Send CHANNEL_OPEN with forbidden types from server to client: - "x11", "forwarded-tcpip", "direct-tcpip" auto-rejected - "session" from server to client auto-rejected per RFC 4254 s6.1 Verify the client's accept queue is empty after rejection (the channel was rejected by the demux thread, not queued for the app). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1061. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_auth.c diff
    Add 12 more server auth parse and callback tests Password parse errors: - truncated password data (pw_len > remaining) - password change with no new_password field - password change with truncated new_password Missing callbacks: - password method with no password_cb (gets FAILURE, retries with none) - publickey method with no publickey_cb (gets FAILURE, retries with none) Publickey parse errors: - no algo length field after has_sig - no pubkey blob after algo name - has_sig=true but no signature length Publickey protocol: - unknown algo name with has_sig=true (FAILURE response) - key probe (has_sig=false) rejected by callback (FAILURE not PK_OK) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1062. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_auth.c diff
    Add 11 server auth parse error tests Tests send malformed USERAUTH_REQUEST packets through an encrypted session to exercise parse_userauth_prefix and method-specific parse branches in dssh_auth_server: - empty request (just message type byte) - truncated username (length > remaining data) - no service name field after username - truncated service name (length > remaining) - no method field after service name - truncated method name (length > remaining) - password method with no change boolean - password method with no password length - publickey method with no has_signature boolean - first message is not SERVICE_REQUEST - username >= 256 bytes (truncation to saved_user) ssh-auth.c branch coverage: 63.33% -> 67.27% (-13 missed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1063. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/dssh_test_internal.h diff
    src/ssh/test/test_transport.c diff
    Expose version_tx, parse_bn_mpint, dh_value_valid for testing; add 12 tests Move DSSH_TESTABLE macro definition from ssh-trans.c to ssh-internal.h so all library source files can use it. Expose three static functions via DSSH_TESTABLE: - version_tx (ssh-trans.c): sends the SSH version identification line - parse_bn_mpint (dh-gex-sha256.c): parses an mpint from wire format - dh_value_valid (dh-gex-sha256.c): validates DH e/f in [1, p-1] Add test accessors dssh_test_set_sw_version/set_version_comment to bypass set_version validation for defense-in-depth testing. New tests: - version_tx TOOLONG with oversized version string - version_tx TOOLONG with oversized comment - parse_bn_mpint: valid, short header, truncated data - dh_value_valid: zero, negative, equal to p, greater than p, valid interior, boundary values (1 and p-1) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1064. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/TODO.md diff
    src/ssh/enc/aes256-ctr.c diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/mac/hmac-sha2-256.c diff
    src/ssh/test/test_alloc.c diff
    Fix curve25519 OPENSSL_cleanse on NULL; revert incorrect cleanup guards Bug: curve25519 handler called OPENSSL_cleanse(raw_secret, len) when raw_secret was NULL (malloc failure). The NULL check and derive call were combined in one if-statement, so the malloc failure path fell through to the cleanse. Split into separate checks. Revert: the dead-code guards on module cleanup functions (ed25519, rsa, aes256-ctr, hmac-sha2-256) assumed cleanup is never called with a NULL context. This is false during allocation failure testing -- registration succeeds but keygen/init fails, leaving ctx as NULL when the global config cleanup runs. Restore the NULL checks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1065. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/test/test_algo_key.c diff
    Add 10 key algo verify parse and pre-registration tests Deeper verify parse errors: - ed25519/rsa key blob truncated after algo name (before raw key len) - ed25519/rsa sig blob truncated after algo name (before raw sig len) - rsa key blob truncated after e field (before n field) - ed25519 verify with valid format but cryptographically wrong sig Pre-registration errors: - ed25519/rsa generate_key before register (ka == NULL) - ed25519/rsa get_pub_str before register (ka == NULL) ssh-ed25519.c: 71.43% -> 75.00% (-4 missed) rsa-sha2-256.c: 63.75% -> 66.88% (-5 missed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1066. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/NOTES.md diff
    Modified Files:

    src/ssh/TODO.md diff
    src/ssh/ssh-auth.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-internal.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/ssh.c diff
    src/ssh/test/mock_io.c diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_conn.c diff
    Fix session termination: signal waiters, promote fatal auth errors Previously, setting sess->terminate did not wake threads blocked on library condvars or I/O callbacks, causing deadlocks when one side of a connection failed internally. Changes: - dssh_session_set_terminate(): new internal helper that sets the terminate flag AND broadcasts rekey_cnd, accept_cnd, and all per-channel poll_cnd. All code that previously set sess->terminate directly now calls this function. - send_packet sets terminate on fatal errors (not TOOLONG/REKEY_NEEDED). recv_packet_raw sets terminate on all errors (all are fatal). handshake() and rekey() set terminate on failure. - Auth functions (server, password, get_methods, keyboard_interactive, publickey) wrap their implementations with auth_check_terminated(), which promotes any negative return to DSSH_ERROR_TERMINATED when sess->terminate is set. Auth rejection (USERAUTH_FAILURE) does NOT set terminate, so callers can distinguish recoverable rejection from fatal connection loss. - Mock I/O uses 50ms timed waits instead of indefinite cnd_wait so the terminate flag check runs promptly. - conn_cleanup no longer needs to close pipes before session_stop since the terminate signal now propagates through timed waits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1067. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/binkp.js diff
    For Mystic peers, lower log level for unfixed message Mystic up to version 1.12A49 sends an M_EOB without an M_GOT but only after the transfer is successful. For this case, since Mystic is dead, log at INFO level to keep sysops willing to look at warnings. Should fix issue #1103
  1068. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/enc/aes256-ctr.c diff
    src/ssh/kex/curve25519-sha256.c diff
    src/ssh/kex/dh-gex-sha256.c diff
    src/ssh/key_algo/rsa-sha2-256.c diff
    src/ssh/key_algo/ssh-ed25519.c diff
    src/ssh/mac/hmac-sha2-256.c diff
    src/ssh/ssh-arch.c diff
    src/ssh/ssh-conn.c diff
    src/ssh/ssh-trans.c diff
    Compile out unreachable defense-in-depth guards under DSSH_TESTING Wrap ~46 dead-code branches in #ifndef DSSH_TESTING so coverage reports reflect only reachable code. Each guard has a comment explaining why it is unreachable: ssh-arch.c: dssh_parse_uint32 cannot fail after bufsz >= 4 check ssh-trans.c: rekey_time never 0 after init, enc blocksize always >= 8, payload_len always > 0, enc->encrypt/decrypt always non-NULL, all modules provide cleanup, ka->haskey always non-NULL, kex_selected validated before kex(), shared secret always non-empty, namelist buffers adequate, remote_languages never populated ssh-conn.c: send_extended_data len already clamped by public API, chan_type always set after init, channels array never contains NULL key_algo: cbd->pkey always set before sign/pubkey/save callable, caller buffers always adequate, EVP_PKEY_id always matches module, cleanup only called after successful init kex modules: ka and function pointers always set by negotiation, own-key pubkey always succeeds, serialize buffers adequate enc/mac: cleanup only called after successful init Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1069. Deucе
    Mon Mar 23 2026 20:49:11 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/CMakeLists.txt diff
    src/ssh/deucessh.h diff
    src/ssh/ssh-trans.c diff
    src/ssh/test/test_transport.c diff
    Add dssh_transport_set_version() public API New function to set the SSH software version and optional comment strings used in the version exchange (RFC 4253 s4.2). Validates: - software_version: non-NULL, non-empty, printable US-ASCII (0x21-0x7E); pass NULL to keep the library's built-in default - comment: printable US-ASCII (0x20-0x7E), spaces allowed; NULL to omit - Combined "SSH-2.0-version SP comment CR LF" must fit in 255 bytes - Must be called before any session is initialized Default version string "DeuceSSH-X.Y" now derived from PROJECT_VERSION via DSSH_VERSION_STRING compile definition. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1070. Deucе
    Mon Mar 23 2026 20:49:10 GMT-0700 (PDT)
    Added Files:
    

    src/ssh/TODO.md diff
    src/ssh/test/mock_alloc.c diff
    src/ssh/test/mock_alloc.h diff
    src/ssh/test/test_alloc.c diff
    src/ssh/test/test_dhgex_provider.h diff
    src/ssh/test/test_enc.c diff
    src/ssh/test/test_enc.h diff
    src/ssh/test/test_mac.c diff
    src/ssh/test/test_mac.h diff
    src/ssh/test/test_transport_errors.c diff
    Modified Files:

    src/ssh/CMakeLists.txt diff
    src/ssh/test/test_algo_key.c diff
    src/ssh/test/test_arch.c diff
    src/ssh/test/test_auth.c diff
    src/ssh/test/test_chan.c diff
    src/ssh/test/test_conn.c diff
    src/ssh/test/test_selftest.c diff
    src/ssh/test/test_transport.c diff
    Add branch coverage test suite: 537 tests across 11 executables Comprehensive test coverage for the DeuceSSH library, targeting every testable branch identified in an exhaustive audit of all source files. Test infrastructure: - mock_alloc.h/.c: countdown allocator via --wrap=malloc/calloc/realloc - test_enc.h/.c: XOR cipher as "aes256-ctr" with failure injection - test_mac.h/.c: XOR-fold MAC as "hmac-sha2-256" with failure injection, corrupt output, and oversized digest modes - test_dhgex_provider.h: DH-GEX group provider and RSA key test helpers - CMakeLists.txt: 4 KEX x key combos, 23 CTest configurations New test files (7): - test_alloc.c: 20 malloc failure tests across transport and auth - test_transport_errors.c: 11 enc/mac failure injection tests - test_algo_key.c: 67 tests for ed25519/RSA key operations, verify parse errors (malformed blobs), file I/O edge cases Extended test files (6): - test_transport.c: +30 tests for version exchange, GLOBAL_REQUEST handler, DEBUG/UNIMPLEMENTED edge cases, registration validation, getter-before-handshake, build_namelist overflow, packet_size clamping - test_auth.c: +14 client-side KBI error path tests - test_conn.c: +5 tests for start-twice, accept timeout, reject NULL, poll timeout - test_arch.c: +2 namelist parse edge cases - test_chan.c: +4 msgqueue peek, sigqueue stderr/truncation tests - test_selftest.c: DH-GEX and RSA key algorithm support Branch coverage results (ssh-chan.c reaches 100%): ssh-chan.c 100.00% ssh-arch.c 98.53% ssh.c 90.00% ssh-trans.c 79.12% aes256-ctr 72.22% ed25519 69.05% ssh-auth.c 62.88% rsa-sha2-256 62.36% ssh-conn.c 61.69% Remaining uncovered branches are OpenSSL error paths (82), dead code defense-in-depth (52), malloc failures needing --wrap extension (51), C11 thread init failures (16), and deep protocol paths requiring multi-threaded session infrastructure (~230). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1071. Deucе
    Mon Mar 23 2026 20:49:10 GMT-0700 (PDT)
    Modified Files:
    

    src/ssh/ssh-trans.c diff
    Fix send_packet buffer overflow with large MAC digest Move mac_len computation before the packet buffer size check so the overflow guard accounts for the MAC bytes that will be appended after the encrypted packet. Without this, a MAC module with a digest larger than the padding headroom could write past the end of tx_packet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
  1072. Rob Swindell (on Debian Linux)
    Mon Mar 23 2026 20:20:37 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/podcast_routines.js diff
    Apparently an excplicit http.sock.close() is rquired between Head requests or else podcast.js will create an open connection for every episode of the podcast. This looks like a hack, but there's no Http close method and explicitly deleting the 'http' object here didn't close the socket either. This fixes the Error: Unable to parse status line '429 Too Many Requests' getting head of http://mp3.techdorks.net/episodes/techdorks-2015-11-04-ep9.mp3 I've been getting every time podcast.js ran.
  1073. Rob Swindell
    Mon Mar 23 2026 19:08:14 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/rip_lightbar_menu.js diff
    exec/load/rip_scrollbar.js diff
    xtrn/ddfilelister/ddfilelister.js diff
    Merge branch 'rip_scrollbar_horizontal_and_vertical' into 'master' The RIPScrollbar class (in rip_scrollbar.js) can now be used horizontally as well as vertically. Updated RIPLightbarMenu accordingly. Small change in ddfilelister.js to use the RIP horizontal scrollbar only when extended descriptions are disabled. See merge request main/sbbs!667
  1074. Eric Oulashin
    Mon Mar 23 2026 19:08:14 GMT-0700 (PDT)
    Modified Files:
    

    exec/load/rip_lightbar_menu.js diff
    exec/load/rip_scrollbar.js diff
    xtrn/ddfilelister/ddfilelister.js diff
    The RIPScrollbar class (in rip_scrollbar.js) can now be used horizontally as well as vertically. Updated RIPLightbarMenu accordingly. Small change in ddfilelister.js to use the RIP horizontal scrollbar only when extended descriptions are disabled.
AuthorCommitsLatest
Rob Swindell476Mon Jun 29 2026 23:37:57 GMT-0700 (PDT)
Deucе576Mon Jun 29 2026 22:56:04 GMT-0700 (PDT)
Thomas McCaffery7Tue Jun 23 2026 01:33:12 GMT-0700 (PDT)
xbit ops2Sat Jun 06 2026 13:34:01 GMT-0700 (PDT)
HM Derdok3Thu May 07 2026 20:16:56 GMT-0700 (PDT)
Eric Oulashin8Mon Apr 27 2026 09:07:17 GMT-0700 (PDT)
Nigel Reed1Sun Apr 26 2026 12:30:45 GMT-0700 (PDT)
echicken1Wed Apr 22 2026 07:33:12 GMT-0700 (PDT)

For older commits (in CVS), click here

Dynamically generated in 528 milliseconds