aboutsummaryrefslogtreecommitdiff
path: root/modules/core/core-bundle.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-11-02 21:05:58 +0100
committerAgando <roduccomm@gmail.com>2026-11-02 21:05:58 +0100
commitc2f2ea62503f81c07f4d36d7c3a798b91146153e (patch)
tree6f20a4437c75a7f27d4b01f4dd2fe413e4bcfcd1 /modules/core/core-bundle.js
parentb339bc13199eb9326307905dd635c56974b55f53 (diff)
fix(newos4): WebDAV listing sizes + pull, and five MemoryGraph defects
Closes STEER-20261102-205416-facd and STEER-20261102-204031-f82a. WebDAV (modules/ext/webdav.js): - listings showed "21 B" for a 21 KB file: nginx emits humanised sizes (autoindex_exact_size off, vhost :412) and _parseAutoindex did parseInt("21K"). _parseAutoindexSize() handles K/M/G, decimals, lowercase and "-"; PROPFIND's exact getcontentlength path is untouched. - webdav-pull into a missing local directory printed "Pulled: ... (N bytes)" and wrote nothing: it ignored fs.writeFile's false return (virtual-fs.js:561). It now creates the parent chain, checks the write, and only reports success after a confirmed write. The byte count uses TextEncoder, not .length (a Cyrillic file under-reported ~2x). - _formatSize gained the GB branch the parser already accepted (3 GB printed "3072.0 MB") and no longer prints "NaN MB" for a missing size. MemoryGraph (modules/core/module-memory-graph.js): - addEdge averages weights, so every repeat pulled a strong edge toward the call site's constant (0.9 -> 0.7 -> 0.6). Now Math.max, with decay left to applyDecay(); prune() untouched. - task ids were `task-${Date.now()}`, so re-submitting a task never deduped: now a hash of agent + normalised text. - recordFileAccess no longer records the graph's own /.memory_graph/ store. - failed tool calls were weighted 0.8 vs success 0.5, so showStats ranked failures as strongest: now 0.2. Legacy 0.8 failures are capped on load. - concept ids keep punctuation ("concept:(v2!),"): slugged, and the slug uses \p{L}\p{N} so Cyrillic/CJK/accented text still yields nodes -- an a-z0-9 slug silently produced none. Review findings folded in from the regression pass: the non-Latin regression (a-z0-9 slug), the GB formatter asymmetry, and the pull byte count were all found by review, not by the patch authors, and each now has a test. Tests: test-webdav-sizes.js 27 (was RED 15/24 before the fix), test-memory-graph.js 37 (RED 17/32), both auto-discovered by newos4-maintain -- 26/26 node tests. Live on v4.6.140: _parseAutoindexSize('21K') 21504, _formatSize -> "21.0 KB", _formatSize(2GiB) "2.0 GB", slug Cyrillic "исправить-парсер-ошибок", 0 console errors.
Diffstat (limited to 'modules/core/core-bundle.js')
-rw-r--r--modules/core/core-bundle.js106
1 files changed, 89 insertions, 17 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index cf805a9..379c297 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -18205,6 +18205,33 @@ class MemoryGraphModule {
this._lastDecayRun = 0;
}
+ // ─── Id helpers ───
+
+ // Concept ids must be stable, punctuation-free slugs. The old split was whitespace-only, so
+ // 'Neural VFS, v2!' became 'concept:Neural VFS, v2!' — an id that never matched the same
+ // word written anywhere else (and put commas/brackets into stored node keys).
+ _slug(text) {
+ return String(text == null ? '' : text)
+ .toLowerCase()
+ // \p{L}\p{N} keeps Cyrillic, CJK, accents: a-z0-9 slugged
+ // 'Исправить парсер' to '' and German 'Ärger' to 'rger'.
+ .replace(/[^\p{L}\p{N}]+/gu, '-')
+ .replace(/^-+|-+$/g, '');
+ }
+
+ // FNV-1a (32-bit) as base36. Used for content-derived task ids: Date.now() ids meant an
+ // identical task re-submitted a second later created a brand new node (and a new 'executed'
+ // edge) instead of reinforcing the existing one.
+ _hash(text) {
+ const s = String(text == null ? '' : text);
+ let h = 0x811c9dc5;
+ for (let i = 0; i < s.length; i++) {
+ h ^= s.charCodeAt(i);
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
+ }
+ return h.toString(36);
+ }
+
getHelpInfo() {
return {
'memgraph-status': 'memgraph-status — Show node/edge counts, last access',
@@ -18300,7 +18327,16 @@ class MemoryGraphModule {
e.source === sourceKey && e.target === targetKey && e.type === type
);
if (edge) {
- edge.weight = (edge.weight + weight) / 2;
+ // v4.6.140: max, not mean. Every one of the 9 call sites passes a constant that
+ // describes the *kind* of relationship ('used' 0.5, 'accessed' 0.5, 'maps_to' 0.7).
+ // Averaging therefore decayed a strong edge toward that constant on every repeat:
+ // using a relationship weakened it, and a 0.9 edge became 0.6 after three accesses.
+ // Weight is now monotonic — an edge is as strong as the strongest observation of it,
+ // and weak observations can never erode strong evidence. Fading over time is still
+ // applyDecay()'s job (decay_score); prune() is untouched.
+ const prev = Number.isFinite(Number(edge.weight)) ? Number(edge.weight) : 0;
+ const next = Number.isFinite(Number(weight)) ? Number(weight) : prev;
+ edge.weight = Math.max(prev, next);
edge.last_used = new Date().toISOString();
edge.use_count++;
if (agentId) edge.source_agents = [...new Set([...(edge.source_agents || []), agentId])];
@@ -18323,6 +18359,14 @@ class MemoryGraphModule {
// v4.5.89 fix: Thread agentId through record* methods (P1-3 from review)
recordFileAccess(path, agentId = null) {
+ // v4.6.140: never record the graph's own store. Saving (and exportGraph) goes through
+ // the same VFS as AI file access, so /.memory_graph/* was recorded as user files: the
+ // graph observed itself, minting a file node plus 'accessed'/'has_type' edges per save.
+ if (typeof path !== 'string' || !path) return null;
+ const storeDir = this._indexPath.replace(/\/+$/, ''); // '/.memory_graph'
+ const abs = path.charAt(0) === '/' ? path : '/' + path;
+ if (abs === storeDir || abs.indexOf(storeDir + '/') === 0) return null;
+
const name = path.split('/').pop() || path;
const nodeKey = this.addNode('file', path, name, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
@@ -18339,11 +18383,17 @@ class MemoryGraphModule {
recordToolCall(toolName, success, agentId = null) {
const nodeKey = this.addNode('tool', toolName, toolName, {}, agentId);
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
- this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.8, agentId);
+ // v4.6.140: failure used to persist at 0.8 while success persisted at 0.5, so showStats'
+ // 'Strongest relationships' ranked failures above successes. A failure is weaker evidence
+ // than a success: keep it below the success weight (and below prune()'s 0.8 keep-bar) so
+ // stale failures age out like any other weak edge instead of dominating the graph.
+ this.addEdge(sessionKey, nodeKey, success ? 'used' : 'failed', success ? 0.5 : 0.2, agentId);
}
recordConcept(concept, filePath) {
- const conceptKey = this.addNode('concept', concept, concept);
+ const slug = this._slug(concept);
+ if (!slug) return;
+ const conceptKey = this.addNode('concept', slug, concept);
const fileKey = this.addNode('file', filePath, filePath.split('/').pop());
this.addEdge(conceptKey, fileKey, 'maps_to', 0.7);
this.addEdge(fileKey, conceptKey, 'tagged_as', 0.7);
@@ -18351,8 +18401,13 @@ class MemoryGraphModule {
// v4.5.88: recordTask now passes agentId for namespacing + adds source_session link
recordTask(task, result, agentId) {
- const taskId = `task-${Date.now()}`;
- const taskKey = this.addNode('task', taskId, task.substring(0, 60), {
+ // v4.6.140: id derived from the task text + agent namespace, not Date.now(). Re-submitting
+ // the same task now merges into one node (access_count/use_count grow) instead of creating
+ // a fresh node every millisecond. Normalisation ignores case and runs of whitespace.
+ const text = typeof task === 'string' ? task : String(task == null ? '' : task);
+ const normalized = text.trim().replace(/\s+/g, ' ').toLowerCase();
+ const taskId = `task-${this._hash((agentId || 'global') + '|' + normalized)}`;
+ const taskKey = this.addNode('task', taskId, text.substring(0, 60), {
full: task,
result: result?.substring(0, 100),
agent: agentId
@@ -18360,11 +18415,14 @@ class MemoryGraphModule {
const sessionKey = `session:session-${Math.floor(Date.now() / 86400000)}`;
this.addEdge(sessionKey, taskKey, 'executed', 0.6, agentId);
- // Extract keywords as concepts
- const words = task.toLowerCase().split(/\s+/).filter(w => w.length > 3);
+ // Extract keywords as concepts. Split on anything that is not alphanumeric and slug the
+ // result, so the id can never carry punctuation ('(v2!),' used to become a key).
+ const words = normalized.split(/[^\p{L}\p{N}]+/u).filter(w => w.length > 3);
for (const word of [...new Set(words)].slice(0, 5)) {
- this.addNode('concept', word, word);
- this.addEdge(taskKey, `concept:${word}`, 'involves', 0.4);
+ const slug = this._slug(word);
+ if (!slug) continue;
+ this.addNode('concept', slug, word);
+ this.addEdge(taskKey, `concept:${slug}`, 'involves', 0.4);
}
// Link to tools used (if any found in task)
@@ -18708,9 +18766,13 @@ class MemoryGraphModule {
if (!args || args.length < 2) return this.terminal.print('Usage: memgraph-learn <entity> <fact>', 'error');
const entity = args[0];
const fact = args.slice(1).join(' ');
- this.addNode('concept', entity, entity);
+ // v4.6.140: same slug rule as recordConcept/recordTask, so 'memgraph-learn "Neural VFS"'
+ // maps to the concept:neural-vfs node the other writers already created.
+ const entitySlug = this._slug(entity);
+ if (!entitySlug) return this.terminal.print('Entity needs at least one letter or digit', 'error');
+ this.addNode('concept', entitySlug, entity);
const factKey = this.addNode('fact', entity + '-' + Date.now(), fact);
- const conceptKey = `concept:${entity}`;
+ const conceptKey = `concept:${entitySlug}`;
this.addEdge(conceptKey, factKey, 'has_fact', 0.9);
this.terminal.print(`✅ Learned: ${entity} → ${fact}`, 'success');
// v4.5.89b fix: Index only the new fact, not all facts (P2 from review — O(n) → O(1))
@@ -18800,9 +18862,7 @@ class MemoryGraphModule {
this._nodes[n.id] = n;
}
this._edges = exportData.edges || [];
- for (const e of this._edges) {
- if (!e.source_agents) e.source_agents = [];
- }
+ this._normalizeEdges();
this._totalNodeAccesses = (exportData.stats?.totalAccesses) || 0;
return;
}
@@ -18822,9 +18882,7 @@ class MemoryGraphModule {
const edges = fs.fsReadJSON(`${this._indexPath}edges.json`);
if (edges) {
this._edges = edges;
- for (const e of this._edges) {
- if (!e.source_agents) e.source_agents = [];
- }
+ this._normalizeEdges();
}
// v4.5.89b fix: Load persisted decay config (P1 from review)
const config = fs.fsReadJSON(`${this._indexPath}config.json`);
@@ -18838,6 +18896,20 @@ class MemoryGraphModule {
}
}
+ // v4.6.140: normalise edges read from an older store. Two things differ: pre-v4.5.88 records
+ // have no source_agents array, and 'failed' edges were written at 0.8 — higher than the 0.5
+ // successes — so a stored graph would keep ranking failures on top until they were re-seen.
+ // Nothing else is rewritten: weights, ids and timestamps are otherwise left exactly as stored,
+ // so old Date.now() task ids stay addressable (they simply coexist with new hashed ids).
+ _normalizeEdges() {
+ for (const e of this._edges) {
+ if (!e.source_agents) e.source_agents = [];
+ const w = Number(e.weight);
+ e.weight = Number.isFinite(w) ? w : 0.5;
+ if (e.type === 'failed' && e.weight > 0.2) e.weight = 0.2;
+ }
+ }
+
// MAINT-003: keep an unreadable store aside instead of starting fresh over it.
_quarantineCorrupt(name) {
try {