tinymux (2.12.0.10-2) [PTS] [DDPO]
OLD: VCS is behind the version in the archive: 2.10.1.14-1 < 2.12.0.10-2.Hint: This git repository uses http. You might need to run git update-server-info to update the auxiliary info files.
- Git: https://github.com/brazilofmux/tinymux.git
- Branch: master
- Path: debian/changelog
- Repo size: 13123584
- Browser: https://github.com/brazilofmux/tinymux
- Last scan: 2026-10-15 06:20:04+00
- Next scan: 2026-10-16 05:36:00+00
- Debian changelog in Git:
tinymux (2.10.1.14-1) unstable; urgency=medium * New upstream release + fixes integer/pointer comparison (Closes: #853683) * Update Standards-Version in debian/control from 3.9.8 to 4.0.1 (no change needed). * cppflags.diff was accepted upstream and therefore dropped from the package. * Added PCRE license to debian/copyright. * Dropped unnecessary build dependencies (Closes: #877604) -- Stephen Dennis <brazilofmux@gmail.com> Tue, 03 Oct 2017 07:55:00 -0600 - This branch is 50 commits ahead of tag Build-2.14.0.11
- Git log:
commit 5f3a3f1ded91f069cef9247837d3b59e779bcb78 Merge: 6aaf6266 22bbd002 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Fri Oct 9 01:33:57 2026 -0600 Merge pull request #2308 from brazilofmux/docs/2307-unicode-license docs(#2307): ship the Unicode License V3 notice with the Unicode data and tables commit 22bbd002133b5575dd49e5a0de2fa408ba1a8073 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Fri Oct 9 01:32:47 2026 -0600 docs(#2307): ship the Unicode License V3 notice with the Unicode data and tables TinyMUX vendors Unicode data under utf/ and ships tables generated from it in every release package, but carried the Unicode License V3 notice nowhere; the data files' one-line headers point at unicode.org's terms rather than reproducing the notice the license asks to appear with all copies. libutf, which shares this lineage, closed the same gap in 5cef455. - LICENSE-UNICODE at the top level and as mux/LICENSE-UNICODE, verbatim from libutf, so the source tree and every package carry it. Packages are built from mux/ alone, which is why the second copy exists. - unix/TOC.patchable and both win32 patchable lists name it, so the Unix tarball and the Windows source and binary packages all include it. - LICENSE says which parts are Unicode License V3, beside its existing PCRE note; copyright.h, the one license file every package already carries, says the same in shipped-tree paths. - debian/copyright, the per-file manifest, gains a Unicode-3.0 stanza for utf/*.txt and the six generated tables, and the license text. utf/ConvertUTF.* keep their own earlier Unicode notice in-file and are not in any package; unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K24f5N3TzqpFfaVqQqD7cv commit 6aaf6266346e10fa1e778b8ceff67ca470e73ddf Merge: 19ffa49c 6fee75f9 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Oct 8 22:55:00 2026 -0600 Merge pull request #2304 from brazilofmux/fix/collate-implicit-trail fix(collate): keep all 16 bits of an implicit weight's trail commit 6fee75f9f23c82544209adb1a4c2604cb2705a1a Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Oct 8 22:43:32 2026 -0600 fix(collate): keep all 16 bits of an implicit weight's trail UCA 10.1.3 gives an ideograph two CEs, a lead and a trail BBBB << 16 with BBBB = (cp & 0x7FFF) | 0x8000. CE_PRIMARY masked every primary to 15 bits, so the trail 0x8000 of U+8000, U+18000, U+20000, U+28000 and U+30000 became 0 -- an ignorable -- and those ideographs compared against whatever followed them: "耀b" sorted after "老a" (U+8000, U+8001). A trail is the only CE with a primary but no secondary or tertiary, so CE_PRIMARY now keeps all 16 bits for it and masks the rest as before. ducet_cetable.h is unchanged; DUCET itself holds no 0x8000 trail. Ported from libutf dfa04c5, which instead drops the unused variable flag. test_collate_implicit_trail fails without the fix and passes with it; expectations checked against ICU 72 root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> commit 19ffa49c959c74c2d342733fe4099516c6b87181 Merge: 15ba1c9c e10219a1 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Oct 8 21:42:43 2026 -0600 Merge pull request #2301 from brazilofmux/fix/collate-contractions fix(collate): apply contractions that start with ASCII, and the three-code-point ones commit e10219a1a1199e9b850dfa046f98c211b192421c Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Oct 8 21:29:43 2026 -0600 fix(collate): apply contractions that start with ASCII, and the three-code-point ones ExtractCEs and the Latin fast paths assumed ASCII starts no contraction, but DUCET contracts l and L with a middle dot (U+00B7, or U+0387) for Catalan's ela geminada, so the dot weighed in as a punctuation primary instead of an accent on l: "L·" sorted after "L!". And ducet_contract3 was generated but never read, so the eight three-code-point contractions (Kannada, Sinhala, Tibetan, Gurung Khema, Kirat Rai) were split into their parts. Characters that can start a contraction now leave s_latin_ce at 0 and keep their CE in s_latin_starter_ce, used only when the next character does not complete a contraction; the ASCII byte-skip stops at them. The fast sort-key paths keep a local cursor, which GCC otherwise pins to memory once the full path's helpers stop inlining. Ported from libutf b0d12f5. Against ICU 72 root, on 700,336 pairs built around every contraction, libmux now disagrees 0 times. test_collate_contractions fails without the fix and passes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> commit 15ba1c9cbd8193965895134b1ef6e6212e75d1e4 Merge: 94bb46dd 5d1c811d Author: Stephen Dennis <brazilofmux@gmail.com> Date: Tue Oct 6 16:53:37 2026 -0600 Merge pull request #2299 from brazilofmux/fix/2298-telnet-signed-char fix(#2298): telnet UTF-8 input dead on unsigned-char platforms commit 5d1c811db005a73000d2870063bbaafcd80174df Author: Stephen Dennis <brazilofmux@gmail.com> Date: Tue Oct 6 16:45:30 2026 -0600 fix(#2298): read telnet UTF-8 COPY lengths through signed char process_input_helper walked cl_print_sbt with static_cast<char>. COPY phrases are stored as negative lengths, so on platforms where plain char is unsigned (Linux aarch64, ARM32, PowerPC, s390x) no phrase ever read as COPY, the state machine never accepted a byte, and a connection that negotiated CHARSET UTF-8 -- by either route -- could not even log in. Every other walker of these tables already uses signed char or the explicit y < 128 / 256 - y form; this was the only plain-char site. tests/scenario/telnet_negotiation.py gains two cases, one per route into UTF-8 (client ACCEPTED of the server's REQUEST, and the client's own REQUEST), each logging in and round-tripping a non-ASCII @pemit. The existing CHARSET cases negotiated and disconnected, so none typed a line in UTF-8 mode. On this aarch64 box, before the fix: both round trips fail with no output at all. After: telnet-negotiation 21/21, make test 37/37. On x86-64 the new cases pass either way; they detect this only on an unsigned-char host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> commit 94bb46dd55072d9d1fb1e556f15560b7feaf7308 Merge: 2a3c7c91 34128517 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Tue Oct 6 15:53:39 2026 -0600 Merge pull request #2296 from brazilofmux/fix/2295-addcom-stale-alias fix(#2295): addcom re-joins a stale alias; @cboot stops leaving them commit 341285179bf5ad58b312ac0e7aacc6aa52deff99 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Oct 5 21:16:02 2026 -0600 fix(#2295): @cboot drops the victim's aliases for the channel @cboot removed the victim from the member list but kept its aliases, which then pointed at a channel the victim was not on -- the state that made addcom refuse to re-join. Both implementations now sweep the victim's aliases for that channel after the boot (built-in via a per-player split of purge_aliases_for_channel). With @cboot fixed, nothing in 2.14 produces an alias without its membership, so the addcom rejoin path is now covered where that state can still come from: a carried-over database. test-comsys-handoff builds it by deleting the channel_users row under each implementation, asserts the loader keeps the orphan alias (precondition), then asserts addcom re-joins. comsys_fn TC021 now asserts @cboot leaves neither membership nor alias, and that a plain addcom puts both back. Negative control (master's comsys C++, new tests): handoff 10 and 12 fail (POST=0:se / 0:sm) with preconditions holding, and TC021 fails under both module and built-in (after boot=0:cst). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> commit aa9b82034b4b9b35947fc0758e421edae113aa52 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Oct 5 21:06:42 2026 -0600 fix(#2295): addcom re-joins through an alias it already holds An alias can outlive the caller's membership in its channel: @cboot removes the member but keeps the alias, and 2.13's @cdestroy left aliases behind outright. addcom then saw the alias, refused it as a duplicate and returned before the join step, so repeating the addcom never recovered -- only delcom followed by addcom did. When the existing alias already maps to the requested channel and the caller is not a member, finish the join instead. An alias that maps to a different channel is still refused. The duplicate check now runs before the alias cap, so a re-add of an existing alias no longer reports "maximum number of aliases" -- which misdirected the diagnosis in the field report. Applied to both the comsys module and the engine's built-in path. comsys_fn TC021 builds the stale state with @cboot/quiet on self and asserts it was built (member gone, alias kept) before asserting the rejoin. Negative control: without the C++ change TC021 fails with "after boot=0:cst, after addcom member=0". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> commit 2a3c7c91b453535c76dd3781115c0c987a9d3076 Merge: b92161f1 8da85e5a Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Oct 4 12:03:35 2026 -0600 Merge pull request #2294 from brazilofmux/fix/2292-penn-password-v2 fix(#2292): accept PennMUSH's current 2:<algo>:salt+hash passwords commit 8da85e5aa552b9fa90ccf0900230ab79ad303e64 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Oct 4 12:03:06 2026 -0600 fix(#2292): accept PennMUSH's current 2:<algo>:salt+hash passwords The 2.14 half; 2.13's landed as #2293. Current PennMUSH stores passwords as 2:<algo>:<s1><s2><hex(algo(s1 s2 password))>:<timestamp> with sha512 the default algo (src/mycrypt.c, password_hash() and password_comp()). Omega carries that across behind a $P6H$$ prefix, but p6h_vaht_crypt() recognised only the older unsalted 1:sha1 form, so every password from a current Penn database failed. p6h_vaht_crypt() now parses $P6H$$<version>:<algo>:<field>:<timestamp> field by field for versions 1 and 2, as Penn's password_comp() does: version 2 takes the first two characters of the field as salt and digests salt then password; version 1 digests the password alone. It still returns the rebuilt setting for check_pass()'s strcmp, copies the timestamp, and emits lowercase hex as Penn does. Every read is bounded by what the parse found, so the fail-closed property #1182 added to the old fixed-width 1:sha1 parse is kept: a truncated A_PASS fails rather than being read past its end. Unlike the 2.13 change, the digest goes through libmux's mux_digest(), so sha512 and the other names Penn may have compiled work on both crypto backends, Windows CNG included. No upgrade code is needed: check_pass() already replaces a Penn hash with a native one at the first successful login. Verified against a 2.14 server with vectors replicating Penn's password_hash() byte for byte: 2:sha512 right / wrong / salt typed in OK (now native) / FAIL / FAIL 2:sha256, 2:sha1 OK 1:sha1 right / wrong OK / FAIL truncated 1:sha1 (#1182 shape) FAIL malformed 2:, unknown algo FAIL native $SHA1$ OK make test-smoke: all 1674 passed (1 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcJaEiKW33UDU18G6cTfFn commit b92161f132d3784f055b0670e39925a8693f23ad Merge: 0724d7bb 0c7192a4 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Sep 28 22:29:09 2026 -0600 Merge pull request #2284 from brazilofmux/fix/grapheme-color-ops-uax29 fix(#2283): bring color_ops' grapheme stepper to UAX #29 parity commit 0c7192a4f76dfda30f79c7c9dcddaf9e1e9dd888 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Sep 28 21:50:55 2026 -0600 fix(#2283): bring color_ops' grapheme stepper to UAX #29 parity TinyMUX has two grapheme steppers. c3fafc153 fixed GB4, GB12/13 and GB11 in utf8_next_grapheme and left color_ops' next_grapheme_plain alone, so for two months they disagreed: the interpreter's strlen() said two clusters where mid(), delete(), left(), right() and a JIT-compiled strlen() -- all color_ops -- said one. Port the three fixes into color_ops.rl: - GB4: nothing but LF joins a CR. From libutf e46427a. - GB12/13: regional indicators pair only when adjacent, never through an Extend or ZWJ. From libutf e46427a. - GB11: a three-state tracker in place of the boolean, so "ExtPict ZWJ ZWJ ExtPict" breaks before the second pictograph. From c3fafc153; libutf still lacks this one. color_ops.c is regenerated from the .rl. softlib.rv64 is rebuilt with the recorded toolchain (riscv-none-elf 15.2.0), and test-blob confirms it matches its source. Verification: - GraphemeBreakTest-16.0.0 (1,093 cases): color_ops 39 -> 7 mismatches, the 7 being GB9c, which neither stepper implements. - Exhaustive differential, every sequence of up to five code points over one per GCB class (813,615): 44,536 disagreements -> 0. - tests/color_ops: 7 checks; 4 fail on master's color_ops.c, 3 are positive controls. - tests/libmux: test_grapheme_steppers_agree pins both steppers against the same answers; fails on master's libmux. - smoke strlen_fn TC006: master fails (1 1 1 0 0 1), branch passes. - make test EXPECT_CONFIG="jit=yes stubslave=yes": 37/37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K24f5N3TzqpFfaVqQqD7cv commit 0724d7bb13114d4f2c98f4471946156a2946f0ef Merge: 3e0ad0ae f2e706b4 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Wed Sep 9 22:15:21 2026 -0700 Merge pull request #2282 from brazilofmux/fix/2281-do-toad-pcache fix(#2281): drop pcache entry in do_toad before the type flip commit f2e706b435fc1b7a42f3ea7c2d54cc4d35ae032c Author: Stephen Dennis <brazilofmux@gmail.com> Date: Wed Sep 9 23:14:45 2026 -0600 fix(#2281): drop pcache entry in do_toad before the type flip #1180 wired pcache_delete into destroy_obj / destroy_bad_obj, but do_toad has the same order — flip to TYPE_THING then s_Pennies(1) — so the attribute gets 1 while the dirty cache keeps the old balance and flushes it back on @dump. Same one-line fix as the 2.13 backport in #2280. commit 3e0ad0ae06b81289dcb27e1aaae4974136565857 Merge: 7a088f7d 4e17802e Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Sep 6 13:06:31 2026 -0700 Merge pull request #2251 from brazilofmux/fix/2250-brokenreason-visibility fix(#2250): put DCL_EXPORT ahead of the pointer return type commit 4e17802e61295235c8790d912fa4e4808ff7a2a5 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Sep 6 13:34:21 2026 -0600 fix(#2250): put DCL_EXPORT ahead of the pointer return type Written after the `*`, `__attribute__((visibility("default")))` binds to the pointer type rather than to the function, and GCC discards it -- with a -Wattributes warning that read as routine noise in the build log. Under -fvisibility=hidden that left Pipe_BrokenReason out of libmux.so's dynamic symbol table, so netmux failed to link against the call added in the same change at ganl_adapter.cpp:4133. Pipe_IsBroken, declared two lines above with a bare `bool` before the macro, was exported normally: spelling the star is what moved the macro past the declarator. These two sites were the only occurrences in the tree. Moving the macro ahead of the return type is also the canonical __declspec position on Windows, where the original spelling happened to work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcJaEiKW33UDU18G6cTfFn commit 7a088f7d51e8c324ef11450bdf3c2b66728e72a6 Merge: 312931c3 2ba8b2cb Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Sep 6 11:07:31 2026 -0700 Merge pull request #2245 from brazilofmux/fix/pipe-protocol-fault fix(#2244): fault the module transport loudly on a corrupt frame header commit 2ba8b2cb55131bb8bad76a5628a24939d3653c5d Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Sep 6 10:56:45 2026 -0700 fix(#2244): fault the module transport loudly on a corrupt frame header The #2238 core forensics proved which branch ran during the farm's 22-day wedge: Pipe_DecodeFrames' protocol-error branch. It was silent -- no log, no teardown, and the same `return false` the decoder uses for "need more bytes" -- so the host looped back to its pump and waited for input that could never make the stream parseable again. A header that fails validation now marks the transport broken, once, with a kept reason. From then on the decoder consumes nothing and every Pipe_Send* fails with a new MUX_E_PROTOCOL. Pipe_SendReceive gives the host exactly one pump call after the fault so it can observe it (Pipe_IsBroken / Pipe_BrokenReason), then fails regardless: looping back could never terminate on its own, since a peer that keeps sending defeats the pump's stall detection. Header validation also rejects CHANNEL_INVALID, which the stub's pump waits on, and the send entry points refuse it before writing a header so no peer from this tree emits one. A new transport resets the decoder statics. netmux's pump_stubslave and muxscript's script_pipepump log the reason and stop the stubslave; the stub prints it to stderr and exits. tests/libmux drives the header recovered verbatim from the farm's core through the real decoder and asserts the fault after exactly two pump calls, plus CHANNEL_INVALID at both ends (in a forked child, since a transport installs once per process). Both fail against a neutered decoder without hanging. tests/stubwedge/run_fault.sh boots muxscript against a stand-in stubslave that writes those nine bytes and holds the pipe open, and asserts the fault line, that the stall watchdog did not fire, and that the game continued. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K24f5N3TzqpFfaVqQqD7cv commit 312931c3331cd4f1725eb93ffe209e1d21034ec5 Merge: cb219da3 6d325881 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Sep 6 09:36:34 2026 -0700 Merge pull request #2243 from brazilofmux/fix/2240-cansee-probe-and-jit-decline fix(#2240): guard cansee() in TC003, and stop the JIT reconstructing unknown names commit 6d3258815d68e670f76cff7043535d563f6045cb Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sun Sep 6 09:26:06 2026 -0700 fix(#2240): guard cansee() in TC003, and stop the JIT reconstructing unknown names powersee_fn TC003 called cansee() unconditionally; the function exists only on WOD_REALMS / REALITY_LVLS builds, so every smoke variant failed on a build without them (#1946 class -- invisible on the standard configure line). Guarded with the emitconn_fn TC004 convention: an unknown name in argument context reconstructs as literal text, so matching "cansee(*)" detects the missing builtin and reports Skipped. Reproduced on a jit+stubslave/no-realms worktree: the literal is "cansee(me,get(me/thing))" -- nested call unevaluated, formatting space dropped -- and the probe matches it. ThresholdOps' review of the issue raised two points about that convention, both of which held up under verification: 1. Nothing pinned the reconstruction shape. Every Skipped-probe guard depends on it, and a guard cannot fail if the shape changes -- it reports Skipped forever, which the accounting treats as conclusive. jit_route_parity_fn TC013 now pins both arms: argument context comes back as the SOURCE text with nested calls unevaluated and brackets still evaluated (nosuchfn(x,[add(3,4)]) -> "nosuchfn(x,7)"), and bracket context is EV_FMAND -> "#-1 FUNCTION (X) NOT FOUND" (#1247). A change to either now fails loudly instead of silently. 2. hir_lower's reconstruction arm evaluated the arguments -- its own comment said so -- while the AST's ast_emit_literal_funccall strips EV_FCHECK and emits them as text. Confirmed in the HIR dump: rvbench(strcat(nosuchfn(add(1,2))),3) compiled to the constant "nosuchfn(3)" where the interpreter yields "nosuchfn(add(1,2))". A real route divergence, though no ordinary softcode could see it: jit_can_handle refuses any unresolved name before compiling, so the arm was reachable only through rvbench(), which bypasses that gate. It now declines (rc.out_exhausted, the established idiom), so the interpreter's implementation is the only one -- the same "no dual emission paths" rule the MAP arm states. rvbench() of an unknown name reports COMPILATION FAILED; a known-function control still compiles. TC013's last leg pins the decline via benchmark()'s jit_handled=0, gated on jitstats() so no-JIT builds skip it cleanly. The mandatory-context arm (#1247) is untouched. Their fourth point -- rvbench_fn.mux is unguarded but harmless -- also checks out, with one refinement: TC056 does carry a Succeeded branch, but it is gated on u(me/tr.has_jit), and the BENCH00N labels have none, so the generator counts nothing that a no-JIT build cannot report. Verified: make test EXPECT_CONFIG="jit=yes stubslave=yes" on master (realms on) 37/37, TC003 Succeeded, TC013 Succeeded x3; the same on a no-realms worktree 35 passed / 2 skipped (nls) / 0 failed, TC003 Skipped, TC013 Succeeded x3. Every TC013 leg was also run standalone through muxscript and returned 1 before the suite was trusted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K24f5N3TzqpFfaVqQqD7cv commit cb219da30497f146824bdcf09541cc5bea88f203 Merge: 03322237 ead51cee Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Sep 3 21:42:51 2026 -0700 Merge pull request #2242 from brazilofmux/test/collate-nfc-equivalence test(collate): pin canonical equivalence through the NFC tiebreak commit ead51cee1de03b69e0dd5603e681ce7910f7d8dd Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Sep 3 21:42:08 2026 -0700 test(collate): pin canonical equivalence through the NFC tiebreak Collation had no unit coverage at all, which is how a port that looked like a fix nearly landed as a regression. The tiebreak exists for one job: make canonically equivalent strings compare equal once DUCET weights have tied. "e"+U+0301 against the precomposed U+00E9 is the textbook pair -- same weights, different bytes, NFC reconciles them. This asserts that at 500 characters as well as at one, and that the comparator and the sort key agree about it. Written while evaluating libutf d19d65e, which replaces its tiebreak buffers with a small fixed budget. That fix is right for libutf and wrong here: libutf's buffers were VLAs sized to each operand's own input, so two operands truncated at different points and a comparison could disagree with itself. TinyMUX's are fixed and equal-sized and never had that bug. Adopting the budget would fix nothing and break this test -- GetNFCBytes' fast path returns already-NFC input untouched at ANY length while only the slow path would be capped, so a long precomposed string compares in full against its decomposed twin truncated at the budget, and text that IS the same orders as different. Measured: the first assertion fails with a 768-byte budget. No source change. The comment records the finding so the port is not re-attempted, and notes that if the LBUF_SIZE buffers are ever revisited for stack (64KB in CompareNFCTiebreak, 48KB in the sort-key path, both measured) the requirement is to bound BOTH paths to the same output length. Two earlier versions of this test passed against the deliberately broken build before I noticed why: a composition exclusion's decomposed form IS its NFC form, so that input took the fast path and never touched the buffers under test. make test EXPECT_CONFIG="jit=yes stubslave=yes": 37 targets, 37 passed, 0 skipped, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K24f5N3TzqpFfaVqQqD7cv commit 0332223735b21d5ec0012deabec58f6b9dde663f Merge: bb9562bf 22f602a4 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Sep 3 09:21:12 2026 -0700 Merge pull request #2239 from brazilofmux/fix/2238-stubslave-wedge fix(#2238): bound the stubslave pump, and stop Pipe_AppendBytes corrupting frames commit 22f602a494e4a8c90412f42a34248345281f8210 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Sep 3 09:13:03 2026 -0700 fix(#2238): bound the stubslave pump, and stop Pipe_AppendBytes corrupting frames Two independent defects behind the 22-day farm hang; the first is proven by test, the second is the hang itself. **Pipe_AppendBytes never advanced its source pointer.** The copy loop fills the tail QUEUE_BLOCK, allocates another, and copies again -- from the START of the caller's buffer each time. An append spanning a 32 KB block boundary therefore wrote the first chunk twice and dropped the tail. nBytes was still advanced correctly, so the queue reported the right LENGTH with the wrong CONTENT: not a visible short write, but a mis-framed module-IPC packet that the peer then desyncs on. It is reachable on a long-lived channel because Pipe_GetBytes consumes a block in place and only frees it once fully drained, so pBuffer marches toward the end of the block and the free space ahead of it shrinks to a few bytes under steady small traffic -- exactly the stubslave's profile. tests/libmux gains a case that fills to five bytes short of a block and appends across the seam; it fails on the old library and passes on the new one. **Both pipe pumps polled with an infinite timeout.** A synchronous COM call parks the WHOLE process inside the pump, and do_dbck drives one round-trip every check_interval (default 600s), so a channel that goes quiet takes the server with it: no network events, no timers, no dumps, no signal processing, not even @shutdown -- only SIGKILL ends it. The farm deployment sat that way, silent and at zero CPU, for 22 days. Both pumps now poll in one-second slices and count time in which no byte moved in either direction. Past the limit they log loudly, tear the channel down (shutdown_stubslave / shutdown_stubslave_parent, which already make later COM calls fail cleanly rather than hang) and let the game continue without its stubslave. A degraded stubslave should cost its features, not the deployment. 60s default -- orders of magnitude above a healthy ModuleMaintenance, well under any human's patience -- overridable with TINYMUX_STUB_STALL_MS, which the regression drives down to four seconds and which doubles as an operator escape hatch. GanlAdapter::pump_stubslave and script_pipepump had the identical unbounded poll: the incomplete-hardening shape this repo keeps finding, where one path is guarded and its sibling is not. Both are fixed here; fixing only the one the backtrace named would have left muxscript able to hang the same way, which is how the new test caught it -- it wedged muxscript after the adapter was already fixed. tests/stubwedge reproduces the wedge deterministically with SIGSTOP: the stubslave stays alive with its socket open, so there is no EOF and no HUP to rescue the poll, and never answers -- the shape of the captured hang. It then drives @dbck, the command the farm hang was attributed to, and asserts the process survives, logs the stall, and keeps executing commands. Pre-fix it times out with all three assertions failing. NOT established: which defect lost the bytes in the captured hang. The append bug corrupts content while preserving the byte count, so it does not by itself explain 18 missing bytes. Left a note on the issue with the exact predicate to check against the captured cores. Verified: make test EXPECT_CONFIG="jit=yes stubslave=yes" -- 37 targets, 37 passed, 0 skipped, 0 failed. This is the first run in this tree with nothing skipped; the default configure omits --enable-stubslave, so both stubslave harnesses had been skipping green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K24f5N3TzqpFfaVqQqD7cv commit bb9562bf1ab5e8c167f553493110ea072190b17f Merge: 2d9154bd 21b85189 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sat Aug 15 12:26:05 2026 -0600 Merge pull request #2235 from brazilofmux/docs/2234-markdown-block-syntax-wraps docs(#2234): stop wrapped prose lines from starting with block syntax commit 21b8518972598bc3f68698abd8b24efe997a0338 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Sat Aug 15 12:25:53 2026 -0600 docs(#2234): stop wrapped prose lines from starting with block syntax mdfix over all 147 tracked .md files produced 87 findings. Nineteen were the same false positive, and its proposed repair was destructive: a hard-wrapped line beginning `#2135` is paragraph text under CommonMark but an <h1> under Markdown.pl, and "fixing" the heading spacing would have rewritten the issue reference to `# 2135` and split the sentence. The false positive was pointing at something real. Verified with pandoc: mux/CHANGES.md rendered 26 <h1> under markdown_strict where it has only 16 real headings — ten spurious headings mid-paragraph. Four variants of the same class were found: `#NNNN` (issue ref), `NNNN.` (a year — an ordered list marker), `+ `/`- ` left at line start by wrapped prose, and `(1)`. All are now reflowed so no prose line starts with block syntax; wording is unchanged (word counts identical per file, checked). With the ambiguity gone the linter is trustworthy — 19 false positives to 0 — and the genuine findings underneath were applied: - 46 lists that immediately follow a paragraph now have a blank line (they collapse into that paragraph under markdown_strict); - 11 paragraphs that immediately follow a list now have one — this shape is absorbed into the last bullet in EVERY dialect, GitHub included (docs/survey-jit-perf.md's "Parity verified (...)" was rendering as part of the preceding bullet); - 30 lists directly under a heading, cosmetic but consistent. Two content bugs surfaced on the way: a stray `)` on its own line in docs/plan-softcode-post-entry-contract.md, and a tab-indented list continuation in mux/ATTACK.md whose siblings use four spaces. CLAUDE.md gained the trailing newline it was missing. Verified: every tracked .md is mdfix-clean; no line in any file starts with block syntax outside a fence; spurious <h1> counts now match the real heading counts exactly; per-file word counts unchanged except the removed `)` and the added "Issue" that de-fangs a paragraph-initial #2048. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> commit 2d9154bd78bab9a20fb8590cead9cf03e1b454cb Merge: 5d6f7fb4 ee4477a3 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Aug 13 00:44:06 2026 -0600 Merge pull request #2233 from brazilofmux/fix/2232-nfc-valid-prefix fix(#2232): a bounded utf8_normalize_nfc result is always a valid prefix commit 5d6f7fb4cf2b7eeaff7fbef0132a25ce893c850b Merge: 823a9291 6f5237a7 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Aug 13 00:43:54 2026 -0600 Merge pull request #2231 from brazilofmux/fix/2229-conn-idle-truncation-214 fix(#2229): stop truncating 64-bit connect and idle deltas to int (2.14) commit ee4477a30e312d02378a692941328f0670fdf4f4 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Aug 13 00:38:04 2026 -0600 fix(#2232): a bounded utf8_normalize_nfc result is always a valid prefix Both output paths could lose text in ways worse than truncation: - The fast path (already-NFC input) was a truncating memcpy with no UTF-8 boundary check, so a byte-count cut could split a multi-byte sequence and hand the caller invalid UTF-8. It now backs off over the continuation bytes and the split sequence's lead; the walk is over boundary-validated input, so it is defined by contract. - The slow path's encode loop SKIPPED a code point that did not fit and kept appending -- a wide code point dropped from the middle while later, narrower ones were still written, yielding the correct string with a hole in it, indistinguishable from text that legitimately composed to that size. It now stops at the first code point that does not fit. Truncation remains silent (the void signature and the status-return question from the issue are left to the maintainer -- the internal callers are all LBUF-bounded), but whatever IS returned is now a valid UTF-8 prefix of the full normalized string on both paths. tests/nfc gains the issue's demo vector (a-acute + U+1D15E + XY, whose excluded composition stays 4+4 bytes) at bounds 4 and 8, plus a fast-path mid-sequence cut; the characterization comment that pinned the old behaviour as intentional now documents the prefix guarantee. Negative control run: the three new cases -- and only they -- fail against the pre-fix code. Suite 35/0 (jit=yes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> commit 6f5237a775476af0753af2693b63b578da20bb57 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Thu Aug 13 00:24:07 2026 -0600 review(#2231): digit_format was the fourth sibling still narrowing fun_digittime parses with mux_atoi64 into int64_t and then called digit_format(int) -- the implicit narrowing survived the widening of its three siblings, so digittime(6220596985) answered 22287d 09:08 (the value modulo 2^32) while singletime() of the same value said 197y. Widened digit_format; days is the unbounded field (15 digits max), hours/minutes are remainders and keep int. mux_vsnprintf's %lld arm (nLongs==2 -> mux_i64toa) carries it. Verified: digittime(6220596985) now 71997d 15:36, agreeing with the PR's own WHO figure; digittime(90061) unchanged at 1d 01:01; negative clamp intact. Full suite 35/0 on macOS arm64 (jit=yes) -- this branch's first run on this architecture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> commit 2264d602dc8fe31d34d337c6c38dc2b43c173e7f Author: Stephen Dennis <brazilofmux@gmail.com> Date: Wed Aug 12 15:26:30 2026 -0600 fix(#2229): stop truncating 64-bit connect and idle deltas to int CLinearTimeDelta::ReturnSeconds() is 64-bit, but the reporting path narrowed it to int, so a delta past 2^31 wrapped into a plausible smaller number instead of an obviously wrong one. That is how 2.13's #2227 stayed hidden: a live descriptor whose conn(<port>) is 6,220,867,076 s -- 197 years -- shows in WHO as 22290d 11h, exactly that value less 2^32. 2.14 already widened the softcode entry points (mux_atoi64 in fun_singletime and friends) but narrowed the value straight back at time_format_2, so singletime() was wrong anyway. The formatters cross the driver/engine boundary, so this is an ABI change: TimeFormat1 and TimeFormat2 on mux_IGameEngine, and FetchIdle and FetchConnect on mux_IConnectionManager, all widen. IID_IGameEngine and IID_IConnectionManager are bumped accordingly, and both implementations of the connection manager -- netmux's CConnectionManager and muxscript's CScriptConnectionManager -- move together. Also widened GeneralTimeConversion and its int iValue accumulator. With a 64-bit input the term-by-term remainder subtraction overflowed and every term printed the whole span rather than what was left of it; exptime() answered "197y 1634M 7104w 49713d 1193069h 71582803m 1s". The buffer-size comments there already reason in terms of 2^63, so 64-bit was the original intent. tf1_width_table used INT_MAX as the end-of-cases sentinel for widths 10 and 11. Widening the parameter alone would let a large value walk past the final case into case 3, whose format spec is empty at those widths. The real range maxima from the comment block replace it, and a value too large for the available columns now yields "???" -- which the function already emits for a bad maxWidth. Rendering an impossible duration as a believable one is the failure being fixed; a marker is the point. Verified against a running server, with #2227's corruption injected on the driver side so the oversized value has to survive the vtable crossing: singletime(6220596985) 61y -> 197y exptime(6220596985) cumulative junk -> 197y 1M 2w 12h 20s WHO On For 22290d 11h -> 71997d 15h 197y 1M 2w 12h 20s decomposes back to 6,220,596,985 exactly. Ordinary values are unchanged: singletime(90061) is 1d and a fresh session still reads 0:00. engine.so loads against the bumped IIDs. make test: 34 passed, 2 skipped, 0 failed (jit=yes stubslave=yes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> commit 823a9291ee95380c1af5cfca568246fdd168770b Merge: 07586ea7 86306410 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 23:25:12 2026 -0600 Merge pull request #2226 from brazilofmux/fix/2223-ship-windows-build-docs docs(#2223): ship current Windows build instructions in the source distribution commit 07586ea7605ae78f27217044928714672ec47da4 Merge: 66991dcb e4ff0b99 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 23:24:57 2026 -0600 Merge pull request #2225 from brazilofmux/fix/2222-stale-companion-checksums fix(#2222): clear companion checksums when their artifacts are cleaned commit 66991dcbe5105c32c8434e07659d8651e38e6946 Merge: a54d379f dfe6c3d7 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 23:23:51 2026 -0600 Merge pull request #2224 from brazilofmux/fix/2221-stage-pcre2-runtime fix(#2221): stage the vcpkg PCRE2 runtime beside the build output commit 86306410c452921832793afb5642bc94aa7821b8 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 23:06:51 2026 -0600 docs(#2223): ship current Windows build instructions in the source dist docs/building.md sits outside mux/, so the Windows source archive never carried it. The Windows document that does ship, readme.txt, was last touched in July 2012: it pointed builders at mux2.14/src/netmux.dsw, described the binaries as Intel 9.1 builds for Windows 98 through Vista with a separate win64 subdirectory, and gave unpacking instructions for PKZip, JAR and tar/gzip. The archives are .zip, x64 only, and that .dsw has since been deleted outright by win32/TOC.src.removed. Rewrites the distribution and build sections against what the tree actually ships: the vcpkg manifest install with the shallow-clone and first-run caveats, the vcpkg_installed layout and $(VcpkgDir) override, netmux.sln under Visual Studio or MSBuild, the file list to stage into game/bin, and why the redistributable DLLs appear in the binary distribution but not in a source build. Two corrections found while checking each claim: cscript startmux.wsf has been superseded by Startmux.bat, which says so in its own header comment; and mux.config is on neither Windows TOC, so step 4 was directing operators at a file the distribution does not contain. The database section is left alone. It is older still, but nothing in it was verified here and it is outside what this issue asks for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> commit dfe6c3d7d8cf4c73396e9d0da0e7e5a5767c72ef Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 23:03:37 2026 -0600 fix(#2221): stage the vcpkg PCRE2 runtime beside the build output The x64-windows triplet links dynamically, so engine.dll imports pcre2-8.dll (pcre2-8d.dll in Debug). #2212 pointed the include and library paths at vcpkg_installed but nothing copies the runtime next to the output, and the documented setup does not turn on vcpkg's MSBuild integration. A clean checkout therefore linked cleanly and then failed at LoadLibrary with ERROR_MOD_NOT_FOUND. Verified both directions with a LoadLibraryEx probe against bin_release and bin_debug: error 126 with the DLL absent, success with it staged, in both configurations. Missing runtime is an error rather than a skipped copy. If the link found the import library the DLL is there too, so a miss means the vcpkg tree is wrong -- and staying quiet reproduces exactly the silent-unloadable-output failure this fixes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> commit e4ff0b9994ebbeba51775a1c4c53adb27eb54efe Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 23:01:22 2026 -0600 fix(#2222): clear companion checksums when their artifacts are cleaned The blobs archive and APPLY.txt are both conditional on there being changed binary blobs, and dounix.sh removes them up front so a rerun that finds none leaves neither behind. It did not remove their .sha256 files, so those survived from the previous run. A release operator staging by glob then signs and uploads checksums for artifacts that no longer exist. #2214 added the APPLY.txt checksum, which widened an existing gap on the blobs checksum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> commit a54d379f094c09eb3e4d4599f10ca90d795bffc3 Merge: ce21a572 b05539f8 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 20:41:42 2026 -0600 Merge pull request #2220 from brazilofmux/fix/2212-pcre2-vcpkg fix(#2212): PCRE2 comes from vcpkg, like grpc commit b05539f86a276c39df63c4a4b15a594aa75e495d Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 20:39:31 2026 -0600 fix(#2212): PCRE2 comes from vcpkg, like grpc The Windows build took PCRE2 from a hand-built out-of-tree directory reached through $(Pcre2Dir). engine.vcxproj honoured that property for its headers but hardcoded ..\..\src\pcre2\{Release,Debug} for its libraries, so -p:Pcre2Dir= applied to one half and not the other and the link failed with LNK1104 in any tree where PCRE2 was not at the default. Declare pcre2 in mux/vcpkg.json instead, alongside grpc and nlohmann-json, and point netmux, libmux and engine at $(VcpkgDir). All three now read include and library paths from the same place. Note vcpkg's layout is lib/ for release and debug/lib/ for debug, not Release/ and Debug/. PCRE2's JIT is required -- funceval2.cpp calls pcre2_jit_compile -- and comes automatically: the port's platform-default-features pulls in jit everywhere except emscripten and iOS. Ship mux/vcpkg.json in both source TOCs. It was in neither, so the source distribution carried no statement of its own dependencies; with PCRE2 moving to vcpkg, the file that declares it has to travel with the source or an unpacking reader is worse off than before. Unix is unaffected -- configure finds system PCRE2 via pkg-config. Verified on hatsuhara: Release and Debug both build clean with no hand-built PCRE2 present anywhere on the box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> commit ce21a572c38a54db22f795138fe9cd1a30b3ff26 Merge: e97992b1 73dc1749 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 20:05:28 2026 -0600 Merge pull request #2219 from brazilofmux/fix/2214-dounix-apply-sha256 fix(release): generate .sha256 for APPLY.txt (#2214) commit 73dc1749d06a5f180eba5cb768e6985eb0bfff44 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 19:54:50 2026 -0600 fix(release): generate .sha256 for APPLY.txt (#2214) dounix.sh already produced APPLY.txt when companion blobs ship, and every release cut has been checksumming it by hand. Add the missing sha256sum step (same blob-gate as BLOBS_FILE) and reword the closing reminder so checksums and signatures cover the same artifact set. commit e97992b1d7951470ea18b7487ad3be49bae53158 Merge: 204e5031 e3159941 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 19:18:23 2026 -0600 Merge pull request #2218 from brazilofmux/fix/timefmt-invalid-secs fix(softcode): timefmt rejects unparseable seconds argument commit e315994177279a9e517d4ffc629fcc567518b2a3 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Mon Aug 10 19:18:00 2026 -0600 fix(softcode): timefmt rejects unparseable seconds argument SetSecondsString was ignored on failure, so a default-constructed CLinearTimeAbsolute (FILETIME zero / 1601-01-01 UTC) was formatted. In western zones that looked like "Sunday, December 31, 1600, 05:00 PM" — e.g. timefmt(..., #-1 INVALID DATE) after a failed convtime(). Return #-1 INVALID DATE like convsecs/convtime on bad input. Smoke TC004 pins the case. commit 204e50319900dc852008e98ff2b06ba0c0b3eeae Merge: 02c4a6ac 4e94bb75 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Fri Aug 7 22:12:10 2026 -0600 Merge pull request #2217 from brazilofmux/docs/2215-readme-direct-websocket docs: browser client connects directly to netmux — correct #2215's README commit 4e94bb75aa6a6b72d91ee9c4f062d4061fa6db57 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Fri Aug 7 22:12:06 2026 -0600 docs: the browser client connects DIRECTLY to netmux — correct #2215's README The README merged in #2216 claimed browsers cannot reach netmux and that both transports require Hydra. Wrong on the central point: netmux serves WebSocket natively on its ordinary game ports (mux/src/websocket.cpp, RFC 6455) with first-byte protocol detection sharing each port between telnet and WebSocket (#1074/#2193, proto_detect_window), and the handshake accepts both /wsclient and / — which is exactly the path js/connection.js dials. The minimal browser deployment is netmux plus static files, no extra process; Hydra is the OPTIONAL layer for session resume, multi-game links, stored credentials, and gRPC-Web. The error came from concluding absence out of a truncated grep: the file listing was piped through head and mux/proxy's matches filled the window before mux/src/websocket.cpp appeared. A truncated listing is not a complete listing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> commit 02c4a6acca5a9ccc0501c272063355d963887fb0 Merge: ad4c8d82 6e6dc6d6 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Fri Aug 7 22:06:04 2026 -0600 Merge pull request #2216 from brazilofmux/docs/2215-web-client-readme docs(#2215): README for the browser client commit 6e6dc6d6111bebd50473d287b6bc2da17e03fb26 Author: Stephen Dennis <brazilofmux@gmail.com> Date: Fri Aug 7 22:05:50 2026 -0600 docs(#2215): README for the browser client client/web was a complete HTML5 client with nothing self-describing in it — and players have started asking publicly whether TinyMUX has browser support, with no page to point at. Covers what it is, why both transports go through Hydra (browsers cannot open raw TCP), which transport is proxy-agnostic, deployment (static hosting + hydra.conf, the mixed-content and cors_origin rules), the test_web.js harness and its keep-modules-dependency-free convention, and the 2.13-vs-2.14 status. Facts checked against the sources: transport split per connection.js/hydra_connection.js (including the /h-command list and gRPC-Web fallback), listener types and CORS default from hydra.conf.example, hydra's standalone make from mux/proxy/Makefile, localStorage persistence from settings.js, and `node test_web.js` run green before writing (all PASS). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
