prism-mcp-server 8.0.2 → 9.0.4
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/README.md +80 -9
- package/dist/cli.js +0 -0
- package/dist/config.js +42 -5
- package/dist/dashboard/server.js +118 -0
- package/dist/dashboard/ui.js +211 -1
- package/dist/memory/cognitiveBudget.js +224 -0
- package/dist/memory/surprisalGate.js +119 -0
- package/dist/memory/synapseEngine.js +28 -2
- package/dist/memory/valenceEngine.js +234 -0
- package/dist/scholar/webScholar.js +7 -6
- package/dist/server.js +60 -19
- package/dist/storage/index.js +53 -9
- package/dist/storage/sqlite.js +103 -11
- package/dist/storage/supabase.js +74 -5
- package/dist/storage/supabaseMigrations.js +30 -0
- package/dist/sync/factory.js +5 -1
- package/dist/tools/graphHandlers.js +24 -2
- package/dist/tools/ledgerHandlers.js +122 -4
- package/dist/utils/universalImporter.js +0 -0
- package/package.json +13 -3
- package/dist/dashboard/ui.tmp.js +0 -3475
- package/dist/test-cli.js +0 -18
- package/dist/tools/sessionMemoryHandlers.js +0 -2633
- package/dist/utils/embeddingApi.js +0 -104
- package/dist/utils/googleAi.js +0 -88
- package/dist/utils/testUniversalImporter.js +0 -10
- package/dist/verification/renameDetector.js +0 -170
package/dist/dashboard/ui.js
CHANGED
|
@@ -615,6 +615,9 @@ export function renderDashboardHTML(version) {
|
|
|
615
615
|
</div>
|
|
616
616
|
|
|
617
617
|
<div id="content" class="grid grid-main fade-in">
|
|
618
|
+
<div style="grid-column: 1 / -1; display: flex; align-items: center; gap: 1rem; margin-bottom: -1rem; padding: 0 1rem;">
|
|
619
|
+
<h1 id="activeProjectTitle" style="margin: 0; font-size: 2.2rem; color: var(--text-primary); text-shadow: 0 0 15px rgba(168, 85, 247, 0.4); font-weight: 700; letter-spacing: -0.02em;"></h1>
|
|
620
|
+
</div>
|
|
618
621
|
<!-- Left Column -->
|
|
619
622
|
<div class="grid" style="align-content: start;">
|
|
620
623
|
<!-- Current State -->
|
|
@@ -1048,12 +1051,70 @@ export function renderDashboardHTML(version) {
|
|
|
1048
1051
|
<div class="setting-label">Storage Backend</div>
|
|
1049
1052
|
<div class="setting-desc">Switch between SQLite and Supabase</div>
|
|
1050
1053
|
</div>
|
|
1051
|
-
<select id="storageBackendSelect" onchange="
|
|
1054
|
+
<select id="storageBackendSelect" onchange="onStorageProviderChange(this.value)" style="padding: 0.2rem 0.4rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); cursor: pointer;">
|
|
1052
1055
|
<option value="local">SQLite</option>
|
|
1053
1056
|
<option value="supabase">Supabase</option>
|
|
1054
1057
|
</select>
|
|
1055
1058
|
</div>
|
|
1056
1059
|
|
|
1060
|
+
<!-- Supabase fields -->
|
|
1061
|
+
<div id="provider-fields-supabase" style="display:none; padding: 1rem; background: rgba(139,92,246,0.05); border-radius: var(--radius-sm); border: 1px solid var(--border-glass); margin-top: 0.5rem;">
|
|
1062
|
+
<div class="setting-section" style="margin-top:0">Supabase Connection Setup</div>
|
|
1063
|
+
<div style="font-size:0.75rem; color:var(--text-muted); margin-bottom: 1rem; line-height: 1.5;">
|
|
1064
|
+
Configure your Supabase backend. This will run the migration schema to set up tables automatically before saving credentials.
|
|
1065
|
+
</div>
|
|
1066
|
+
|
|
1067
|
+
<div class="setting-row" style="border:none; padding:0.25rem 0">
|
|
1068
|
+
<div>
|
|
1069
|
+
<div class="setting-label">Supabase URL</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
<input type="text" id="input-supabase-url"
|
|
1072
|
+
placeholder="https://xyz.supabase.co"
|
|
1073
|
+
style="padding: 0.3rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;" />
|
|
1074
|
+
</div>
|
|
1075
|
+
<div class="setting-row" style="border:none; padding:0.25rem 0">
|
|
1076
|
+
<div>
|
|
1077
|
+
<div class="setting-label">Service Role Key</div>
|
|
1078
|
+
</div>
|
|
1079
|
+
<input type="password" id="input-supabase-key"
|
|
1080
|
+
placeholder="eyJh..."
|
|
1081
|
+
style="padding: 0.3rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;" />
|
|
1082
|
+
</div>
|
|
1083
|
+
<div class="setting-row" style="border:none; padding:0.25rem 0">
|
|
1084
|
+
<div>
|
|
1085
|
+
<div class="setting-label">Database Password</div>
|
|
1086
|
+
<div class="setting-desc">Needed once to bootstrap tables. Never saved.</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
<input type="password" id="input-supabase-dbpass"
|
|
1089
|
+
placeholder="••••••••"
|
|
1090
|
+
style="padding: 0.3rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;" />
|
|
1091
|
+
</div>
|
|
1092
|
+
|
|
1093
|
+
<div style="margin-top: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
1094
|
+
<button onclick="setupSupabase()" id="btn-setup-supabase" style="background: var(--accent-purple); color: white; border: none; padding: 0.5rem 1rem; border-radius: var(--radius-sm); font-size: 0.85rem; font-weight: 600; cursor: pointer; transition: opacity 0.2s;">
|
|
1095
|
+
Set Up & Migrate
|
|
1096
|
+
</button>
|
|
1097
|
+
<span id="setup-supabase-status" style="font-size: 0.8rem; font-weight: 500;"></span>
|
|
1098
|
+
</div>
|
|
1099
|
+
|
|
1100
|
+
<!-- Migration progress bar (hidden until setup starts) -->
|
|
1101
|
+
<div id="migration-progress-wrap" style="display:none; margin-top: 0.75rem;">
|
|
1102
|
+
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 0.35rem;">
|
|
1103
|
+
<span id="migration-step-label" style="font-size:0.75rem; color:var(--text-muted); font-weight:500;">Initializing…</span>
|
|
1104
|
+
<span id="migration-pct-label" style="font-size:0.75rem; color:var(--accent-purple); font-weight:700;">0%</span>
|
|
1105
|
+
</div>
|
|
1106
|
+
<div style="height:6px; border-radius:99px; background:var(--bg-hover); overflow:hidden;">
|
|
1107
|
+
<div id="migration-progress-bar"
|
|
1108
|
+
style="height:100%; width:0%; border-radius:99px;
|
|
1109
|
+
background: linear-gradient(90deg, #7c3aed, #a78bfa);
|
|
1110
|
+
transition: width 0.4s cubic-bezier(0.4,0,0.2,1);"></div>
|
|
1111
|
+
</div>
|
|
1112
|
+
<!-- Step dots -->
|
|
1113
|
+
<div id="migration-step-dots" style="display:flex; gap:0.35rem; margin-top:0.5rem; flex-wrap:wrap;"></div>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
1116
|
+
</div>
|
|
1117
|
+
|
|
1057
1118
|
<div class="setting-row" style="align-items:flex-start">
|
|
1058
1119
|
<div>
|
|
1059
1120
|
<div class="setting-label">Auto-Load Projects</div>
|
|
@@ -1810,6 +1871,7 @@ function loadProject() {
|
|
|
1810
1871
|
document.getElementById('welcome').style.display = 'none';
|
|
1811
1872
|
document.getElementById('content').style.display = 'none';
|
|
1812
1873
|
document.getElementById('loading').style.display = 'block';
|
|
1874
|
+
document.getElementById('activeProjectTitle').textContent = "📁 " + project;
|
|
1813
1875
|
_a.label = 1;
|
|
1814
1876
|
case 1:
|
|
1815
1877
|
_a.trys.push([1, 9, 10, 11]);
|
|
@@ -3196,6 +3258,146 @@ function onEmbeddingProviderChange(value) {
|
|
|
3196
3258
|
refreshAnthropicWarning(textVal, value);
|
|
3197
3259
|
saveBootSetting('embedding_provider', value);
|
|
3198
3260
|
}
|
|
3261
|
+
|
|
3262
|
+
function onStorageProviderChange(value) {
|
|
3263
|
+
var supFields = document.getElementById('provider-fields-supabase');
|
|
3264
|
+
if (supFields) supFields.style.display = value === 'supabase' ? '' : 'none';
|
|
3265
|
+
|
|
3266
|
+
// Only auto-save if switching to local. Supabase is saved via the migrate button.
|
|
3267
|
+
if (value === 'local') {
|
|
3268
|
+
saveBootSetting('PRISM_STORAGE', value);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
function setupSupabase() {
|
|
3273
|
+
var url = document.getElementById('input-supabase-url').value.trim();
|
|
3274
|
+
var serviceKey = document.getElementById('input-supabase-key').value.trim();
|
|
3275
|
+
var dbPassword = document.getElementById('input-supabase-dbpass').value.trim();
|
|
3276
|
+
var statusEl = document.getElementById('setup-supabase-status');
|
|
3277
|
+
var btn = document.getElementById('btn-setup-supabase');
|
|
3278
|
+
|
|
3279
|
+
if (!url || !serviceKey || !dbPassword) {
|
|
3280
|
+
statusEl.innerText = "All fields required.";
|
|
3281
|
+
statusEl.style.color = "var(--accent-rose)";
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
if (url.indexOf('https://') !== 0) {
|
|
3285
|
+
statusEl.innerText = "URL must start with https://";
|
|
3286
|
+
statusEl.style.color = "var(--accent-rose)";
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
if (serviceKey.indexOf('eyJ') !== 0) {
|
|
3290
|
+
statusEl.innerText = "Service key appears invalid. Expected JWT.";
|
|
3291
|
+
statusEl.style.color = "var(--accent-rose)";
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// Reset UI state
|
|
3296
|
+
statusEl.innerText = "Connecting & Migrating…";
|
|
3297
|
+
statusEl.style.color = "var(--text-muted)";
|
|
3298
|
+
btn.disabled = true;
|
|
3299
|
+
btn.style.opacity = "0.5";
|
|
3300
|
+
|
|
3301
|
+
// Show progress bar
|
|
3302
|
+
var progressWrap = document.getElementById('migration-progress-wrap');
|
|
3303
|
+
var progressBar = document.getElementById('migration-progress-bar');
|
|
3304
|
+
var stepLabel = document.getElementById('migration-step-label');
|
|
3305
|
+
var pctLabel = document.getElementById('migration-pct-label');
|
|
3306
|
+
var stepDots = document.getElementById('migration-step-dots');
|
|
3307
|
+
progressWrap.style.display = '';
|
|
3308
|
+
progressBar.style.width = '0%';
|
|
3309
|
+
stepDots.innerHTML = '';
|
|
3310
|
+
|
|
3311
|
+
// Helper: update bar
|
|
3312
|
+
function setProgress(pct, label, ok) {
|
|
3313
|
+
progressBar.style.width = pct + '%';
|
|
3314
|
+
if (ok === false) progressBar.style.background = 'linear-gradient(90deg,#dc2626,#f87171)';
|
|
3315
|
+
stepLabel.innerText = label;
|
|
3316
|
+
pctLabel.innerText = Math.round(pct) + '%';
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
// Helper: add step dot
|
|
3320
|
+
function addDot(label, state) {
|
|
3321
|
+
var dot = document.createElement('span');
|
|
3322
|
+
var colors = { pending: 'var(--text-muted)', ok: 'var(--accent-green)', err: 'var(--accent-rose)' };
|
|
3323
|
+
var icons = { pending: '○', ok: '✓', err: '×' };
|
|
3324
|
+
dot.title = label;
|
|
3325
|
+
dot.style.cssText = 'display:inline-flex;align-items:center;gap:2px;font-size:0.7rem;color:' + colors[state] + ';font-weight:600;';
|
|
3326
|
+
dot.innerText = icons[state] + ' ' + label;
|
|
3327
|
+
stepDots.appendChild(dot);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
setProgress(5, 'Connecting to Supabase…');
|
|
3331
|
+
|
|
3332
|
+
// Subscribe to server-sent progress events for this session
|
|
3333
|
+
var evtKey = Date.now().toString();
|
|
3334
|
+
var sse = new EventSource('/api/migration/progress?key=' + evtKey);
|
|
3335
|
+
var sseTimeout = setTimeout(function() {
|
|
3336
|
+
if (sse) { sse.close(); sse = null; }
|
|
3337
|
+
}, 120000); // 2-min safety timeout
|
|
3338
|
+
|
|
3339
|
+
if (sse) {
|
|
3340
|
+
sse.onmessage = function(e) {
|
|
3341
|
+
try {
|
|
3342
|
+
var msg = JSON.parse(e.data);
|
|
3343
|
+
// msg: { step, total, label, pct?, done?, error? }
|
|
3344
|
+
var pct = msg.pct !== undefined ? msg.pct : Math.round((msg.step / msg.total) * 100);
|
|
3345
|
+
if (msg.error) {
|
|
3346
|
+
setProgress(pct, '⚠ ' + msg.label, false);
|
|
3347
|
+
addDot(msg.label, 'err');
|
|
3348
|
+
} else if (msg.done) {
|
|
3349
|
+
setProgress(100, '✓ All migrations applied');
|
|
3350
|
+
addDot('Done', 'ok');
|
|
3351
|
+
if (sse) { sse.close(); sse = null; }
|
|
3352
|
+
clearTimeout(sseTimeout);
|
|
3353
|
+
} else {
|
|
3354
|
+
setProgress(pct, msg.label);
|
|
3355
|
+
if (msg.step > 1) addDot(msg.prevLabel || msg.label, 'ok');
|
|
3356
|
+
}
|
|
3357
|
+
} catch(_) {}
|
|
3358
|
+
};
|
|
3359
|
+
sse.onerror = function() {
|
|
3360
|
+
// SSE closed server-side after completion — normal
|
|
3361
|
+
if (sse) { sse.close(); sse = null; }
|
|
3362
|
+
clearTimeout(sseTimeout);
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
fetch('/api/setup-supabase', {
|
|
3367
|
+
method: 'POST',
|
|
3368
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3369
|
+
body: JSON.stringify({ url: url, serviceKey: serviceKey, dbPassword: dbPassword, progressKey: evtKey })
|
|
3370
|
+
})
|
|
3371
|
+
.then(function(res) {
|
|
3372
|
+
return res.json().then(function(data) {
|
|
3373
|
+
if (res.ok && data.ok) {
|
|
3374
|
+
setProgress(100, '✓ All done!');
|
|
3375
|
+
statusEl.innerText = "✓ Configured. Restart Prism.";
|
|
3376
|
+
statusEl.style.color = "var(--accent-green)";
|
|
3377
|
+
setTimeout(function() {
|
|
3378
|
+
alert("Supabase configured!\\n\\nRestart Prism MCP server for changes to take effect.");
|
|
3379
|
+
}, 300);
|
|
3380
|
+
} else {
|
|
3381
|
+
var errMsg = data.error || "Setup failed.";
|
|
3382
|
+
setProgress(progressBar ? parseFloat(progressBar.style.width) : 0, '⚠ ' + errMsg, false);
|
|
3383
|
+
statusEl.innerText = errMsg;
|
|
3384
|
+
statusEl.style.color = "var(--accent-rose)";
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
})
|
|
3388
|
+
.catch(function() {
|
|
3389
|
+
setProgress(0, 'Network error', false);
|
|
3390
|
+
statusEl.innerText = "Network error.";
|
|
3391
|
+
statusEl.style.color = "var(--accent-rose)";
|
|
3392
|
+
})
|
|
3393
|
+
.finally(function() {
|
|
3394
|
+
btn.disabled = false;
|
|
3395
|
+
btn.style.opacity = "1";
|
|
3396
|
+
if (sse) { sse.close(); sse = null; }
|
|
3397
|
+
clearTimeout(sseTimeout);
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3199
3401
|
// Shows/hides the Anthropic+auto warning.
|
|
3200
3402
|
// Warning appears when: text=anthropic AND embedding=auto (auto-bridges to Gemini).
|
|
3201
3403
|
function refreshAnthropicWarning(textVal, embedVal) {
|
|
@@ -3438,6 +3640,14 @@ function loadSettings() {
|
|
|
3438
3640
|
// Storage Backend
|
|
3439
3641
|
if (s.PRISM_STORAGE) {
|
|
3440
3642
|
document.getElementById('storageBackendSelect').value = s.PRISM_STORAGE;
|
|
3643
|
+
var supFields = document.getElementById('provider-fields-supabase');
|
|
3644
|
+
if (supFields) supFields.style.display = s.PRISM_STORAGE === 'supabase' ? '' : 'none';
|
|
3645
|
+
}
|
|
3646
|
+
if (s.SUPABASE_URL) {
|
|
3647
|
+
document.getElementById('input-supabase-url').value = s.SUPABASE_URL;
|
|
3648
|
+
}
|
|
3649
|
+
if (s.SUPABASE_SERVICE_KEY) {
|
|
3650
|
+
document.getElementById('input-supabase-key').placeholder = '(key saved — paste to update)';
|
|
3441
3651
|
}
|
|
3442
3652
|
// Agent Identity
|
|
3443
3653
|
if (s.default_role)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognitive Budget — Token-Economic RL (v9.0)
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Implements a strict token economy for agent memory operations.
|
|
7
|
+
* Instead of having infinite memory budgets, agents must learn to
|
|
8
|
+
* save high-signal, compressed entries — through physics, not prompts.
|
|
9
|
+
*
|
|
10
|
+
* ECONOMY DESIGN:
|
|
11
|
+
* - Budget is PERSISTENT (stored in session_handoffs.cognitive_budget)
|
|
12
|
+
* - Budget belongs to the PROJECT, not the ephemeral session
|
|
13
|
+
* - This prevents the "Reset Exploit" (close & reopen to get free tokens)
|
|
14
|
+
* - Revenue comes from Universal Basic Income (time-based) + success bonuses
|
|
15
|
+
* - No retrieval-based earning (prevents the "Minting Exploit" / search spam)
|
|
16
|
+
*
|
|
17
|
+
* COST MULTIPLIERS:
|
|
18
|
+
* Incoming entry surprisal determines the budget cost multiplier:
|
|
19
|
+
* - Low surprisal (boilerplate): 2.0× cost — penalizes "I updated CSS"
|
|
20
|
+
* - Normal surprisal: 1.0× cost — standard rate
|
|
21
|
+
* - High surprisal (novel): 0.5× cost — rewards novel insights
|
|
22
|
+
*
|
|
23
|
+
* GRACEFUL DEGRADATION:
|
|
24
|
+
* Budget exhaustion produces a WARNING in the MCP response but NEVER
|
|
25
|
+
* blocks the SQL insert. We never lose agent work due to verbosity.
|
|
26
|
+
*
|
|
27
|
+
* MINIMUM BASE COST:
|
|
28
|
+
* Empty/trivial summaries still bleed the budget (10 token minimum)
|
|
29
|
+
* to prevent zero-cost gaming with empty saves.
|
|
30
|
+
*
|
|
31
|
+
* UBI (UNIVERSAL BASIC INCOME):
|
|
32
|
+
* Instead of earning through arbitrary search spam, agents earn
|
|
33
|
+
* budget passively through time elapsed since last save:
|
|
34
|
+
* - +100 tokens per hour since last ledger save (capped at +500/session)
|
|
35
|
+
* - +200 bonus for a `success` experience event
|
|
36
|
+
* - +100 bonus for a `learning` experience event
|
|
37
|
+
*
|
|
38
|
+
* FILES THAT IMPORT THIS:
|
|
39
|
+
* - src/tools/ledgerHandlers.ts (budget tracking + diagnostics)
|
|
40
|
+
* - src/tools/ledgerHandlers.ts (budget persistence in handoff)
|
|
41
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
42
|
+
*/
|
|
43
|
+
// ─── Constants ────────────────────────────────────────────────
|
|
44
|
+
/** Default initial budget per project (tokens) */
|
|
45
|
+
export const DEFAULT_BUDGET_SIZE = 2000;
|
|
46
|
+
/** Minimum base cost per save operation (tokens) — prevents zero-cost gaming */
|
|
47
|
+
export const MINIMUM_BASE_COST = 10;
|
|
48
|
+
/** UBI: tokens earned per hour since last save */
|
|
49
|
+
export const UBI_TOKENS_PER_HOUR = 100;
|
|
50
|
+
/** UBI: maximum tokens earnable via time-based UBI per session */
|
|
51
|
+
export const UBI_MAX_PER_SESSION = 500;
|
|
52
|
+
/** Bonus tokens for saving a `success` experience event */
|
|
53
|
+
export const SUCCESS_BONUS = 200;
|
|
54
|
+
/** Bonus tokens for saving a `learning` experience event */
|
|
55
|
+
export const LEARNING_BONUS = 100;
|
|
56
|
+
/** Budget warning threshold (below this, show advisory) */
|
|
57
|
+
export const LOW_BUDGET_THRESHOLD = 300;
|
|
58
|
+
// ─── Cost Multipliers ────────────────────────────────────────
|
|
59
|
+
/** Surprisal thresholds for cost multiplier tiers */
|
|
60
|
+
export const BOILERPLATE_THRESHOLD = 0.2;
|
|
61
|
+
export const NOVEL_THRESHOLD = 0.7;
|
|
62
|
+
/**
|
|
63
|
+
* Compute the cost multiplier based on content surprisal.
|
|
64
|
+
*
|
|
65
|
+
* - Low surprisal (< 0.2): 2.0× — penalizes boilerplate
|
|
66
|
+
* - Normal surprisal (0.2 - 0.7): 1.0× — standard rate
|
|
67
|
+
* - High surprisal (> 0.7): 0.5× — rewards novel insights
|
|
68
|
+
*
|
|
69
|
+
* @param surprisal - Surprisal score in [0.0, 1.0]
|
|
70
|
+
* @returns Cost multiplier
|
|
71
|
+
*/
|
|
72
|
+
export function computeCostMultiplier(surprisal) {
|
|
73
|
+
if (!Number.isFinite(surprisal))
|
|
74
|
+
return 1.0;
|
|
75
|
+
if (surprisal < BOILERPLATE_THRESHOLD)
|
|
76
|
+
return 2.0;
|
|
77
|
+
if (surprisal > NOVEL_THRESHOLD)
|
|
78
|
+
return 0.5;
|
|
79
|
+
return 1.0;
|
|
80
|
+
}
|
|
81
|
+
// ─── Token Counting ───────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Estimate token count from text using the standard 1 token ≈ 4 chars.
|
|
84
|
+
* Enforces the minimum base cost to prevent zero-cost gaming.
|
|
85
|
+
*
|
|
86
|
+
* @param text - The text to estimate tokens for
|
|
87
|
+
* @returns Estimated token count (minimum: MINIMUM_BASE_COST)
|
|
88
|
+
*/
|
|
89
|
+
export function estimateTokens(text) {
|
|
90
|
+
if (!text || text.trim().length === 0)
|
|
91
|
+
return MINIMUM_BASE_COST;
|
|
92
|
+
return Math.max(MINIMUM_BASE_COST, Math.ceil(text.length / 4));
|
|
93
|
+
}
|
|
94
|
+
// ─── UBI Calculator ───────────────────────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* Compute Universal Basic Income tokens earned since last save.
|
|
97
|
+
*
|
|
98
|
+
* @param lastSaveTime - ISO timestamp of last ledger save (or null if first save)
|
|
99
|
+
* @param currentTime - Current time (default: now)
|
|
100
|
+
* @returns Tokens earned via UBI (capped at UBI_MAX_PER_SESSION)
|
|
101
|
+
*/
|
|
102
|
+
export function computeUBI(lastSaveTime, currentTime = new Date()) {
|
|
103
|
+
if (!lastSaveTime)
|
|
104
|
+
return 0; // First save — no UBI
|
|
105
|
+
const lastSave = new Date(lastSaveTime);
|
|
106
|
+
if (isNaN(lastSave.getTime()))
|
|
107
|
+
return 0;
|
|
108
|
+
const hoursSinceLastSave = (currentTime.getTime() - lastSave.getTime()) / (1000 * 60 * 60);
|
|
109
|
+
if (hoursSinceLastSave <= 0)
|
|
110
|
+
return 0;
|
|
111
|
+
// NOTE: Do NOT use Math.floor here — it destroys fractional earnings.
|
|
112
|
+
// An agent saving every 15 min computes floor(0.25 * 100) = 0 tokens.
|
|
113
|
+
// Since cognitive_budget is REAL (SQLite) / float8 (Postgres), fractional
|
|
114
|
+
// values are natively supported. Only round at the UI display layer.
|
|
115
|
+
const earned = hoursSinceLastSave * UBI_TOKENS_PER_HOUR;
|
|
116
|
+
return Math.min(earned, UBI_MAX_PER_SESSION);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compute bonus tokens for specific experience event types.
|
|
120
|
+
*
|
|
121
|
+
* @param eventType - The experience event type
|
|
122
|
+
* @returns Bonus tokens to add to budget
|
|
123
|
+
*/
|
|
124
|
+
export function computeEventBonus(eventType) {
|
|
125
|
+
switch (eventType) {
|
|
126
|
+
case 'success': return SUCCESS_BONUS;
|
|
127
|
+
case 'learning': return LEARNING_BONUS;
|
|
128
|
+
default: return 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ─── Budget Manager ───────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Stateless budget operations.
|
|
134
|
+
*
|
|
135
|
+
* The budget is stored as a number in session_handoffs.cognitive_budget.
|
|
136
|
+
* These functions compute the new balance — they don't persist anything.
|
|
137
|
+
* The caller (ledgerHandlers.ts) is responsible for persistence.
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* Process a budget spend operation.
|
|
141
|
+
*
|
|
142
|
+
* @param currentBalance - Current budget balance
|
|
143
|
+
* @param rawTokenCost - Raw token cost of the entry
|
|
144
|
+
* @param surprisal - Surprisal score of the content [0, 1]
|
|
145
|
+
* @param budgetSize - Initial budget size (for diagnostics)
|
|
146
|
+
* @returns BudgetResult with new balance, warnings, and diagnostics
|
|
147
|
+
*/
|
|
148
|
+
export function spendBudget(currentBalance, rawTokenCost, surprisal, budgetSize = DEFAULT_BUDGET_SIZE) {
|
|
149
|
+
const safeCost = Math.max(MINIMUM_BASE_COST, rawTokenCost);
|
|
150
|
+
const multiplier = computeCostMultiplier(surprisal);
|
|
151
|
+
const adjustedCost = Math.ceil(safeCost * multiplier);
|
|
152
|
+
const newBalance = currentBalance - adjustedCost;
|
|
153
|
+
const remaining = Math.max(0, newBalance);
|
|
154
|
+
let warning;
|
|
155
|
+
if (newBalance <= 0) {
|
|
156
|
+
warning = `⚠️ Cognitive budget exhausted (${remaining}/${budgetSize} tokens). ` +
|
|
157
|
+
'Consider saving more concise, high-signal entries. ' +
|
|
158
|
+
'Budget recovers passively over time (+100 tokens/hour).';
|
|
159
|
+
}
|
|
160
|
+
else if (newBalance < LOW_BUDGET_THRESHOLD) {
|
|
161
|
+
warning = `⚡ Cognitive budget running low (${remaining}/${budgetSize} tokens). ` +
|
|
162
|
+
'Prioritize novel, dense entries to reduce cost.';
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
allowed: true, // Always allow — graceful degradation
|
|
166
|
+
spent: adjustedCost,
|
|
167
|
+
remaining,
|
|
168
|
+
warning,
|
|
169
|
+
surprisal,
|
|
170
|
+
costMultiplier: multiplier,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Apply Universal Basic Income + event bonuses to a budget balance.
|
|
175
|
+
*
|
|
176
|
+
* @param currentBalance - Current budget balance
|
|
177
|
+
* @param lastSaveTime - ISO timestamp of last save
|
|
178
|
+
* @param eventType - Optional event type for bonus
|
|
179
|
+
* @param budgetSize - Maximum budget cap
|
|
180
|
+
* @returns New balance after UBI + bonuses (capped at budgetSize)
|
|
181
|
+
*/
|
|
182
|
+
export function applyEarnings(currentBalance, lastSaveTime, eventType, budgetSize = DEFAULT_BUDGET_SIZE) {
|
|
183
|
+
const ubiEarned = computeUBI(lastSaveTime);
|
|
184
|
+
const bonusEarned = computeEventBonus(eventType);
|
|
185
|
+
// Cap at initial budget size — can't exceed maximum
|
|
186
|
+
const newBalance = Math.min(budgetSize, currentBalance + ubiEarned + bonusEarned);
|
|
187
|
+
return { newBalance, ubiEarned, bonusEarned };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Format budget diagnostics for inclusion in MCP response text.
|
|
191
|
+
*
|
|
192
|
+
* @param result - The BudgetResult from spendBudget()
|
|
193
|
+
* @param budgetSize - Initial budget size
|
|
194
|
+
* @param ubiEarned - Tokens earned from UBI this operation
|
|
195
|
+
* @param bonusEarned - Tokens earned from event bonus
|
|
196
|
+
* @returns Formatted diagnostic string
|
|
197
|
+
*/
|
|
198
|
+
export function formatBudgetDiagnostics(result, budgetSize = DEFAULT_BUDGET_SIZE, ubiEarned = 0, bonusEarned = 0) {
|
|
199
|
+
const parts = [];
|
|
200
|
+
// Budget line
|
|
201
|
+
const barLength = 20;
|
|
202
|
+
const fillLength = Math.round((result.remaining / budgetSize) * barLength);
|
|
203
|
+
const bar = '█'.repeat(Math.max(0, fillLength)) + '░'.repeat(Math.max(0, barLength - fillLength));
|
|
204
|
+
parts.push(`💰 Budget: ${bar} ${result.remaining}/${budgetSize}`);
|
|
205
|
+
// Surprisal line
|
|
206
|
+
if (result.surprisal !== undefined) {
|
|
207
|
+
const surprisalLabel = result.surprisal < BOILERPLATE_THRESHOLD ? 'boilerplate'
|
|
208
|
+
: result.surprisal > NOVEL_THRESHOLD ? 'novel'
|
|
209
|
+
: 'standard';
|
|
210
|
+
parts.push(`📊 Surprisal: ${result.surprisal.toFixed(2)} (${surprisalLabel}) — cost: ${result.costMultiplier?.toFixed(1)}×`);
|
|
211
|
+
}
|
|
212
|
+
// Cost line
|
|
213
|
+
parts.push(`🪙 Spent: ${result.spent} tokens`);
|
|
214
|
+
// Earnings line (if any)
|
|
215
|
+
if (ubiEarned > 0 || bonusEarned > 0) {
|
|
216
|
+
const earningParts = [];
|
|
217
|
+
if (ubiEarned > 0)
|
|
218
|
+
earningParts.push(`+${Math.round(ubiEarned)} UBI`);
|
|
219
|
+
if (bonusEarned > 0)
|
|
220
|
+
earningParts.push(`+${Math.round(bonusEarned)} bonus`);
|
|
221
|
+
parts.push(`📈 Earned: ${earningParts.join(', ')}`);
|
|
222
|
+
}
|
|
223
|
+
return parts.join('\n');
|
|
224
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Surprisal Gate — Vector-Based Novelty Scoring (v9.0)
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Computes the information-theoretic "surprisal" of an incoming
|
|
7
|
+
* memory entry by measuring its semantic distance from recent entries.
|
|
8
|
+
*
|
|
9
|
+
* WHY NOT TF-IDF:
|
|
10
|
+
* A naive TF-IDF approach would require downloading all summaries
|
|
11
|
+
* into V8 memory and running a custom JS tokenizer. On projects with
|
|
12
|
+
* 10K+ entries (common after Universal Import), this blocks the
|
|
13
|
+
* Node.js event loop for seconds, causing MCP handshake timeouts.
|
|
14
|
+
*
|
|
15
|
+
* VECTOR-BASED SURPRISAL:
|
|
16
|
+
* Surprisal = 1 - max_similarity_to_recent_entries
|
|
17
|
+
*
|
|
18
|
+
* When the agent tries to save an entry, we embed the summary
|
|
19
|
+
* (already happening in the save flow) and query the DB for the
|
|
20
|
+
* single most similar entry from the last 7 days.
|
|
21
|
+
*
|
|
22
|
+
* - Similarity 0.95 → Surprisal 0.05 → "You're repeating yourself" → 2× cost
|
|
23
|
+
* - Similarity 0.40 → Surprisal 0.60 → "Completely novel thought" → 0.5× cost
|
|
24
|
+
*
|
|
25
|
+
* This uses the existing native sqlite-vec index, takes < 5ms,
|
|
26
|
+
* uses zero extra memory, and is far more accurate than word counting.
|
|
27
|
+
*
|
|
28
|
+
* FILES THAT IMPORT THIS:
|
|
29
|
+
* - src/tools/ledgerHandlers.ts (surprisal computation during save)
|
|
30
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
31
|
+
*/
|
|
32
|
+
import { debugLog } from "../utils/logger.js";
|
|
33
|
+
// ─── Constants ────────────────────────────────────────────────
|
|
34
|
+
/** Maximum age of entries to compare against (days) */
|
|
35
|
+
export const RECENCY_WINDOW_DAYS = 7;
|
|
36
|
+
/** Number of similar entries to fetch for comparison */
|
|
37
|
+
export const TOP_K = 1;
|
|
38
|
+
/** Similarity above which content is considered boilerplate */
|
|
39
|
+
export const BOILERPLATE_SIMILARITY = 0.80;
|
|
40
|
+
/** Similarity below which content is considered novel */
|
|
41
|
+
export const NOVEL_SIMILARITY = 0.30;
|
|
42
|
+
// ─── Core Computation ─────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Compute surprisal from a semantic similarity score.
|
|
45
|
+
*
|
|
46
|
+
* This is the pure math core — no I/O. The caller is responsible
|
|
47
|
+
* for running the actual vector search to find maxSimilarity.
|
|
48
|
+
*
|
|
49
|
+
* @param maxSimilarity - Cosine similarity to the most similar recent entry (0-1)
|
|
50
|
+
* @returns SurprisalResult with classification
|
|
51
|
+
*/
|
|
52
|
+
export function computeSurprisal(maxSimilarity) {
|
|
53
|
+
// Guard: no recent entries found (first entry in project) → maximum novelty
|
|
54
|
+
if (!Number.isFinite(maxSimilarity) || maxSimilarity < 0) {
|
|
55
|
+
return {
|
|
56
|
+
surprisal: 1.0,
|
|
57
|
+
maxSimilarity: 0.0,
|
|
58
|
+
isBoilerplate: false,
|
|
59
|
+
isNovel: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Clamp to [0, 1]
|
|
63
|
+
const clamped = Math.min(1.0, Math.max(0.0, maxSimilarity));
|
|
64
|
+
const surprisal = 1.0 - clamped;
|
|
65
|
+
return {
|
|
66
|
+
surprisal,
|
|
67
|
+
maxSimilarity: clamped,
|
|
68
|
+
isBoilerplate: clamped >= BOILERPLATE_SIMILARITY,
|
|
69
|
+
isNovel: clamped <= NOVEL_SIMILARITY,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Compute surprisal using the existing storage backend's vector search.
|
|
74
|
+
*
|
|
75
|
+
* This is the integration wrapper. It:
|
|
76
|
+
* 1. Takes the query embedding (already generated for the save flow)
|
|
77
|
+
* 2. Finds the most similar recent entry via sqlite-vec
|
|
78
|
+
* 3. Computes surprisal = 1 - max_similarity
|
|
79
|
+
*
|
|
80
|
+
* Falls back to surprisal=0.5 (neutral) on any error, to avoid
|
|
81
|
+
* blocking saves due to search failures.
|
|
82
|
+
*
|
|
83
|
+
* @param searchFn - The storage backend's searchMemory function
|
|
84
|
+
* @param queryEmbedding - JSON-stringified embedding of the new entry
|
|
85
|
+
* @param project - Project scope
|
|
86
|
+
* @param userId - Tenant ID
|
|
87
|
+
* @returns SurprisalResult
|
|
88
|
+
*/
|
|
89
|
+
export async function computeVectorSurprisal(searchFn, queryEmbedding, project, userId) {
|
|
90
|
+
try {
|
|
91
|
+
// Search for the single most similar recent entry
|
|
92
|
+
// Using a very low threshold (0.0) to get the closest match regardless
|
|
93
|
+
const results = await searchFn({
|
|
94
|
+
queryEmbedding,
|
|
95
|
+
project,
|
|
96
|
+
limit: TOP_K,
|
|
97
|
+
similarityThreshold: 0.0, // Get closest match regardless of distance
|
|
98
|
+
userId,
|
|
99
|
+
});
|
|
100
|
+
if (results.length === 0) {
|
|
101
|
+
// No existing entries → fully novel
|
|
102
|
+
debugLog('[surprisal] No recent entries found — maximum novelty');
|
|
103
|
+
return computeSurprisal(-1);
|
|
104
|
+
}
|
|
105
|
+
const maxSimilarity = results[0].similarity;
|
|
106
|
+
debugLog(`[surprisal] Max similarity to recent entries: ${maxSimilarity.toFixed(3)}`);
|
|
107
|
+
return computeSurprisal(maxSimilarity);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// Non-fatal: fall back to neutral surprisal
|
|
111
|
+
debugLog(`[surprisal] Vector search failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
return {
|
|
113
|
+
surprisal: 0.5,
|
|
114
|
+
maxSimilarity: 0.5,
|
|
115
|
+
isBoilerplate: false,
|
|
116
|
+
isNovel: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -107,6 +107,11 @@ export async function propagateActivation(anchors, linkFetcher, config = {}) {
|
|
|
107
107
|
const hopDistance = new Map();
|
|
108
108
|
// Track visited edges to prevent cyclic re-traversal
|
|
109
109
|
const visitedEdges = new Set();
|
|
110
|
+
// v9.0: Track energy flow weights for valence propagation
|
|
111
|
+
// Maps targetId → Array<{ sourceId, weight }> representing which nodes
|
|
112
|
+
// contributed energy and how much. Used by propagateValence() to compute
|
|
113
|
+
// energy-weighted average valence for discovered nodes.
|
|
114
|
+
const flowWeights = new Map();
|
|
110
115
|
// Total edges traversed for telemetry
|
|
111
116
|
let totalEdgesTraversed = 0;
|
|
112
117
|
// Initialize with anchor scores
|
|
@@ -120,6 +125,7 @@ export async function propagateActivation(anchors, linkFetcher, config = {}) {
|
|
|
120
125
|
return {
|
|
121
126
|
results,
|
|
122
127
|
telemetry: buildTelemetry(results, anchors, 0, 0, startMs),
|
|
128
|
+
flowWeights: new Map(),
|
|
123
129
|
};
|
|
124
130
|
}
|
|
125
131
|
// ─── Propagation Loop ────────────────────────────────────
|
|
@@ -138,13 +144,20 @@ export async function propagateActivation(anchors, linkFetcher, config = {}) {
|
|
|
138
144
|
break;
|
|
139
145
|
}
|
|
140
146
|
totalEdgesTraversed += edges.length;
|
|
141
|
-
// Compute out-degree per active source node (for fan effect)
|
|
147
|
+
// Compute out-degree per active source node (for forward fan effect)
|
|
142
148
|
const outDegree = new Map();
|
|
143
149
|
for (const edge of edges) {
|
|
144
150
|
if (activeNodes.has(edge.source_id)) {
|
|
145
151
|
outDegree.set(edge.source_id, (outDegree.get(edge.source_id) || 0) + 1);
|
|
146
152
|
}
|
|
147
153
|
}
|
|
154
|
+
// Compute in-degree per active target node (for backward fan effect)
|
|
155
|
+
const inDegree = new Map();
|
|
156
|
+
for (const edge of edges) {
|
|
157
|
+
if (activeNodes.has(edge.target_id)) {
|
|
158
|
+
inDegree.set(edge.target_id, (inDegree.get(edge.target_id) || 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
148
161
|
// Next-iteration activation: starts with current values (activation persists)
|
|
149
162
|
const nextNodes = new Map(activeNodes);
|
|
150
163
|
for (const edge of edges) {
|
|
@@ -159,6 +172,10 @@ export async function propagateActivation(anchors, linkFetcher, config = {}) {
|
|
|
159
172
|
const dampedFan = Math.log(degree + Math.E);
|
|
160
173
|
const flow = cfg.spreadFactor * (strength * sourceEnergy / dampedFan);
|
|
161
174
|
nextNodes.set(edge.target_id, (nextNodes.get(edge.target_id) || 0) + flow);
|
|
175
|
+
// v9.0: Record flow for valence propagation
|
|
176
|
+
if (!flowWeights.has(edge.target_id))
|
|
177
|
+
flowWeights.set(edge.target_id, []);
|
|
178
|
+
flowWeights.get(edge.target_id).push({ sourceId: edge.source_id, weight: flow });
|
|
162
179
|
// Track hop distance (minimum)
|
|
163
180
|
const sourceHops = hopDistance.get(edge.source_id) ?? 0;
|
|
164
181
|
const currentTargetHops = hopDistance.get(edge.target_id);
|
|
@@ -171,8 +188,16 @@ export async function propagateActivation(anchors, linkFetcher, config = {}) {
|
|
|
171
188
|
if (activeNodes.has(edge.target_id) && !visitedEdges.has(reverseEdgeKey)) {
|
|
172
189
|
visitedEdges.add(reverseEdgeKey);
|
|
173
190
|
const targetEnergy = activeNodes.get(edge.target_id);
|
|
174
|
-
|
|
191
|
+
// Dampened fan effect for backward flow: prevents hub nodes with many
|
|
192
|
+
// inbound edges from blasting energy to all sources equally.
|
|
193
|
+
const inDegreeCount = inDegree.get(edge.target_id) || 1;
|
|
194
|
+
const dampedFanBack = Math.log(inDegreeCount + Math.E);
|
|
195
|
+
const flow = (cfg.spreadFactor * 0.5) * (strength * targetEnergy / dampedFanBack);
|
|
175
196
|
nextNodes.set(edge.source_id, (nextNodes.get(edge.source_id) || 0) + flow);
|
|
197
|
+
// v9.0: Record backward flow for valence propagation
|
|
198
|
+
if (!flowWeights.has(edge.source_id))
|
|
199
|
+
flowWeights.set(edge.source_id, []);
|
|
200
|
+
flowWeights.get(edge.source_id).push({ sourceId: edge.target_id, weight: flow });
|
|
176
201
|
// Track hop distance for backward discoveries
|
|
177
202
|
const targetHops = hopDistance.get(edge.target_id) ?? 0;
|
|
178
203
|
const currentSourceHops = hopDistance.get(edge.source_id);
|
|
@@ -190,6 +215,7 @@ export async function propagateActivation(anchors, linkFetcher, config = {}) {
|
|
|
190
215
|
return {
|
|
191
216
|
results,
|
|
192
217
|
telemetry: buildTelemetry(results, anchors, cfg.iterations, totalEdgesTraversed, startMs),
|
|
218
|
+
flowWeights,
|
|
193
219
|
};
|
|
194
220
|
}
|
|
195
221
|
// ─── Helpers ──────────────────────────────────────────────────
|