diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 20:37:54 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 20:37:54 +0200 |
| commit | 4d717018ea8a748b8bb16ad71c3deab6d8c20545 (patch) | |
| tree | dc4996d6904d2f89120b07cfa2c59c0fb41c8e75 /modules/core/core-bundle.js | |
| parent | e34c1203500451e65f0439435df55a2fa4c0f4ab (diff) | |
fix(chat): GLM review batch — corruption, traversal, cursor truncation, double print (v4.6.78-80)
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.
Diffstat (limited to 'modules/core/core-bundle.js')
| -rw-r--r-- | modules/core/core-bundle.js | 21 |
1 files changed, 14 insertions, 7 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 90e2593..73f5ab3 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -20751,6 +20751,7 @@ class DebugSupervisorModule { const err = new Error((j && j.error) || ('HTTP ' + resp.status)); err.status = resp.status; err.code = (j && j.code) || ''; + err.retryAfterMs = (j && j.retry_after_ms) || null; throw err; } const st = { token: token, cursor: j.epoch || 0, thread: j.thread || '' }; @@ -20758,7 +20759,7 @@ class DebugSupervisorModule { return j; } - _chatPoll(force) { + _chatPoll(force, silent) { const st = this._chatThread(); const token = st.token; if (!token) return Promise.resolve(null); @@ -20779,7 +20780,7 @@ class DebugSupervisorModule { if ((m.epoch || 0) > (this._chatThread().cursor || 0)) { this._chatSaveState({ token: token, cursor: m.epoch || 0, thread: j.thread || '' }); } - if (m.author === 'hermes') this.terminal.print('🤖 ' + m.text, ''); + if (m.author === 'hermes' && !silent) this.terminal.print('🤖 ' + m.text, ''); }); return j; }) @@ -21578,7 +21579,7 @@ class DebugSupervisorModule { _cmdFeedback(args) { const msg = (Array.isArray(args) ? args.join(' ') : String(args || '')).trim(); if (!msg) { - this.terminal.print('usage: feedback <message> (alias: chat <message>)', 'error'); + this.terminal.print('usage: hermes <message> (alias: feedback <message>)', 'error'); return; } this.terminal.print('📨 Sending…', ''); @@ -21588,9 +21589,10 @@ class DebugSupervisorModule { ' — reply arrives here; `hermes-poll` to check now', 'success')) .catch(e => { // 503 is nginx's limit_req, not ours: it arrives as HTML, so status is all we have. - const hint = e.status === 429 ? ' (rate limited — retry later)' + const wait = e.retryAfterMs ? ' in ' + Math.ceil(e.retryAfterMs / 1000) + 's' : ' later'; + const hint = e.status === 429 ? ' (rate limited — retry' + wait + ')' : e.status === 503 ? ' (host busy — retry)' - : e.status === 401 ? ' (bad token — try chat-reset)' + : e.status === 401 ? ' (bad token — try hermes-reset)' : ''; this.terminal.print('❌ ' + e.message + hint, 'error'); }); @@ -21598,10 +21600,15 @@ class DebugSupervisorModule { _cmdFeedbackInbox() { this.terminal.print('📬 Checking…', ''); + // _chatPoll prints the 🤖 lines (once); this loop skips hermes so it is not shown + // twice. Making the poll silent instead would print the reply nowhere. this._chatPoll(true).then(j => { if (!j) { this.terminal.print('❌ no answer from the chat endpoint', 'error'); return; } - const msgs = j.messages || []; - if (!msgs.length) this.terminal.print('(no new messages)', 'info'); + const incoming = j.messages || []; + // hermes lines were already printed by the poll above; only count them as + // 'new' so the box does not say "no new messages" right under a new one + const msgs = incoming.filter(m => m.author !== 'hermes'); + if (!incoming.length) this.terminal.print('(no new messages)', 'info'); msgs.forEach(m => this.terminal.print( (m.author === 'hermes' ? '🤖 ' : m.author === 'agent' ? '🧠 ' : '👤 ') + m.text, '')); if (j.awaiting) this.terminal.print('⏳ waiting for Hermes', 'info'); |