aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-debug-supervisor.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 20:37:54 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 20:37:54 +0200
commit577c9241a4ac1c55c5ed0031b76a8a1f83798752 (patch)
treeaa544a86e82c1a6fd4891d50aa3adc13a7890892 /modules/core/module-debug-supervisor.js
parentdfef70b61b98382a1f78fc6d970fb24ac86c4e36 (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/module-debug-supervisor.js')
-rw-r--r--modules/core/module-debug-supervisor.js21
1 files changed, 14 insertions, 7 deletions
diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js
index 79c9acc..25e0566 100644
--- a/modules/core/module-debug-supervisor.js
+++ b/modules/core/module-debug-supervisor.js
@@ -134,6 +134,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 || '' };
@@ -141,7 +142,7 @@ class DebugSupervisorModule {
return j;
}
- _chatPoll(force) {
+ _chatPoll(force, silent) {
const st = this._chatThread();
const token = st.token;
if (!token) return Promise.resolve(null);
@@ -162,7 +163,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;
})
@@ -961,7 +962,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…', '');
@@ -971,9 +972,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');
});
@@ -981,10 +983,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');