aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/debug/agent-replies.php59
-rw-r--r--help.html18
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js9
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/module-orchestrator.js9
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);
diff --git a/help.html b/help.html
index c70b641..a989b5b 100644
--- a/help.html
+++ b/help.html
@@ -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>
diff --git a/main.html b/main.html
index f27008a..5f57851 100644
--- a/main.html
+++ b/main.html
@@ -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 ───