diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-23 19:36:52 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-23 19:36:52 +0200 |
| commit | d0a6c805a142b6a637913a6af876a08e72bcb1f7 (patch) | |
| tree | 05d9965d3c7086a707133bb5182a44e53c613102 | |
| parent | 313f13955ad9c72895b0914ac828c61b3e469715 (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).
| -rw-r--r-- | _docs/chat-contract.md | 8 | ||||
| -rw-r--r-- | help.html | 22 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 205 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 49 | ||||
| -rw-r--r-- | modules/core/module-debug-supervisor.js | 156 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 |
7 files changed, 390 insertions, 58 deletions
diff --git a/_docs/chat-contract.md b/_docs/chat-contract.md index ee2d006..6f94d79 100644 --- a/_docs/chat-contract.md +++ b/_docs/chat-contract.md @@ -83,8 +83,12 @@ relay cron. A conversation is not an archive. ## What this replaces -`feedback <msg>` and `chat <msg>` are the **same code path**; `feedback` is a deprecated alias. -Neither writes to the bug-report intake any more. `chat-poll` = `feedback-inbox`. +`feedback <msg>` and `hermes <msg>` are the **same code path**; `feedback` is the legacy alias, +`hermes` the explicit one. Neither writes to the bug-report intake any more. + +The command is **not** called `chat`: `ChatStudioModule` already registers `chat` (it toggles the +chat overlay) and command dispatch walks modules in order, so a second `chat` is unreachable. +`hermes-poll` = `feedback-inbox`; `hermes-reset` rotates the token. `POST /api/debug/agent-replies.php` is retired in favour of this contract: after the acceptance round trip it answers `410 Gone` with a pointer to `/api/chat/post.php`. One write path, one truth. @@ -1689,6 +1689,28 @@ listmods</pre> <h2>π Changelog</h2> <div class="version-item"> + <div class="v-header"><span class="v-num">v4.6.71 β October 2026</span> + <span class="v-date">chat bridge</span></div> + <ul> + <li><strong>Chat with Hermes from the terminal.</strong> <code>hermes <message></code> + (alias: <code>feedback</code>) sends to the operator channel and prints the message id. + Replies appear by themselves within 30 seconds; <code>hermes-poll</code> checks immediately.</li> + <li><strong>Contract first.</strong> Shapes, error codes and the millisecond cursor live in + <code>_docs/chat-contract.md</code>. A wrong token is <code>401</code>, oversize text is + <code>413</code> (rejected, never trimmed), rate limits are <code>429</code> with + <code>retry_after_ms</code>.</li> + <li><strong>One write path.</strong> <code>feedback</code> no longer files a bug report; + the old replies endpoint answers <code>410 Gone</code>. <code>hermes-reset</code> rotates + the browser's token and starts a fresh thread.</li> + <li><strong>The in-page agent can ask too.</strong> New tool <code>chat_hermes</code> β + send a question, read the answer on a later step.</li> + <li><strong>Server side.</strong> Per-thread and per-IP rate windows, atomic writes, + <code>data/chat/</code> unreachable from the web. Host CLI: + <code>scripts/chat.py list|show|reply</code>.</li> + </ul> + </div> + + <div class="version-item"> <div class="v-header"><span class="v-num">v4.6.57β68 β October 2026</span><span class="v-date">2026-10-23</span></div> <ul> <li><strong>No shell β and it now says so</strong> β <code>;</code>, <code>&&</code> and <code>||</code> are refused with an explicit message instead of being folded into the command's argv (where <code>wc -l f && grep -c x f</code> returned a plausible wrong number). A <code>|</code> arriving from the AI <code>run_command</code> tool is refused too, because that path bypasses the pipe splitter and degraded the pattern to empty. Documented at the top of the command reference.</li> @@ -1349,13 +1349,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.70"></script> +<script src="modules/core/core-bundle.js?v=v4.6.73"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.70"></script> +<script src="modules/core/module-loader.js?v=v4.6.73"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.70"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.73"></script> <script> // βββ ONE TRUE VERSION SOURCE βββ diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 303c151..a689774 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -8820,6 +8820,20 @@ buildSystemPrompt() { }, { type: 'function', function: { + name: 'chat_hermes', + description: 'Send a message to Hermes, the platform operator agent, and read replies. Use when a task needs a decision only the operator can make: policy, a deploy approval, scope. Replies are asynchronous β call with action "read" to check for an answer.', + parameters: { + type: 'object', + properties: { + action: { type: 'string', enum: ['send', 'read'], description: 'send a message, or read replies' }, + text: { type: 'string', description: 'message body, required for send' } + }, + required: ['action'] + } + } + }, { + type: 'function', + function: { name: 'spawn_agent', description: '(DESTRUCTIVE) Spawn a new worker agent with a role and capabilities', parameters: { @@ -11063,6 +11077,41 @@ buildSystemPrompt() { } return JSON.stringify({ agents: results, count: results.length }); } + case 'chat_hermes': { + // v4.6.71: one channel, one store. Contract: _docs/chat-contract.md + const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; + if (!ds || typeof ds._chatPost !== 'function') { + return JSON.stringify({ error: 'chat bridge unavailable' }); + } + try { + if ((a.action || 'read') === 'send') { + const text = String(a.text || '').trim(); + if (!text) return JSON.stringify({ error: 'text is required for send' }); + const j = await ds._chatPost(text, 'agent'); + return JSON.stringify({ + sent: true, id: j.id, epoch: j.epoch, + note: 'asynchronous β call chat_hermes with action read for the answer' + }); + } + const st = ds._chatThread(); + const resp = await fetch( + '/spaces/newos4/api/chat/poll.php' + (st.cursor ? '?since=' + st.cursor : ''), + { headers: { 'X-Chat-Token': st.token } }); + const j = await resp.json().catch(() => null); + if (!resp.ok || !j || !j.success) { + return JSON.stringify({ error: (j && j.error) || ('HTTP ' + resp.status) }); + } + if (j.cursor) ds._chatSaveState({ token: st.token, cursor: j.cursor, thread: j.thread || '' }); + return JSON.stringify({ + awaiting: j.awaiting, + messages: (j.messages || []).slice(-10).map(m => ({ + author: m.author, ts: m.ts, text: m.text + })) + }); + } catch (e) { + return JSON.stringify({ error: e.message }); + } + } case 'delegate_task': { const task = a.task || ''; if (!task) return JSON.stringify({ error: 'No task provided' }); @@ -20612,8 +20661,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() { @@ -20630,6 +20685,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() { @@ -21409,35 +21562,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) { diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index f5895f3..ce650b9 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -433,6 +433,20 @@ buildSystemPrompt() { }, { type: 'function', function: { + name: 'chat_hermes', + description: 'Send a message to Hermes, the platform operator agent, and read replies. Use when a task needs a decision only the operator can make: policy, a deploy approval, scope. Replies are asynchronous β call with action "read" to check for an answer.', + parameters: { + type: 'object', + properties: { + action: { type: 'string', enum: ['send', 'read'], description: 'send a message, or read replies' }, + text: { type: 'string', description: 'message body, required for send' } + }, + required: ['action'] + } + } + }, { + type: 'function', + function: { name: 'spawn_agent', description: '(DESTRUCTIVE) Spawn a new worker agent with a role and capabilities', parameters: { @@ -2676,6 +2690,41 @@ buildSystemPrompt() { } return JSON.stringify({ agents: results, count: results.length }); } + case 'chat_hermes': { + // v4.6.71: one channel, one store. Contract: _docs/chat-contract.md + const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; + if (!ds || typeof ds._chatPost !== 'function') { + return JSON.stringify({ error: 'chat bridge unavailable' }); + } + try { + if ((a.action || 'read') === 'send') { + const text = String(a.text || '').trim(); + if (!text) return JSON.stringify({ error: 'text is required for send' }); + const j = await ds._chatPost(text, 'agent'); + return JSON.stringify({ + sent: true, id: j.id, epoch: j.epoch, + note: 'asynchronous β call chat_hermes with action read for the answer' + }); + } + const st = ds._chatThread(); + const resp = await fetch( + '/spaces/newos4/api/chat/poll.php' + (st.cursor ? '?since=' + st.cursor : ''), + { headers: { 'X-Chat-Token': st.token } }); + const j = await resp.json().catch(() => null); + if (!resp.ok || !j || !j.success) { + return JSON.stringify({ error: (j && j.error) || ('HTTP ' + resp.status) }); + } + if (j.cursor) ds._chatSaveState({ token: st.token, cursor: j.cursor, thread: j.thread || '' }); + return JSON.stringify({ + awaiting: j.awaiting, + messages: (j.messages || []).slice(-10).map(m => ({ + author: m.author, ts: m.ts, text: m.text + })) + }); + } catch (e) { + return JSON.stringify({ error: e.message }); + } + } case 'delegate_task': { const task = a.task || ''; if (!task) return JSON.stringify({ error: 'No task provided' }); 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) { diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 634a0c4..291dbd4 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.70'; + const LATEST_VER = 'v4.6.73'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { |