entroly-wasm 0.19.3 → 0.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/server.js CHANGED
@@ -10,6 +10,7 @@ const { CheckpointManager, persistIndex, loadIndex } = require('./checkpoint');
10
10
  const { autoIndex, startIncrementalWatcher } = require('./auto_index');
11
11
  const { startAutotuneDaemon, FeedbackJournal, TaskProfileOptimizer } = require('./autotune');
12
12
  const { VaultManager } = require('./vault');
13
+ const { getTracker } = require('./value_tracker');
13
14
  const { EpistemicRouter, BeliefCompiler, VerificationEngine, ChangePipeline, FlowOrchestrator, compileDocs, exportTrainingData } = require('./cogops');
14
15
  const { WorkspaceChangeListener } = require('./workspace');
15
16
  const { SkillEngine } = require('./skills');
@@ -26,6 +27,11 @@ class EntrolyMCPServer {
26
27
  this.engine = new WasmEntrolyEngine();
27
28
  this.turnCounter = 0;
28
29
 
30
+ // Shared cross-runtime telemetry sink — SAME file/schema/dir as the
31
+ // Python tracker, so npm users' value shows up on the same dashboard
32
+ // as pip/MCP/SDK users. Fail-open: never let telemetry break a tool.
33
+ try { this.valueTracker = getTracker(); } catch (_) { this.valueTracker = null; }
34
+
29
35
  // Feedback journal — persists episodes for cross-session autotune
30
36
  this.feedbackJournal = new FeedbackJournal(this.config.checkpointDir);
31
37
  // Task-conditioned weight profiles (novel: per-task-type optimization)
@@ -112,6 +118,8 @@ class EntrolyMCPServer {
112
118
  // ── Analysis Tools (2) ──
113
119
  { name: 'repo_file_map', description: 'Return the canonical Entroly file map across Python, Rust core, and WASM repos with ownership roles.', inputSchema: { type: 'object', properties: { format: { type: 'string', description: 'Output format: markdown or json', default: 'markdown' } } } },
114
120
  { name: 'prefetch_related', description: 'Predict which files the LLM will need next based on co-access patterns and dependency graph.', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Current file being worked on' }, source_content: { type: 'string', description: 'Content of current file for import analysis', default: '' }, language: { type: 'string', description: 'Programming language', default: '' } }, required: ['file_path'] } },
121
+ // ── Hallucination Detection Tool ──
122
+ { name: 'verify_response', description: 'Verify an AI-generated response for hallucination. Computes entity coverage gap, hedging curvature, and entropy consistency. Returns fused_risk [0.0=safe, 1.0=hallucinated], verdict (pass/warn/flag), and flagged claims. All local — zero LLM calls.', inputSchema: { type: 'object', properties: { response: { type: 'string', description: 'The AI-generated text to verify' }, context: { type: 'string', description: 'Source context provided to the AI', default: '' }, prompt: { type: 'string', description: 'Original user prompt', default: '' } }, required: ['response'] } },
115
123
  ];
116
124
  }
117
125
 
@@ -130,6 +138,18 @@ class EntrolyMCPServer {
130
138
  const profile = this.taskProfiles.applyToEngine(this.engine, query);
131
139
  const result = this.engine.optimize(budget, query);
132
140
  result._taskProfile = { taskType: profile.taskType, confidence: profile.confidence };
141
+ // Record value to the shared sink (mirrors entroly/server.py).
142
+ try {
143
+ const ts = (result && result.tokens_saved) || 0;
144
+ if (this.valueTracker && ts > 0) {
145
+ this.valueTracker.record({
146
+ tokensSaved: ts,
147
+ model: (result && result.model) || '',
148
+ duplicates: (result && result.duplicates_caught) || 0,
149
+ optimized: true,
150
+ });
151
+ }
152
+ } catch (_) { /* never fail optimization for telemetry */ }
133
153
  const state = this.engine.export_state();
134
154
  this._lastOptCtx = { weights: { w_r: state.w_recency, w_f: state.w_frequency, w_s: state.w_semantic, w_e: state.w_entropy }, selectedSources: (result.selected || []).map(s => s.source).filter(Boolean), selectedCount: result.selected_count || 0, tokenBudget: budget, query, turn: this.turnCounter };
135
155
  if (this.checkpointMgr.shouldAutoCheckpoint()) { try { persistIndex(this.engine, this.indexPath); this.checkpointMgr.save({ engine_state: state, turn: this.turnCounter }); } catch {} }
@@ -181,7 +201,21 @@ class EntrolyMCPServer {
181
201
  case 'entroly_dashboard': {
182
202
  const stats = this.engine.stats();
183
203
  const explanation = this.engine.explain_selection();
184
- return { stats, explanation, turn: this.turnCounter };
204
+ // npm users live in their MCP client (no Python dashboard), so
205
+ // surface the persisted cross-session value here directly.
206
+ let value = null;
207
+ try {
208
+ if (this.valueTracker) {
209
+ const tr = this.valueTracker.getTrends();
210
+ value = {
211
+ lifetime: tr.lifetime,
212
+ today: (tr.daily.slice(-1)[0]) || null,
213
+ recent_activity: tr.activity.slice(0, 10),
214
+ data_source: 'shared telemetry file (cross-runtime)',
215
+ };
216
+ }
217
+ } catch (_) { /* fail-open */ }
218
+ return { stats, explanation, value, turn: this.turnCounter };
185
219
  }
186
220
 
187
221
  // ── CogOps Epistemic Tools ──
@@ -327,6 +361,71 @@ class EntrolyMCPServer {
327
361
  return { file: args.file_path, language: args.language || 'unknown', predicted_files: imports.slice(0, 10), dep_graph: depStats, engine: 'javascript' };
328
362
  }
329
363
 
364
+ // ── Hallucination Detection ──
365
+ case 'verify_response': {
366
+ const resp = args.response || '';
367
+ const ctx = args.context || '';
368
+ const prm = args.prompt || '';
369
+
370
+ // Entity coverage gap: extract identifiers from context vs response
371
+ const ctxEntities = new Set((ctx + ' ' + prm).match(/[a-zA-Z_][a-zA-Z0-9_.]{2,}/g) || []);
372
+ const respEntities = (resp.match(/[a-zA-Z_][a-zA-Z0-9_.]{2,}/g) || []);
373
+ const respUnique = [...new Set(respEntities)];
374
+ let grounded = 0;
375
+ const flaggedClaims = [];
376
+ for (const ent of respUnique) {
377
+ if (ctxEntities.has(ent) || ent.length <= 3) {
378
+ grounded++;
379
+ } else {
380
+ flaggedClaims.push({ claim: ent, score: 1.0 });
381
+ }
382
+ }
383
+ const entityGap = respUnique.length > 0 ? 1.0 - (grounded / respUnique.length) : 0;
384
+
385
+ // ECE-like: hedging language curvature
386
+ const hedgeWords = ['might', 'could', 'possibly', 'perhaps', 'maybe', 'uncertain', 'likely', 'probably', 'appears to', 'seems'];
387
+ const wordCount = resp.split(/\s+/).length;
388
+ let hedgeCount = 0;
389
+ for (const h of hedgeWords) { hedgeCount += (resp.toLowerCase().match(new RegExp(h, 'g')) || []).length; }
390
+ const hedgeCurvature = Math.min(1.0, hedgeCount / Math.max(wordCount, 1) * 20);
391
+
392
+ // EPR-like: entropy consistency (char-bigram)
393
+ const bigrams = {};
394
+ for (let i = 0; i < resp.length - 1; i++) {
395
+ const bg = resp.slice(i, i + 2);
396
+ bigrams[bg] = (bigrams[bg] || 0) + 1;
397
+ }
398
+ const total = Object.values(bigrams).reduce((a, b) => a + b, 0) || 1;
399
+ let entropy = 0;
400
+ for (const c of Object.values(bigrams)) {
401
+ const p = c / total;
402
+ if (p > 0) entropy -= p * Math.log2(p);
403
+ }
404
+ // Normalize: high entropy is normal for text, low entropy is suspicious
405
+ const maxEntropy = Math.log2(Math.min(Object.keys(bigrams).length, 500)) || 1;
406
+ const eprRisk = Math.max(0, 1.0 - entropy / maxEntropy);
407
+
408
+ // Fuse (same weights as proxy)
409
+ const fused = Math.max(0, Math.min(1.0,
410
+ 0.80 * entityGap + 0.08 * hedgeCurvature + 0.07 * eprRisk + 0.05 * 0
411
+ ));
412
+
413
+ let verdict, recommendation;
414
+ if (fused < 0.15) { verdict = 'pass'; recommendation = 'Accept — response appears well-grounded'; }
415
+ else if (fused < 0.40) { verdict = 'warn'; recommendation = 'Review — some claims may not be grounded'; }
416
+ else { verdict = 'flag'; recommendation = 'Reject or rephrase — high hallucination risk'; }
417
+
418
+ return {
419
+ fused_risk: Math.round(fused * 10000) / 10000,
420
+ verdict,
421
+ recommendation,
422
+ witness: { entity_coverage_gap: Math.round(entityGap * 10000) / 10000, total_entities: respUnique.length, grounded_count: grounded },
423
+ ece: { hedging_curvature: Math.round(hedgeCurvature * 10000) / 10000, hedge_count: hedgeCount },
424
+ epr: { entropy_risk: Math.round(eprRisk * 10000) / 10000, bigram_entropy: Math.round(entropy * 100) / 100 },
425
+ flagged_claims: flaggedClaims.slice(0, 10),
426
+ };
427
+ }
428
+
330
429
  default:
331
430
  throw new Error(`Unknown tool: ${name}`);
332
431
  }
@@ -1,13 +1,20 @@
1
1
  /**
2
- * ValueTracker — JS port of entroly/value_tracker.py
2
+ * ValueTracker — JS port of entroly/value_tracker.py (schema v3)
3
3
  *
4
4
  * Persistent, lifetime-savings accounting with the self-funded evolution
5
5
  * budget invariant:
6
6
  *
7
7
  * C_spent(t) ≤ τ · S(t) (τ = 5%)
8
8
  *
9
- * Any token-costing evolution step MUST gate on `getEvolutionBudget().canEvolve`.
10
- * The tracker is atomic-write safe and survives process restarts.
9
+ * CROSS-RUNTIME CONTRACT: this writes the SAME file, SAME directory and
10
+ * SAME JSON schema as the Python tracker so that one shared dashboard
11
+ * shows value no matter whether the user installed via pip (MCP/proxy),
12
+ * the SDK, or npm (this wasm runtime). The directory resolution and the
13
+ * UTC date-key formats below MUST stay byte-identical to
14
+ * entroly/value_tracker.py — diverging them silently re-creates the
15
+ * "blank dashboard for npm users" bug.
16
+ *
17
+ * Atomic-write safe; survives process restarts.
11
18
  */
12
19
 
13
20
  'use strict';
@@ -18,35 +25,22 @@ const os = require('os');
18
25
 
19
26
  const EVOLUTION_TAX_RATE = 0.05;
20
27
  const FILE_NAME = 'value_tracker.json';
28
+ const ACTIVITY_NAME = 'activity.jsonl';
29
+ const SCHEMA_VERSION = 3;
30
+ const MAX_DAILY = 90, MAX_WEEKLY = 52, MAX_MONTHLY = 24, MAX_ACTIVITY = 200;
21
31
 
22
32
  // Per-model $/1M tokens — kept in sync with entroly/value_tracker.py (×1000).
23
33
  const COST_PER_M = {
24
34
  default: 3.0,
25
- // OpenAI
26
- 'gpt-4o': 2.5,
27
- 'gpt-4o-mini': 0.15,
28
- 'gpt-4-turbo': 10.0,
29
- 'gpt-4': 30.0,
30
- 'gpt-3.5-turbo': 0.5,
31
- 'o1': 15.0,
32
- 'o1-mini': 3.0,
33
- 'o3': 10.0,
34
- 'o3-mini': 1.1,
35
- 'o4-mini': 1.1,
36
- // Anthropic
37
- 'claude-opus-4': 15.0,
38
- 'claude-sonnet-4': 3.0,
39
- 'claude-haiku-4': 0.8,
40
- 'claude-3-5-sonnet': 3.0,
41
- 'claude-3-5-haiku': 0.8,
42
- // Google
43
- 'gemini-2.5-pro': 1.25,
44
- 'gemini-2.5-flash': 0.075,
45
- 'gemini-1.5-pro': 1.25,
46
- 'gemini-1.5-flash': 0.075,
35
+ 'gpt-4o': 2.5, 'gpt-4o-mini': 0.15, 'gpt-4-turbo': 10.0, 'gpt-4': 30.0,
36
+ 'gpt-3.5-turbo': 0.5, 'o1': 15.0, 'o1-mini': 3.0, 'o3': 10.0,
37
+ 'o3-mini': 1.1, 'o4-mini': 1.1,
38
+ 'claude-opus-4': 15.0, 'claude-sonnet-4': 3.0, 'claude-haiku-4': 0.8,
39
+ 'claude-3-5-sonnet': 3.0, 'claude-3-5-haiku': 0.8,
40
+ 'gemini-2.5-pro': 1.25, 'gemini-2.5-flash': 0.075,
41
+ 'gemini-1.5-pro': 1.25, 'gemini-1.5-flash': 0.075,
47
42
  };
48
43
 
49
- // Old→new name aliases so 'claude-3-opus' hits the same rate as Python.
50
44
  const MODEL_ALIASES = {
51
45
  'claude-3-opus': 'claude-opus-4',
52
46
  'claude-3-sonnet': 'claude-sonnet-4',
@@ -60,11 +54,10 @@ function estimateCost(tokens, model = '') {
60
54
  for (const [alias, canonical] of Object.entries(MODEL_ALIASES)) {
61
55
  if (m.startsWith(alias)) { m = canonical + m.slice(alias.length); break; }
62
56
  }
63
- // Longest-prefix match prevents 'gpt-4o' eating 'gpt-4o-mini'.
64
- const keys = Object.keys(COST_PER_M).filter(k => k !== 'default').sort((a, b) => b.length - a.length);
57
+ const keys = Object.keys(COST_PER_M).filter(k => k !== 'default')
58
+ .sort((a, b) => b.length - a.length);
65
59
  const key = (m && keys.find(k => m.startsWith(k)));
66
60
  if (!key && model) {
67
- // One-time warning per unknown model — stderr, doesn't pollute stdout.
68
61
  if (!estimateCost._warned) estimateCost._warned = new Set();
69
62
  if (!estimateCost._warned.has(model)) {
70
63
  estimateCost._warned.add(model);
@@ -74,6 +67,29 @@ function estimateCost(tokens, model = '') {
74
67
  return (tokens / 1_000_000) * COST_PER_M[key || 'default'];
75
68
  }
76
69
 
70
+ // ── UTC date keys — MUST match Python time.gmtime()-based keys ──────────
71
+
72
+ function _pad(n) { return String(n).padStart(2, '0'); }
73
+
74
+ function _dayKey(d = new Date()) {
75
+ return `${d.getUTCFullYear()}-${_pad(d.getUTCMonth() + 1)}-${_pad(d.getUTCDate())}`;
76
+ }
77
+
78
+ function _monthKey(d = new Date()) {
79
+ return `${d.getUTCFullYear()}-${_pad(d.getUTCMonth() + 1)}`;
80
+ }
81
+
82
+ function _weekKey(d = new Date()) {
83
+ // ISO-8601 week, matching Python datetime.date.isocalendar().
84
+ const t = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
85
+ const day = t.getUTCDay() || 7; // Mon=1..Sun=7
86
+ t.setUTCDate(t.getUTCDate() + 4 - day); // nearest Thursday
87
+ const isoYear = t.getUTCFullYear();
88
+ const yearStart = new Date(Date.UTC(isoYear, 0, 1));
89
+ const week = Math.ceil((((t - yearStart) / 86400000) + 1) / 7);
90
+ return `${isoYear}-W${_pad(week)}`;
91
+ }
92
+
77
93
  function atomicWrite(filePath, content) {
78
94
  const tmp = filePath + '.tmp-' + process.pid;
79
95
  fs.writeFileSync(tmp, content, 'utf8');
@@ -82,10 +98,16 @@ function atomicWrite(filePath, content) {
82
98
 
83
99
  class ValueTracker {
84
100
  constructor(dataDir = null) {
85
- this._dir = dataDir || path.join(os.homedir(), '.entroly');
101
+ // Honor ENTROLY_DIR exactly like Python's _default_dir(); else
102
+ // ~/.entroly. NOT cwd/.entroly — that was the npm/py path split.
103
+ this._dir = dataDir
104
+ || process.env.ENTROLY_DIR
105
+ || path.join(os.homedir(), '.entroly');
86
106
  fs.mkdirSync(this._dir, { recursive: true });
87
107
  this._path = path.join(this._dir, FILE_NAME);
88
- this._data = this._load();
108
+ this._activityPath = path.join(this._dir, ACTIVITY_NAME);
109
+ this._data = this._migrate(this._load());
110
+ this._activity = this._loadActivity();
89
111
  }
90
112
 
91
113
  _load() {
@@ -101,36 +123,143 @@ class ValueTracker {
101
123
  _defaults() {
102
124
  const now = Date.now() / 1000;
103
125
  return {
104
- version: 2,
126
+ version: SCHEMA_VERSION,
105
127
  lifetime: {
106
- tokens_saved: 0,
107
- cost_saved_usd: 0.0,
108
- requests_optimized: 0,
109
- first_seen: now,
110
- last_seen: now,
111
- evolution_spent_usd: 0.0,
112
- evolution_attempts: 0,
128
+ tokens_saved: 0, cost_saved_usd: 0.0,
129
+ requests_optimized: 0, requests_total: 0, duplicates_caught: 0,
130
+ first_seen: now, last_seen: now,
131
+ evolution_spent_usd: 0.0, evolution_attempts: 0,
113
132
  evolution_successes: 0,
133
+ hallucinations_blocked: 0, routing_saved_usd: 0.0,
134
+ routing_decisions: 0,
114
135
  },
136
+ daily: {}, weekly: {}, monthly: {},
115
137
  };
116
138
  }
117
139
 
140
+ _migrate(data) {
141
+ // Backward-compatible forward migration (v2 → v3): backfill any
142
+ // missing keys/buckets without touching existing counters.
143
+ const base = this._defaults();
144
+ data.lifetime = data.lifetime || {};
145
+ for (const [k, v] of Object.entries(base.lifetime)) {
146
+ if (!(k in data.lifetime)) data.lifetime[k] = v;
147
+ }
148
+ for (const b of ['daily', 'weekly', 'monthly']) {
149
+ if (!data[b] || typeof data[b] !== 'object') data[b] = {};
150
+ }
151
+ data.version = SCHEMA_VERSION;
152
+ return data;
153
+ }
154
+
155
+ _loadActivity() {
156
+ if (!fs.existsSync(this._activityPath)) return [];
157
+ try {
158
+ const out = [];
159
+ for (const line of fs.readFileSync(this._activityPath, 'utf8').split('\n')) {
160
+ const s = line.trim();
161
+ if (!s) continue;
162
+ try { out.push(JSON.parse(s)); } catch (_) { /* skip */ }
163
+ }
164
+ return out.slice(-MAX_ACTIVITY);
165
+ } catch (_) { return []; }
166
+ }
167
+
118
168
  _save() {
119
169
  try { atomicWrite(this._path, JSON.stringify(this._data, null, 2)); }
120
170
  catch (_) { /* best-effort */ }
121
171
  }
122
172
 
123
- record({ tokensSaved = 0, model = '', optimized = true } = {}) {
173
+ _saveActivity() {
174
+ try {
175
+ this._activity = this._activity.slice(-MAX_ACTIVITY);
176
+ const content = this._activity.map(e => JSON.stringify(e)).join('\n')
177
+ + (this._activity.length ? '\n' : '');
178
+ atomicWrite(this._activityPath, content);
179
+ } catch (_) { /* best-effort */ }
180
+ }
181
+
182
+ _bump(bucketName, key, tokens, cost) {
183
+ const bucket = this._data[bucketName] || (this._data[bucketName] = {});
184
+ if (!bucket[key]) bucket[key] = { tokens_saved: 0, cost_saved: 0.0, requests: 0 };
185
+ bucket[key].tokens_saved += tokens;
186
+ bucket[key].cost_saved = +(bucket[key].cost_saved + cost).toFixed(6);
187
+ bucket[key].requests += 1;
188
+ const limit = { daily: MAX_DAILY, weekly: MAX_WEEKLY, monthly: MAX_MONTHLY }[bucketName];
189
+ const ks = Object.keys(bucket);
190
+ if (ks.length > limit) {
191
+ ks.sort();
192
+ for (const old of ks.slice(0, ks.length - limit)) delete bucket[old];
193
+ }
194
+ }
195
+
196
+ record({ tokensSaved = 0, model = '', duplicates = 0, optimized = true } = {}) {
124
197
  const cost = estimateCost(tokensSaved, model);
198
+ const now = new Date();
125
199
  const lt = this._data.lifetime;
126
200
  lt.tokens_saved += tokensSaved;
127
201
  lt.cost_saved_usd = +(lt.cost_saved_usd + cost).toFixed(6);
202
+ lt.requests_total = (lt.requests_total || 0) + 1;
128
203
  if (optimized) lt.requests_optimized += 1;
204
+ lt.duplicates_caught = (lt.duplicates_caught || 0) + duplicates;
129
205
  lt.last_seen = Date.now() / 1000;
206
+ this._bump('daily', _dayKey(now), tokensSaved, cost);
207
+ this._bump('weekly', _weekKey(now), tokensSaved, cost);
208
+ this._bump('monthly', _monthKey(now), tokensSaved, cost);
130
209
  this._save();
210
+ this._activity.push({
211
+ ts: +(Date.now() / 1000).toFixed(3),
212
+ kind: 'optimize',
213
+ summary: `Optimized request: saved ${tokensSaved.toLocaleString()} tokens`
214
+ + (model ? ` (${model})` : ''),
215
+ tokens_saved: tokensSaved,
216
+ cost_saved_usd: +cost.toFixed(6),
217
+ model: model || '',
218
+ duplicates,
219
+ source: 'npm',
220
+ });
221
+ this._saveActivity();
131
222
  return { tokensSaved, costSaved: cost };
132
223
  }
133
224
 
225
+ recordEvent(kind, summary, opts = {}) {
226
+ try {
227
+ const row = {
228
+ ts: +(Date.now() / 1000).toFixed(3),
229
+ kind: String(kind),
230
+ summary: String(summary).slice(0, 240),
231
+ source: opts.source || 'npm',
232
+ };
233
+ if (opts.tokensSaved) row.tokens_saved = opts.tokensSaved | 0;
234
+ if (opts.costSavedUsd) row.cost_saved_usd = +Number(opts.costSavedUsd).toFixed(6);
235
+ if (opts.model) row.model = opts.model;
236
+ this._activity.push(row);
237
+ this._saveActivity();
238
+ } catch (_) { /* fail-open */ }
239
+ }
240
+
241
+ recordHallucinationBlocked(n = 1, detail = '') {
242
+ try {
243
+ this._data.lifetime.hallucinations_blocked =
244
+ (this._data.lifetime.hallucinations_blocked || 0) + (n | 0);
245
+ this._save();
246
+ this.recordEvent('hallucination',
247
+ detail || `Blocked ${n} unsupported claim(s)`, { source: 'witness' });
248
+ } catch (_) { /* fail-open */ }
249
+ }
250
+
251
+ recordRoutingSaving(costSavedUsd, chosenModel = '', detail = '') {
252
+ try {
253
+ const lt = this._data.lifetime;
254
+ lt.routing_saved_usd = +((lt.routing_saved_usd || 0) + Number(costSavedUsd)).toFixed(6);
255
+ lt.routing_decisions = (lt.routing_decisions || 0) + 1;
256
+ this._save();
257
+ this.recordEvent('routing',
258
+ detail || `Routed to ${chosenModel || 'cheaper model'}`,
259
+ { source: 'ravs', costSavedUsd, model: chosenModel });
260
+ } catch (_) { /* fail-open */ }
261
+ }
262
+
134
263
  getEvolutionBudget() {
135
264
  const lt = this._data.lifetime;
136
265
  const lifetimeSaved = lt.cost_saved_usd || 0;
@@ -152,14 +281,9 @@ class ValueTracker {
152
281
  const currentSpent = lt.evolution_spent_usd || 0;
153
282
  const totalEarned = lifetimeSaved * EVOLUTION_TAX_RATE;
154
283
  const available = totalEarned - currentSpent;
155
-
156
284
  if (costUsd > available + 0.001) {
157
- return {
158
- status: 'rejected',
159
- remainingUsd: +Math.max(0, available).toFixed(6),
160
- };
285
+ return { status: 'rejected', remainingUsd: +Math.max(0, available).toFixed(6) };
161
286
  }
162
-
163
287
  lt.evolution_spent_usd = +(currentSpent + costUsd).toFixed(6);
164
288
  lt.evolution_attempts = (lt.evolution_attempts || 0) + 1;
165
289
  if (success) lt.evolution_successes = (lt.evolution_successes || 0) + 1;
@@ -170,12 +294,43 @@ class ValueTracker {
170
294
  };
171
295
  }
172
296
 
297
+ // ── Read APIs for the entroly_dashboard MCP tool ───────────────────────
298
+
299
+ _sortedBucket(name, lastN) {
300
+ const b = this._data[name] || {};
301
+ return Object.keys(b).sort().slice(-lastN).map(k => ({ key: k, ...b[k] }));
302
+ }
303
+
304
+ getTrends() {
305
+ return {
306
+ lifetime: { ...this._data.lifetime },
307
+ daily: this._sortedBucket('daily', 30),
308
+ weekly: this._sortedBucket('weekly', 12),
309
+ monthly: this._sortedBucket('monthly', 12),
310
+ activity: this._activity.slice(-50).reverse(),
311
+ };
312
+ }
313
+
314
+ getActivity(lastN = 50) {
315
+ return this._activity.slice(-lastN).reverse();
316
+ }
317
+
173
318
  stats() {
174
319
  return {
175
320
  lifetime: { ...this._data.lifetime },
176
321
  budget: this.getEvolutionBudget(),
322
+ activity: this.getActivity(20),
177
323
  };
178
324
  }
179
325
  }
180
326
 
181
- module.exports = { ValueTracker, EVOLUTION_TAX_RATE, estimateCost };
327
+ // Module-level singleton, mirroring Python get_tracker().
328
+ let _tracker = null;
329
+ function getTracker() {
330
+ if (!_tracker) _tracker = new ValueTracker();
331
+ return _tracker;
332
+ }
333
+
334
+ module.exports = {
335
+ ValueTracker, EVOLUTION_TAX_RATE, estimateCost, getTracker,
336
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entroly-wasm",
3
- "version": "0.19.3",
3
+ "version": "0.19.5",
4
4
  "description": "Information-theoretic context optimization for AI coding agents. Zero-friction, pure WebAssembly - no Python dependency.",
5
5
  "main": "index.js",
6
6
  "types": "pkg/entroly_wasm.d.ts",
Binary file