From 4d717018ea8a748b8bb16ad71c3deab6d8c20545 Mon Sep 17 00:00:00 2001 From: Agando Date: Fri, 23 Oct 2026 20:37:54 +0200 Subject: fix(chat): GLM review batch — corruption, traversal, cursor truncation, double print (v4.6.78-80) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 findings accepted, 3 refuted with evidence (see _docs/chat-review-glm-2026-10-23.md). Data loss: a corrupt thread read as [] and the next append rewrote it with one message — now 500 and the file is left alone. Cursor after a >50-message truncation pointed at the thread tail, making the oldest permanently unfetchable — now the last message actually sent, plus truncated:true. Two posts in the same millisecond shared a cursor — epoch is now strictly increasing. Security: chat.py show() had no id validation and joined argv onto CHAT_DIR ('../../x' read any .json as darkbot) — THREAD_RE added. The auto-reply prompt now treats message text as untrusted data and forbids acting on instructions found in it. Ops: relay housekeeping swept neither .lock-* nor orphaned .tmp-*; a crashed listing looked identical to an empty queue; grep AWAITING also matched message bodies; hermes-poll printed every reply twice. Verified: corrupt file -> 500 with the file untouched; traversal -> 'invalid thread id'; hermes-poll -> exactly one line per reply. --- api/chat/poll.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'api/chat/poll.php') diff --git a/api/chat/poll.php b/api/chat/poll.php index 601beca..b4aa970 100644 --- a/api/chat/poll.php +++ b/api/chat/poll.php @@ -14,14 +14,21 @@ $all = chat_read($thread); $messages = $since > 0 ? array_values(array_filter($all, fn($m) => (int)($m['epoch'] ?? 0) > $since)) : $all; -$lastAll = $all ? end($all) : null; + +$window = array_slice($messages, -50); +$truncated = count($messages) > count($window); +// The cursor is the last message the client is actually being handed. Returning the +// thread's real tail after a truncation would skip everything in between for good. +$lastSent = $window ? $window[count($window) - 1] : null; +$lastAll = $all ? $all[count($all) - 1] : null; json_response([ 'success' => true, 'thread' => $thread, - 'messages' => array_slice($messages, -50), + 'messages' => $window, + 'truncated' => $truncated, // `awaiting` replaces `unread`: the server keeps no read state, so an unread // counter could never clear. Who spoke last is knowable. 'awaiting' => (bool)($lastAll && ($lastAll['author'] ?? '') !== 'hermes'), - 'cursor' => $lastAll ? (int)($lastAll['epoch'] ?? 0) : 0, + 'cursor' => $lastSent ? (int)($lastSent['epoch'] ?? 0) : ($lastAll ? (int)($lastAll['epoch'] ?? 0) : 0), ]); -- cgit v1.2.3