diff options
| -rw-r--r-- | api/debug/agent-replies.php | 59 | ||||
| -rw-r--r-- | help.html | 18 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 9 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-orchestrator.js | 9 |
6 files changed, 55 insertions, 48 deletions
diff --git a/api/debug/agent-replies.php b/api/debug/agent-replies.php index 2d2e2b2..b996a29 100644 --- a/api/debug/agent-replies.php +++ b/api/debug/agent-replies.php @@ -1,45 +1,24 @@ <?php -// api/debug/agent-replies.php — outbound half of the maintenance thread. -// Hermes (or the operator) writes replies here; the in-page agent reads them with -// `feedback-inbox`. nginx gates this path behind the WebX11 admin session, so it is -// owner-only: never expose it in the public debug/course location block. +// api/debug/agent-replies.php — RETIRED. +// +// This was the one-way reply store for the old `feedback` command: a single shared +// array every browser wrote to and only an admin session could read. It was replaced +// by the token-scoped chat channel (api/chat/), where each thread belongs to one +// browser and the writer is `hermes`. +// +// Both verbs now answer 410 Gone with a pointer. Keeping GET alive kept a second read +// path over the same concept — the "one write path, one truth" rule applies to reads +// too, because two answers to "what did Hermes say" drift apart. +declare(strict_types=1); require_once __DIR__ . '/config.php'; -define('REPLIES_DIR', __DIR__ . '/../../data/maintenance'); -define('REPLIES_FILE', REPLIES_DIR . '/replies.json'); - -if (!is_dir(REPLIES_DIR)) { - @mkdir(REPLIES_DIR, 0775, true); - @chgrp(REPLIES_DIR, 'www-data'); -} - -$method = $_SERVER['REQUEST_METHOD']; - -if ($method === 'GET') { - $since = $_GET['since'] ?? null; - $all = load_json(REPLIES_FILE); - if (!is_array($all)) $all = []; - if ($since) { - $all = array_values(array_filter($all, fn($r) => ($r['created_at'] ?? '') > $since)); - } - json_response(['success' => true, 'replies' => array_slice($all, -100)]); -} - -if ($method !== 'POST') { - error_response('Method not allowed', 405); -} - -// v4.6.71: retired. Two write paths meant two truths — messages written here never -// reached the chat thread, so they looked like a conversation nobody answered. -// One write path now: /api/chat/post.php (contract: _docs/chat-contract.md). http_response_code(410); -header('Content-Type: application/json'); +header('Content-Type: application/json; charset=utf-8'); echo json_encode([ - 'success' => false, - 'code' => 'gone', - 'error' => 'POST /api/debug/agent-replies.php is retired', - 'use' => '/spaces/newos4/api/chat/post.php', - 'contract'=> '/spaces/newos4/_docs/chat-contract.md', - 'read' => '/spaces/newos4/api/chat/poll.php', -]); -exit; + 'success' => false, + 'code' => 'gone', + 'error' => 'agent-replies.php is retired — the chat channel replaced it', + 'use' => '/spaces/newos4/api/chat/post.php and /api/chat/poll.php', + 'contract' => '/spaces/newos4/_docs/chat-contract.md', + 'host' => 'python3 /var/www/webx11.duckdns.org/spaces/newos4/scripts/chat.py list', +], JSON_UNESCAPED_SLASHES); @@ -1689,6 +1689,24 @@ listmods</pre> <h2>📋 Changelog</h2> <div class="version-item"> + <div class="v-header"><span class="v-num">v4.6.81 — October 2026</span> + <span class="v-date">two silent failures</span></div> + <ul> + <li><strong>Delegation no longer lies.</strong> <code>outboxResult</code> was declared inside + the branch that needs the AI module but read outside it, so any delegation with the module + missing threw <code>outboxResult is not defined</code> — caught as an agent failure, leaving + the worker idle with no result. It now returns + <code>queued — AI module not loaded, nothing executed</code> instead of a crash or a false + "completed".</li> + <li><strong>Markdown renders.</strong> <code>_docs/*.md</code> was served as + <code>application/octet-stream</code>, so every document opened as a download. Now + <code>text/markdown</code>, with <code>nosniff</code>.</li> + <li><strong>One reply path.</strong> <code>agent-replies.php</code> answers + <code>410 Gone</code> for reads as well as writes, pointing at the chat channel.</li> + </ul> + </div> + + <div class="version-item"> <div class="v-header"><span class="v-num">v4.6.72–80 — October 2026</span> <span class="v-date">hardening after review</span></div> <ul> @@ -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.80"></script> +<script src="modules/core/core-bundle.js?v=v4.6.81"></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.80"></script> +<script src="modules/core/module-loader.js?v=v4.6.81"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.80"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.81"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 73f5ab3..b565365 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -16721,6 +16721,10 @@ class OrchestratorModule { // Single sub-task — fall through to normal path } + // Declared in function scope: the AI branch assigns it, but the return at the end of + // this method runs even when that branch never did. + let outboxResult = null; + // Find an idle agent and immediately stamp it busy (atomic claim) let agent = Object.values(this._agents).find(a => a.status === 'idle'); if (agent) { @@ -16809,7 +16813,7 @@ class OrchestratorModule { this.terminal.print(`✅ ${agent.id} completed task`, 'success'); // Return result for callers that await delegateTask - const outboxResult = typeof result === 'string' ? result : (result?.content || 'completed'); + outboxResult = typeof result === 'string' ? result : (result?.content || 'completed'); // Check restart policy — if always, set back to idle for next task if (agent.restartPolicy === 'always') { @@ -16820,6 +16824,7 @@ class OrchestratorModule { } } else { this.terminal.print(`ℹ️ AI module not available — task queued for ${agent.id}`, 'info'); + outboxResult = 'queued — AI module not loaded, nothing executed'; } } catch (e) { this._handleAgentFailure(agent.id, e.message); @@ -16835,7 +16840,7 @@ class OrchestratorModule { agent.status = 'idle'; agent.lastHeartbeat = Date.now(); } - return outboxResult || 'completed'; + return outboxResult || 'no result recorded'; } // ─── Recurring Scheduling ─── diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index acff509..74e3a86 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.80'; + const LATEST_VER = 'v4.6.81'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-orchestrator.js b/modules/core/module-orchestrator.js index b3b76cd..5e03b0b 100644 --- a/modules/core/module-orchestrator.js +++ b/modules/core/module-orchestrator.js @@ -1153,6 +1153,10 @@ class OrchestratorModule { // Single sub-task — fall through to normal path } + // Declared in function scope: the AI branch assigns it, but the return at the end of + // this method runs even when that branch never did. + let outboxResult = null; + // Find an idle agent and immediately stamp it busy (atomic claim) let agent = Object.values(this._agents).find(a => a.status === 'idle'); if (agent) { @@ -1241,7 +1245,7 @@ class OrchestratorModule { this.terminal.print(`✅ ${agent.id} completed task`, 'success'); // Return result for callers that await delegateTask - const outboxResult = typeof result === 'string' ? result : (result?.content || 'completed'); + outboxResult = typeof result === 'string' ? result : (result?.content || 'completed'); // Check restart policy — if always, set back to idle for next task if (agent.restartPolicy === 'always') { @@ -1252,6 +1256,7 @@ class OrchestratorModule { } } else { this.terminal.print(`ℹ️ AI module not available — task queued for ${agent.id}`, 'info'); + outboxResult = 'queued — AI module not loaded, nothing executed'; } } catch (e) { this._handleAgentFailure(agent.id, e.message); @@ -1267,7 +1272,7 @@ class OrchestratorModule { agent.status = 'idle'; agent.lastHeartbeat = Date.now(); } - return outboxResult || 'completed'; + return outboxResult || 'no result recorded'; } // ─── Recurring Scheduling ─── |