aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-debug-supervisor.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 19:36:52 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 19:36:52 +0200
commitd0a6c805a142b6a637913a6af876a08e72bcb1f7 (patch)
tree05d9965d3c7086a707133bb5182a44e53c613102 /modules/core/module-debug-supervisor.js
parent313f13955ad9c72895b0914ac828c61b3e469715 (diff)
feat(newos4): hermes command, auto-poll, chat_hermes tool, token reset (v4.6.73)
Phase C as repo edits (browser edits in _mods/ do not survive a reload). - `hermes <msg>`: sends through the chat endpoint and always prints an outcome β€” 'πŸ“¨ Sending…' then either 'βœ… sent id=… epoch=…' or a coded error with a hint (429 retry later, 503 host busy, 401 run hermes-reset). `feedback` stays as the alias. - NOT named `chat`: ChatStudioModule already registers `chat` and dispatch walks modules in order, so a second `chat` was silently unreachable. Found by test, not by reading. - `hermes-poll` / `feedback-inbox`, `hermes-reset` (token rotation). - Poller: 30 s, paused while the tab is hidden, polls on visibilitychange, first poll adopts the cursor and prints nothing, cursor persisted in webx11_chat_state. - `chat_hermes` tool: send + read from inside the agent loop. - Contract/help updated for the rename. Acceptance: 'hermes acceptance test' in the browser, reply written host-side with scripts/chat.py, appeared in the terminal by itself 30 s later with no command typed (cursor 1792776973800).
Diffstat (limited to 'modules/core/module-debug-supervisor.js')
-rw-r--r--modules/core/module-debug-supervisor.js156
1 files changed, 130 insertions, 26 deletions
diff --git a/modules/core/module-debug-supervisor.js b/modules/core/module-debug-supervisor.js
index 78e3a46..6768df1 100644
--- a/modules/core/module-debug-supervisor.js
+++ b/modules/core/module-debug-supervisor.js
@@ -55,8 +55,14 @@ class DebugSupervisorModule {
this.commands['debug-health'] = () => this._cmdHealth();
this.commands['debug-sync'] = (args) => this._cmdSync(args);
this.commands['debug-sync-url'] = (args) => this._cmdSyncUrl(args);
+ // v4.6.71: one channel. `feedback` keeps working; `hermes` is the explicit name.
+ // NOT `chat` β€” ChatStudioModule already owns that name and dispatch iterates
+ // modules in order, so a second `chat` is silently unreachable.
this.commands['feedback'] = (args) => this._cmdFeedback(args);
+ this.commands['hermes'] = (args) => this._cmdFeedback(args);
this.commands['feedback-inbox'] = () => this._cmdFeedbackInbox();
+ this.commands['hermes-poll'] = () => this._cmdFeedbackInbox();
+ this.commands['hermes-reset'] = () => this._cmdChatReset();
}
seed() {
@@ -73,6 +79,104 @@ class DebugSupervisorModule {
this._syncInterval = setInterval(() => this._syncToBackend(), 300000);
// Also sync on first load after a short delay
setTimeout(() => this._syncToBackend(), 5000);
+ // v4.6.71: replies land by themselves. 30 s, paused while the tab is hidden,
+ // and the first poll only records the cursor β€” a reload must not replay history.
+ this._chatTimer = setInterval(() => this._chatPoll(), 30000);
+ document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState === 'visible') this._chatPoll();
+ });
+ setTimeout(() => this._chatPoll(), 4000);
+ }
+
+ // ── chat bridge (contract: _docs/chat-contract.md) ──────────────────────
+
+ _chatToken() {
+ try {
+ let t = localStorage.getItem('webx11_chat_token');
+ if (!t || !/^[a-f0-9]{64}$/.test(t)) {
+ const b = new Uint8Array(32);
+ (window.crypto || window.msCrypto).getRandomValues(b);
+ t = Array.from(b).map(x => x.toString(16).padStart(2, '0')).join('');
+ localStorage.setItem('webx11_chat_token', t);
+ }
+ return t;
+ } catch (e) { return ''; }
+ }
+
+ _chatState() {
+ try { return JSON.parse(localStorage.getItem('webx11_chat_state') || '{}') || {}; }
+ catch (e) { return {}; }
+ }
+
+ _chatSaveState(st) {
+ try { localStorage.setItem('webx11_chat_state', JSON.stringify(st)); } catch (e) {}
+ }
+
+ _chatThread() {
+ // sha256(token)[0:16] is computed server-side; the client only needs the
+ // cursor, which is scoped to the token it was issued for.
+ const st = this._chatState();
+ const tok = this._chatToken();
+ return st.token === tok ? st : { token: tok, cursor: 0, thread: '' };
+ }
+
+ async _chatPost(text, kind) {
+ const token = this._chatToken();
+ if (!token) throw new Error('localStorage is blocked β€” no chat token');
+ const resp = await fetch('/spaces/newos4/api/chat/post.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-Chat-Token': token },
+ body: JSON.stringify({ text: text, kind: kind || 'operator' })
+ });
+ let j = null;
+ try { j = await resp.json(); } catch (e) { j = null; }
+ if (!resp.ok || !j || !j.success) {
+ const err = new Error((j && j.error) || ('HTTP ' + resp.status));
+ err.status = resp.status;
+ err.code = (j && j.code) || '';
+ throw err;
+ }
+ const st = { token: token, cursor: j.epoch || 0, thread: j.thread || '' };
+ this._chatSaveState(st);
+ return j;
+ }
+
+ _chatPoll(force) {
+ const st = this._chatThread();
+ const token = st.token;
+ if (!token) return Promise.resolve(null);
+ if (!force && document.visibilityState === 'hidden') return Promise.resolve(null);
+ const q = st.cursor ? '?since=' + st.cursor : '';
+ return fetch('/spaces/newos4/api/chat/poll.php' + q, { headers: { 'X-Chat-Token': token } })
+ .then(r => r.ok ? r.json() : null)
+ .then(j => {
+ if (!j || !j.success) return null;
+ const known = st.cursor > 0;
+ const msgs = j.messages || [];
+ if (!known) {
+ // first poll for this token: adopt the cursor, print nothing
+ this._chatSaveState({ token: token, cursor: j.cursor || 0, thread: j.thread || '' });
+ return j;
+ }
+ msgs.forEach(m => {
+ 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, '');
+ });
+ return j;
+ })
+ .catch(() => null);
+ }
+
+ _cmdChatReset() {
+ try {
+ localStorage.removeItem('webx11_chat_token');
+ localStorage.removeItem('webx11_chat_state');
+ this.terminal.print('πŸ”„ Chat token rotated β€” a new thread starts with your next message', 'success');
+ } catch (e) {
+ this.terminal.print('❌ ' + e.message, 'error');
+ }
}
destroy() {
@@ -852,35 +956,35 @@ class DebugSupervisorModule {
_cmdFeedback(args) {
const msg = (Array.isArray(args) ? args.join(' ') : String(args || '')).trim();
- if (!msg) { this.terminal.print('usage: feedback <message>', 'error'); return; }
- const payload = {
- title: msg.slice(0, 120),
- body: msg.slice(0, 4000),
- thread: this._feedbackThread(),
- kind: 'maintenance',
- diagnostics: { version: window.NEWOS4_VERSION || '?', url: location.href, auto: false },
- };
- this.terminal.print('πŸ“¨ Sending feedback…', '');
- fetch('/spaces/newos4/api/debug/report.php', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload),
- }).then(r => r.json()).then(j => {
- if (j && j.success) this.terminal.print('βœ… Sent: ' + (j.id || 'ok') + (j.duplicate ? ' (merged)' : ''), 'success');
- else this.terminal.print('❌ ' + ((j && j.error) || 'send failed'), 'error');
- }).catch(e => this.terminal.print('❌ ' + e.message, 'error'));
+ if (!msg) {
+ this.terminal.print('usage: feedback <message> (alias: chat <message>)', 'error');
+ return;
+ }
+ this.terminal.print('πŸ“¨ Sending…', '');
+ this._chatPost(msg, 'operator')
+ .then(j => this.terminal.print(
+ 'βœ… sent id=' + String(j.id || '?').slice(-6) + ' epoch=' + (j.epoch || 0) +
+ ' β€” 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)'
+ : e.status === 503 ? ' (host busy β€” retry)'
+ : e.status === 401 ? ' (bad token β€” try chat-reset)'
+ : '';
+ this.terminal.print('❌ ' + e.message + hint, 'error');
+ });
}
_cmdFeedbackInbox() {
- this.terminal.print('πŸ“¬ Fetching replies…', '');
- fetch('/spaces/newos4/api/debug/agent-replies.php', { credentials: 'same-origin' })
- .then(r => r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status + ' (admin session required)')))
- .then(j => {
- const list = (j && j.replies) || [];
- if (!list.length) { this.terminal.print('(no replies yet)', ''); return; }
- list.slice(-10).forEach(r => this.terminal.print('πŸ€– ' + (r.created_at || '') + ' ' + r.body, ''));
- })
- .catch(e => this.terminal.print('❌ ' + e.message, 'error'));
+ this.terminal.print('πŸ“¬ Checking…', '');
+ 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');
+ msgs.forEach(m => this.terminal.print(
+ (m.author === 'hermes' ? 'πŸ€– ' : m.author === 'agent' ? '🧠 ' : 'πŸ‘€ ') + m.text, ''));
+ if (j.awaiting) this.terminal.print('⏳ waiting for Hermes', 'info');
+ });
}
_cmdSync(args) {