The repeat-first-message bug - handoff, 2026-09-01¶
Status: FIXED AND VERIFIED LIVE, 2026-09-01 22:10. The root cause was the CLIENT's painter - paintChat handing paintRow a computed-base subarray of the 23KB cache, which painted line 0's bytes on every row while the wire, the server, the history ring and the cache itself were all correct the whole time. See "ROOT CAUSE FOUND" below for the evidence chain and the fix, and the addendum for the test programs built on the way (CHATKT, CHATVT). After the fix: six distinct lines rendered distinctly, the full history replay rendered every stored line with its own text and time, and a two-client session showed ONE join notice and correct text both ways. The temporary five-number instrument is still on the title row pending a decision to remove it, and the fix is deployed on D100 only - D102/D103 still run the broken client.
Addendum, 2026-09-01 evening - the KERNEL PATH IS PROVEN CLEAN¶
SINTRAN-CHAT/CHATKT.PLNC (new, built and run on D100): a loopback test
that opens one port, sends to its own magic number with the SERVER's exact
send sequence (xmpfget / xmpfwri(0,0,...) / xmpsend with 2**XFSEC)
and receives with the CLIENT's exact receive sequence (xmpfrcv / clamp /
xmpfrea(0,0,...) / xmpfrel). Every message a different letter AND a
different length. 49/49 PASS, including the phase that mirrors a server
mid-handling: a received message HELD unreleased while two replies are
built and sent - both replies arrived intact. So:
xmpfget's fresh buffer IS whatxmpfwri(0,0,...)writes into, even with an arrival held current (the guide never says this; now measured);- the client-shape receive returns each message's own bytes, not a stale current message.
The bug is NOT in the kernel semantics of the call shapes the product
uses. Build/run it again: @MODE CHATKT:MODE,, then @CHATKT - one
minute, needs only XMSG up on that machine.
Also that evening: D100 was found HUNG (terminals, Ethernet, OPCOM all
silent, emulator process executing) and was cold-booted with Ronny's
permission. The boot files brought the ladder up. SYSTEM's file table hit
its 256-slot cap mid-build (ATTEMPT TO CREATE TOO MANY FILES from
PROGRAM-FILE) - stale scratch listings CHATL2/3/5/6/8, CHATA3:LIST/BRF
and TMPX:TXT were deleted to free slots. Watch for this on the next build.
ROOT CAUSE FOUND, 2026-09-01 late evening - THE PAINTER, not the data¶
The elimination ran: kernel loopback clean (CHATKT 49/49), server send side clean (CHATVT 48/48), server history ring on disk holds the RIGHT texts with the RIGHT live timestamps (one/two/three/aaaaaaaa, decoded byte by byte from a pulled copy of CHATH1:DATA), bug reproduced on a VIRGIN server after a cold boot AND on a fresh RT-load AND on a second fresh client - so client side, render half. A five-number instrument on the client's title row (kind, readLength, the ARRIVAL's first text byte, cacheCount, the newest CACHE row's first text byte) then said, on a live repeat:
005 015 097 003 097 (with the screen showing "first")
kind kSaid, the arrival's text byte 'a' (the "alpha" just typed), the cache's newest row ALSO 'a' - the wire is right and the cache is right; the PAINT is wrong. paintChat handed paintRow
cache(base : base + lineWide - 1)
a subarray of the 23KB cache with a COMPUTED base - and every row painted line 0's bytes. The same project rule that killed putField (a computed-bounds subarray compiles clean and reads/writes the wrong memory) in different clothes. This paint shape arrived with the no-blanket-clear repaint rework (commit e402a572, 2026-08-31) - exactly when the bug first appeared. Fix: the row is copied into a fixed 77-byte module paintBuf with an explicit indexed loop (an ordinary computed INDEX reads the cache correctly - the instrument itself proved that) and paintRow reads paintBuf; its inner run subarrays are now of that small fixed array, the shape TESTUI has proven.
Note for a future linter rule: computed-bounds subarrays are NOT all broken - whoBuf(0:whoLen-1), roomName(0:len-1) and TESTUI's paintSlot all work. The broken ones so far both had a large multiplied/offset base into a big array (putField's w16, paintChat's slot77 into 23100 bytes). Do not encode a lint rule until the discriminating condition is measured.
SINTRAN-CHAT/CHATVT.PLNC (new) is the CHATSV-side test this handoff
asked for: EXPORTs were added to CHATSV.PLNC (block right after MODULE,
before the includes), and CHATVT plants three fake members whose magic is
its own test port, then checks buildFromSlot's outBuf per call (part A),
broadcast's audience by counting arrivals (part B), and three
build+broadcast rounds whose received content must follow the round
(part C). Build: @MODE CHATSV:MODE,, first (the exports), then
@MODE CHATVT:MODE,,, run @CHATVT.
The symptom, precisely¶
A client joins, sends a message, sees it correctly. Every message after that - typed by that client OR by anyone else in the room - renders as a REPEAT of that first message's text, not the new one. Confirmed on two independent setups:
- Solo client: typed three different lines one after another; all three showed as the FIRST line's text.
- Two clients (SYSTEM/TESTER and ronny): SYSTEM sent "yo" (showed correctly), then sent "ghei" - the room showed "yo" again, not "ghei". The OTHER client (ronny) saw three lines of "nick has joined" - a JOIN notice repeating, not chat text. So the repeat is not specific to kSaid; it can repeat whatever the FIRST thing shown was, including a system notice.
That last fact matters: it rules out any theory tied to one specific message KIND. Whatever is wrong repeats "the first thing this client ever rendered", regardless of what kind of message that was.
What is PROVEN correct (do not re-suspect these)¶
Three real, automated, offline tests were written and pass clean on the
live machine - SINTRAN-CHAT/CHATCTST.PLNC, 23/23 checks:
- The cache itself -
addSaid/cacheAdd/slotAt. Three distinctaddSaidcalls with distinct text land in three distinct slots, each holding its own text. Not the cache. paintChat's row-to-line arithmetic - pulled out into a new, tested, exported routinewantForRow(row)in CHAT.PLNC. Given three real cache entries, row 0/1/2 correctly want lines 0/1/2, further rows correctly want nothing. Not the paint math.- The wire-arrival parse path - built three DIFFERENT synthetic kSaid
messages with CHATLIB's own proven
cmEnc, wrote them straight intoinBuf, and calledshowArrived(the REAL entry pointdrainPortcalls) three times in a row. Three distinct cache entries resulted, correctly. NotuiArrivedInner/uiSaid/showArrived.
Run it again with: compile+link CHATCTST.PLNC against CHAT, CHATARR,
CHATLIB, XMP-100-1-B02, INTRF1B, VTMR, VTMDATA, VTMARR,
MON-CALL-1B-A00, PLANC-1BANK-F00 (same libraries as CHATCC:MODE, in that
order) - @CHATCTST:PROG. No XMSG, no server, no second machine, ~1 minute
total.
Three real fixes were made and deployed. None of them was THE bug.¶
All three are correct, deployed, and should stay:
announceWelcome(CHAT.PLNC, exported, called by CHATARR.PLNC) -firstWelcomeused to be an EXPORTed BOOLEAN read back through an IMPORT in CHATARR.PLNC, and that branch measurably fired TWICE for one real join with bothfirstWelcomeandcacheCountreading their INITIAL values the second time - which a stray message would not do. Moved the whole check and the flag it reads into ONE module, called as a ROUTINE (proven reliable elsewhere in this file) instead of shared as DATA across the module boundary. This did not fix the live symptom, but it is real and it removed a genuinely suspicious cross-module data-sharing pattern.readStatus(CHAT.PLNCdrainPort, CHATSV.PLNCchatServer) -xmpfrea's own return status was being overwritten by the very next statement (xmpfrel(...) =: returnStatus) before anything checked it, so a failed read's staleinBufwould be parsed as if it were new. Both files now capture the read's own status and skip the parse if it is notXMOK. Correct defensive coding. Did not fix the live symptom.XMPSENDnotXMPFSND(CHAT.PLNCsendToServer) - this was ALREADY a known, documented, unfixed plan item (5.7) from before this session:XMPFSNDsends whatever message happens to be "port current" or "task current" on the XMSG kernel side, not necessarily the one just built. This looked like a very strong match for "always resends the first thing" and was fixed toxmpsend(0, ident, myPort, serverMagic), matching the signature CHATSV.PLNC already uses correctly. Deployed and tested live - the symptom is unchanged. This was the most promising lead of the session and it was not it, or not all of it.
What is still unproven / not yet checked¶
- CHATSV.PLNC's own send/broadcast path was NOT independently offline
tested the way the client's cache/paint/parse were.
buildFromSlotandbroadcastwere read carefully and look correct, andsendTowas already fixed to useXMPSENDin an earlier session - but nothing exercised them with synthetic distinct inputs the way CHATCTST.PLNC exercises the client. ACHATCTST-style test that callsbuildFromSlot+broadcast(or a narrower slice of them) with three distinct fake members/messages and inspects the resultingoutBufper call would close this gap the same way the client side was closed. - The fact that a JOIN notice repeated on the OTHER client (ronny saw three
"nick has joined") was reported in the interrupted final message and NOT
yet investigated. It is the single most useful new fact: it means whatever
is wrong is not specific to
kSay/kSaidat all, and it happened on the RECEIVING side of a DIFFERENT client than the one sending. That points either atbroadcast()sending the same built message to the wrong seats repeatedly, or at something inCHATSV.PLNC's SEND path (not receive) that was not part of this session'sreadStatus/XMPSENDfixes. CHATSV:PROGwas rebuilt with thereadStatusfix and RT-loaded onto segment 204B (rt-load.ps1 -Port 9010 -Segment 204 -AndStart) earlier in this session, confirmed alive withFJELLname and both trunks. It has NOT been rebuilt again since - it does not yet carry any fix beyondreadStatus. Client-sideCHAT:PROGDOES carry theXMPSENDfix (confirmed viaFILE-STATISTICS, linked 22:35, read 22:37, matching the failed test).- Never confirmed: does the bug reproduce with only ONE client ever having existed in the room's whole history (a truly virgin server, seat 0 taken exactly once, ever)? Every live test this session ran against a server that had already seen several joins/rebuilds/orphaned clients in its lifetime. A clean RT-load immediately followed by exactly one join and one send, with nothing else touching the server first, has not been tried.
Tooling / environment notes for whoever continues this¶
- The sync daemon's window is 6 hours (21600s) and it does not restart
itself. It died silently partway through this session (
start-relay.ps1again) - checksync-relay.logforstill waiting for 100before assuming a push failed for a code reason. - A stuck link needs the wake trick:
CREATE-FILE WAKE:TEXT,1thenCOPY-FILE WAKE:TEXT,D19999(SYSTEM).WAKE:TEXTon the machine - the copy FAILS (NO SUCH FILE NAME) and that is fine, the point is the frame it sends. CHAT:LISTandCHATSV:LISTrepeatedly hitFILE ALREADY OPENED FOR WRITE BY ANOTHER USERandAMBIGUOUS FILE NAMEacross this session, for reasons never identified (not this repo's own daemon or admin session, as far as could be told). The workaround used throughout: compile to a never-used listing name each time (CHATL3:LIST,CHATL4:LIST,CHATL5:LIST...CHATL8:LISTwere used, next isCHATL9:LISTor start numbering fresh) - the OBJECT name (thirdCOMPILEargument) staysCHAT/CHATSVregardless, so this is always safe and never affects what gets linked.- CHATARR.PLNC must be compiled in its OWN, separate
PLANC-100-F00session from CHAT.PLNC (EXITand re-enter the compiler between them) - compiling both in one session has previously produced a SPARSECHATARR:LISTthat looks fine locally but is corrupt on the machine. This bit this session once; the isolated-session compile is the fix and was used throughout after that. SINTRAN-CHAT/CHATCTST.PLNCis a permanent, valuable test file and should stay in the repo (it currently sits alongsideCHATTST.PLNC, which tests the codec;CHATCTST.PLNCtests the cache/paint/arrival-parse). It is not committed to git yet as of this handoff.SINTRAN-CHAT/CHATXMS.PLNC(from a session before this one) still exists, still compiles clean, and is still not linked into anything. Not touched this session. Unrelated to this bug as far as is known.
Recommended next step¶
Write the CHATSV-side equivalent of CHATCTST.PLNC - a small offline test
that builds two or three distinct synthetic "members" with distinct names/
rooms in the member table, calls buildFromSlot/broadcast for each, and
inspects outBuf after each call for distinct content, the way the client
side was proven. If THAT fails, the bug is server-side send, not client-side
receive, and has never actually been tested at all this session. If it
passes too, the next thing worth trying is the "never confirmed" scenario
above - a genuinely virgin server, exactly one join, exactly one send,
nothing else touching it first - since every live test this session ran
against a server with prior history.