aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-18 16:34:19 +0200
committerAgando <roduccomm@gmail.com>2026-10-18 16:34:19 +0200
commit3d66fbe2dde898f57cd368bc37257b63ef4752f1 (patch)
tree321eed2642b9d0ab74a1cb4df2ff9b96b2c72d90
parent4de231bc9a869ef5049224ad43e2200a882b424c (diff)
newOS4 v4.6.31: tool-result memo cache with epoch invalidation (D2)
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js36
-rw-r--r--modules/core/module-aiassist.js36
-rw-r--r--modules/core/module-loader.js2
4 files changed, 74 insertions, 6 deletions
diff --git a/main.html b/main.html
index b75ebf2..2c86a3d 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.30"></script>
+<script src="modules/core/core-bundle.js?v=v4.6.31"></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.30"></script>
+<script src="modules/core/module-loader.js?v=v4.6.31"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=v4.6.30"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=v4.6.31"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 6022a37..5bbc840 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -8030,6 +8030,14 @@ class ServerMonitorModule {
this.systemPrompt = this.buildSystemPrompt();
this.context = []; this.maxContext = 10;
this._sessions = new Map(); // agentTag -> {context, profile}; 'main' aliases this.context
+ this._toolCache = new Map(); // D2: key -> {result, ts, epoch}
+ this._toolEpoch = 0;
+ this._mutatingTools = new Set(['write_file', 'edit_module', 'run_command', 'delete_file',
+ 'make_directory', 'commit_module', 'ship_module', 'patch_skill', 'delegate_task',
+ 'reload_module', 'create_skill']);
+ // Only deterministic reads are cached — never status/log/polling tools.
+ this._cacheableTools = new Set(['read_file', 'list_files', 'list_modules', 'view_module',
+ 'nvfs_resolve', 'get_help', 'list_skills', 'diff_module', 'get_module_status']);
this.maxContextTokens = 2000;
this.MAX_TOOL_TURNS = 8;
this._pendingConfirmation = null;
@@ -8783,6 +8791,23 @@ buildSystemPrompt() {
this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args);
this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args);
}
+ _cacheGet(name, args) {
+ const key = name + ':' + (typeof args === 'string' ? args : JSON.stringify(args || {}));
+ if (!this._cacheableTools.has(name)) return { hit: false, key: key };
+ const e = this._toolCache.get(key);
+ if (e && e.epoch === this._toolEpoch && (Date.now() - e.ts) < 30000) {
+ return { hit: true, key: key, result: e.result };
+ }
+ return { hit: false, key: key };
+ }
+ _cacheSet(name, key, result) {
+ if (this._mutatingTools.has(name)) { this._toolEpoch++; return; } // invalidate all
+ if (!this._cacheableTools.has(name) || !key || typeof result !== 'string') return;
+ this._toolCache.set(key, { result: result, ts: Date.now(), epoch: this._toolEpoch });
+ if (this._toolCache.size > 200) {
+ this._toolCache.delete(this._toolCache.keys().next().value);
+ }
+ }
_sessionCtx(tag) {
const k = tag || 'main';
// main is always the live this.context (commands like `ai-clear`/`load` reassign it)
@@ -9500,7 +9525,15 @@ buildSystemPrompt() {
for (const tc of safeTools) {
const fn = tc.function;
const t0 = Date.now();
- const toolResult = await this.runTool(fn.name, fn.arguments);
+ const _cg = this._cacheGet(fn.name, fn.arguments);
+ let toolResult;
+ if (_cg.hit) {
+ toolResult = _cg.result;
+ this.terminal.print(`♻️ ${fn.name} → cached (${toolResult.length}b, not re-run)`, 'info');
+ } else {
+ toolResult = await this.runTool(fn.name, fn.arguments);
+ this._cacheSet(fn.name, _cg.key, toolResult);
+ }
const t1 = Math.round((Date.now() - t0) / 1000);
this._toolCallLog.push({
turn: toolTurns,
@@ -9581,6 +9614,7 @@ buildSystemPrompt() {
const t0 = Date.now();
this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
const toolResult = await this.runTool(fn.name, fn.arguments);
+ this._toolEpoch++; // D2: mutating action invalidates every cached read
const t1 = Math.round((Date.now() - t0) / 1000);
this._toolCallLog.push({
turn: toolTurns,
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 74a071b..a5c5022 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -7,6 +7,14 @@
this.systemPrompt = this.buildSystemPrompt();
this.context = []; this.maxContext = 10;
this._sessions = new Map(); // agentTag -> {context, profile}; 'main' aliases this.context
+ this._toolCache = new Map(); // D2: key -> {result, ts, epoch}
+ this._toolEpoch = 0;
+ this._mutatingTools = new Set(['write_file', 'edit_module', 'run_command', 'delete_file',
+ 'make_directory', 'commit_module', 'ship_module', 'patch_skill', 'delegate_task',
+ 'reload_module', 'create_skill']);
+ // Only deterministic reads are cached — never status/log/polling tools.
+ this._cacheableTools = new Set(['read_file', 'list_files', 'list_modules', 'view_module',
+ 'nvfs_resolve', 'get_help', 'list_skills', 'diff_module', 'get_module_status']);
this.maxContextTokens = 2000;
this.MAX_TOOL_TURNS = 8;
this._pendingConfirmation = null;
@@ -760,6 +768,23 @@ buildSystemPrompt() {
this.commands['vaultmind-query'] = (args) => this._vaultmindQueryCmd(args);
this.commands['wallet-lookup'] = (args) => this._walletLookupCmd(args);
}
+ _cacheGet(name, args) {
+ const key = name + ':' + (typeof args === 'string' ? args : JSON.stringify(args || {}));
+ if (!this._cacheableTools.has(name)) return { hit: false, key: key };
+ const e = this._toolCache.get(key);
+ if (e && e.epoch === this._toolEpoch && (Date.now() - e.ts) < 30000) {
+ return { hit: true, key: key, result: e.result };
+ }
+ return { hit: false, key: key };
+ }
+ _cacheSet(name, key, result) {
+ if (this._mutatingTools.has(name)) { this._toolEpoch++; return; } // invalidate all
+ if (!this._cacheableTools.has(name) || !key || typeof result !== 'string') return;
+ this._toolCache.set(key, { result: result, ts: Date.now(), epoch: this._toolEpoch });
+ if (this._toolCache.size > 200) {
+ this._toolCache.delete(this._toolCache.keys().next().value);
+ }
+ }
_sessionCtx(tag) {
const k = tag || 'main';
// main is always the live this.context (commands like `ai-clear`/`load` reassign it)
@@ -1477,7 +1502,15 @@ buildSystemPrompt() {
for (const tc of safeTools) {
const fn = tc.function;
const t0 = Date.now();
- const toolResult = await this.runTool(fn.name, fn.arguments);
+ const _cg = this._cacheGet(fn.name, fn.arguments);
+ let toolResult;
+ if (_cg.hit) {
+ toolResult = _cg.result;
+ this.terminal.print(`♻️ ${fn.name} → cached (${toolResult.length}b, not re-run)`, 'info');
+ } else {
+ toolResult = await this.runTool(fn.name, fn.arguments);
+ this._cacheSet(fn.name, _cg.key, toolResult);
+ }
const t1 = Math.round((Date.now() - t0) / 1000);
this._toolCallLog.push({
turn: toolTurns,
@@ -1558,6 +1591,7 @@ buildSystemPrompt() {
const t0 = Date.now();
this.terminal.print(`🔧 Tool: ${fn.name}(${typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments)})`, 'info');
const toolResult = await this.runTool(fn.name, fn.arguments);
+ this._toolEpoch++; // D2: mutating action invalidates every cached read
const t1 = Math.round((Date.now() - t0) / 1000);
this._toolCallLog.push({
turn: toolTurns,
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index 0674d07..c1571fc 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.30';
+ const LATEST_VER = 'v4.6.31';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {