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.
@@ -21,7 +21,7 @@ import * as path from "path";
21
21
  import * as os from "os";
22
22
  import { randomUUID } from "crypto";
23
23
  import { AccessLogBuffer } from "../utils/accessLogBuffer.js";
24
- import { PRISM_ACTR_BUFFER_FLUSH_MS, PRISM_SYNAPSE_ENABLED, PRISM_SYNAPSE_ITERATIONS, PRISM_SYNAPSE_SPREAD_FACTOR, PRISM_SYNAPSE_LATERAL_INHIBITION, PRISM_SYNAPSE_SOFT_CAP, } from "../config.js";
24
+ import { PRISM_ACTR_BUFFER_FLUSH_MS, PRISM_SYNAPSE_ENABLED, PRISM_SYNAPSE_ITERATIONS, PRISM_SYNAPSE_SPREAD_FACTOR, PRISM_SYNAPSE_LATERAL_INHIBITION, PRISM_SYNAPSE_SOFT_CAP, PRISM_VALENCE_ENABLED, } from "../config.js";
25
25
  import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
26
26
  import { debugLog } from "../utils/logger.js";
27
27
  import { SafetyController } from "../darkfactory/safetyController.js";
@@ -108,7 +108,8 @@ export class SqliteStorage {
108
108
  rollup_count INTEGER DEFAULT 0,
109
109
  archived_at TEXT DEFAULT NULL,
110
110
  session_date TEXT DEFAULT NULL,
111
- created_at TEXT DEFAULT (datetime('now'))
111
+ created_at TEXT DEFAULT (datetime('now')),
112
+ valence REAL DEFAULT NULL
112
113
  );
113
114
 
114
115
  -- ─── Session Handoffs (live project state, OCC-controlled) ───
@@ -123,6 +124,7 @@ export class SqliteStorage {
123
124
  key_context TEXT DEFAULT NULL,
124
125
  active_branch TEXT DEFAULT NULL,
125
126
  version INTEGER NOT NULL DEFAULT 1,
127
+ cognitive_budget REAL DEFAULT NULL,
126
128
  metadata TEXT DEFAULT '{}',
127
129
  created_at TEXT DEFAULT (datetime('now')),
128
130
  updated_at TEXT DEFAULT (datetime('now')),
@@ -166,7 +168,7 @@ export class SqliteStorage {
166
168
  VALUES ('delete', old.rowid, old.project, old.summary, old.decisions, old.keywords);
167
169
  END;
168
170
 
169
- CREATE TRIGGER IF NOT EXISTS ledger_fts_update AFTER UPDATE ON session_ledger BEGIN
171
+ CREATE TRIGGER IF NOT EXISTS ledger_fts_update AFTER UPDATE OF project, summary, decisions, keywords ON session_ledger BEGIN
170
172
  INSERT INTO ledger_fts(ledger_fts, rowid, project, summary, decisions, keywords)
171
173
  VALUES ('delete', old.rowid, old.project, old.summary, old.decisions, old.keywords);
172
174
  INSERT INTO ledger_fts(rowid, project, summary, decisions, keywords)
@@ -725,6 +727,36 @@ export class SqliteStorage {
725
727
  // Non-fatal: some older libSQL versions may not support all integrity_check modes.
726
728
  debugLog(`[SqliteStorage] v6.1: integrity_check skipped (${e.message})`);
727
729
  }
730
+ // ─── v9.0 Migration: Affect-Tagged Memory (Valence) ───────────
731
+ //
732
+ // Adds a REAL valence column to session_ledger for affect-tagged memory.
733
+ // Valence is auto-derived from event_type at save time.
734
+ // Uses Affective Salience: |valence| boosts retrieval, sign drives UX warnings.
735
+ // For fresh DBs, valence is already in the CREATE TABLE. This ALTER TABLE
736
+ // is purely for existing production databases upgrading from v8.x → v9.0.
737
+ try {
738
+ await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN valence REAL DEFAULT NULL`);
739
+ debugLog("[SqliteStorage] v9.0 migration: added valence column");
740
+ }
741
+ catch (e) {
742
+ if (!e.message?.includes("duplicate column name"))
743
+ throw e;
744
+ }
745
+ // Partial index for valence-aware queries (schema parity with Supabase)
746
+ await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_valence ON session_ledger(valence) WHERE valence IS NOT NULL`);
747
+ // ─── v9.0 Migration: Persistent Cognitive Budget ──────────────
748
+ //
749
+ // Budget belongs to the PROJECT (stored in session_handoffs), not
750
+ // the ephemeral session, to prevent the "Reset Exploit" where an
751
+ // agent escapes budget exhaustion by simply starting a new session.
752
+ try {
753
+ await this.db.execute(`ALTER TABLE session_handoffs ADD COLUMN cognitive_budget REAL DEFAULT NULL`);
754
+ debugLog("[SqliteStorage] v9.0 migration: added cognitive_budget column");
755
+ }
756
+ catch (e) {
757
+ if (!e.message?.includes("duplicate column name"))
758
+ throw e;
759
+ }
728
760
  }
729
761
  // ─── PostgREST Filter Parser ───────────────────────────────
730
762
  //
@@ -896,10 +928,10 @@ export class SqliteStorage {
896
928
  sql: `INSERT INTO session_ledger
897
929
  (id, project, conversation_id, user_id, role, summary, todos, files_changed,
898
930
  decisions, keywords, is_rollup, rollup_count, title, agent_name,
899
- event_type, confidence_score, importance,
931
+ event_type, confidence_score, importance, valence,
900
932
  embedding_compressed, embedding_format, embedding_turbo_radius,
901
933
  created_at, session_date)
902
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
934
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
903
935
  args: [
904
936
  id,
905
937
  entry.project,
@@ -918,6 +950,7 @@ export class SqliteStorage {
918
950
  entry.event_type || "session", // v4.0: default to 'session'
919
951
  entry.confidence_score ?? null, // v4.0: nullable
920
952
  entry.importance || 0, // v4.0: default to 0
953
+ entry.valence ?? null, // v9.0: affect-tagged memory
921
954
  entry.embedding_compressed || null, // v5.0: TurboQuant
922
955
  entry.embedding_format || null, // v5.0: turbo3/turbo4/float32
923
956
  entry.embedding_turbo_radius ?? null, // v5.0: original vector magnitude
@@ -950,7 +983,7 @@ export class SqliteStorage {
950
983
  'embedding', 'embedding_compressed', 'embedding_format', 'embedding_turbo_radius',
951
984
  'archived_at', 'deleted_at', 'deleted_reason', 'is_rollup', 'rollup_count',
952
985
  'importance', 'last_accessed_at', 'keywords', 'todos', 'files_changed', 'decisions',
953
- 'summary', 'confidence_score', 'event_type', 'role',
986
+ 'summary', 'confidence_score', 'event_type', 'role', 'valence',
954
987
  ]);
955
988
  const sets = [];
956
989
  const args = [];
@@ -978,6 +1011,18 @@ export class SqliteStorage {
978
1011
  args,
979
1012
  });
980
1013
  }
1014
+ // ─── v9.0: Atomic delta-based budget persistence ────────────────
1015
+ // Uses COALESCE + delta to prevent concurrency race conditions:
1016
+ // Agent A loads budget=2000, spends 100 → delta=-100
1017
+ // Agent B loads budget=2000, spends 50 → delta=-50
1018
+ // With absolute writes: Agent B overwrites Agent A's spend (budget=1950)
1019
+ // With delta: Both apply correctly → budget=2000 + (-100) + (-50) = 1850
1020
+ async patchHandoffBudgetDelta(project, userId, budgetDelta) {
1021
+ await this.db.execute({
1022
+ sql: `UPDATE session_handoffs SET cognitive_budget = MAX(0, COALESCE(cognitive_budget, 2000) + ?) WHERE project = ? AND user_id = ?`,
1023
+ args: [budgetDelta, project, userId],
1024
+ });
1025
+ }
981
1026
  async getLedgerEntries(params) {
982
1027
  const { ids, ...restParams } = params;
983
1028
  const { where, args, select, order, limit } = this.parsePostgRESTFilters(restParams);
@@ -1492,6 +1537,7 @@ export class SqliteStorage {
1492
1537
  const sql = `
1493
1538
  SELECT l.id, l.project, l.summary, l.decisions, l.files_changed,
1494
1539
  l.session_date, l.created_at, l.is_rollup, l.importance, l.last_accessed_at,
1540
+ l.valence,
1495
1541
  (1 - vector_distance_cos(l.embedding, vector(?))) AS similarity
1496
1542
  FROM session_ledger l
1497
1543
  WHERE ${conditions.join(" AND ")}
@@ -1513,6 +1559,7 @@ export class SqliteStorage {
1513
1559
  is_rollup: Boolean(r.is_rollup),
1514
1560
  importance: r.importance ?? 0,
1515
1561
  last_accessed_at: r.last_accessed_at || null,
1562
+ valence: r.valence ?? null, // v9.0: affect-tagged memory
1516
1563
  }));
1517
1564
  if (params.activation?.enabled) {
1518
1565
  return this.applySynapse(baseResults, params.activation, params.userId);
@@ -1634,7 +1681,7 @@ export class SqliteStorage {
1634
1681
  const anchorMap = new Map();
1635
1682
  for (const a of anchors)
1636
1683
  anchorMap.set(a.id, a.similarity ?? 1.0);
1637
- const { results, telemetry } = await propagateActivation(anchorMap, async (nodeIds) => this.getLinksForNodes(nodeIds, userId), {
1684
+ const { results, telemetry, flowWeights } = await propagateActivation(anchorMap, async (nodeIds) => this.getLinksForNodes(nodeIds, userId), {
1638
1685
  iterations: options.iterations ?? PRISM_SYNAPSE_ITERATIONS,
1639
1686
  spreadFactor: options.spreadFactor ?? PRISM_SYNAPSE_SPREAD_FACTOR,
1640
1687
  lateralInhibition: options.lateralInhibition ?? PRISM_SYNAPSE_LATERAL_INHIBITION,
@@ -1649,7 +1696,7 @@ export class SqliteStorage {
1649
1696
  if (missingIds.length > 0) {
1650
1697
  const placeholders = missingIds.map(() => '?').join(',');
1651
1698
  const missingQuery = `
1652
- SELECT id, project, summary, session_date, decisions, files_changed, keywords, is_rollup, importance, last_accessed_at
1699
+ SELECT id, project, summary, session_date, decisions, files_changed, keywords, is_rollup, importance, last_accessed_at, valence
1653
1700
  FROM session_ledger
1654
1701
  WHERE id IN (${placeholders}) AND deleted_at IS NULL AND user_id = ?
1655
1702
  `;
@@ -1666,9 +1713,43 @@ export class SqliteStorage {
1666
1713
  importance: Number(row.importance) || 0,
1667
1714
  last_accessed_at: row.last_accessed_at || null,
1668
1715
  similarity: 0.0,
1716
+ valence: row.valence ?? null,
1669
1717
  });
1670
1718
  }
1671
1719
  }
1720
+ // ── v9.0: Valence Propagation ────────────────────────────────
1721
+ // After activation propagation, compute propagated valence for
1722
+ // discovered nodes using energy-weighted averaging from source flows.
1723
+ let propagatedValenceMap = null;
1724
+ if (PRISM_VALENCE_ENABLED) {
1725
+ try {
1726
+ const { propagateValence } = await import("../memory/valenceEngine.js");
1727
+ // Build valence lookup from all known nodes
1728
+ const valenceLookup = new Map();
1729
+ for (const [id, node] of fullNodeMap) {
1730
+ if (node.valence != null)
1731
+ valenceLookup.set(id, node.valence);
1732
+ }
1733
+ // For missing valence values, bulk-fetch from DB
1734
+ const missingValenceIds = finalIds.filter(id => !valenceLookup.has(id));
1735
+ if (missingValenceIds.length > 0) {
1736
+ const vPlaceholders = missingValenceIds.map(() => '?').join(',');
1737
+ const vQuery = `SELECT id, valence FROM session_ledger WHERE id IN (${vPlaceholders}) AND valence IS NOT NULL`;
1738
+ const vRes = await this.db.execute({ sql: vQuery, args: missingValenceIds });
1739
+ for (const row of vRes.rows) {
1740
+ valenceLookup.set(row.id, row.valence);
1741
+ }
1742
+ }
1743
+ propagatedValenceMap = propagateValence(results, valenceLookup, flowWeights);
1744
+ debugLog(`[SqliteStorage] v9.0 valence propagation: ${propagatedValenceMap.size} nodes processed`);
1745
+ }
1746
+ catch (valErr) {
1747
+ debugLog(`[SqliteStorage] v9.0 valence propagation failed (non-fatal): ${valErr instanceof Error ? valErr.message : String(valErr)}`);
1748
+ }
1749
+ }
1750
+ const { computeHybridScoreWithValence } = PRISM_VALENCE_ENABLED
1751
+ ? await import("../memory/valenceEngine.js")
1752
+ : { computeHybridScoreWithValence: null };
1672
1753
  const finalResults = [];
1673
1754
  for (const r of results) {
1674
1755
  if (fullNodeMap.has(r.id)) {
@@ -1677,8 +1758,19 @@ export class SqliteStorage {
1677
1758
  node.activationScore = normEnergy;
1678
1759
  node.rawActivationEnergy = r.activationEnergy;
1679
1760
  node.isDiscovered = r.isDiscovered;
1680
- // Hybrid blend: 70% original match relevance, 30% structural energy
1681
- node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
1761
+ // v9.0: Attach propagated valence (overrides raw for discovered nodes)
1762
+ if (propagatedValenceMap?.has(r.id)) {
1763
+ node.valence = propagatedValenceMap.get(r.id);
1764
+ }
1765
+ // v9.0: Hybrid blend with valence salience:
1766
+ // 0.65 × similarity + 0.25 × activation + 0.10 × |valence|
1767
+ // Falls back to 70/30 if valence is disabled.
1768
+ if (computeHybridScoreWithValence) {
1769
+ node.hybridScore = computeHybridScoreWithValence(node.similarity, normEnergy, node.valence ?? null);
1770
+ }
1771
+ else {
1772
+ node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
1773
+ }
1682
1774
  finalResults.push(node);
1683
1775
  }
1684
1776
  }
@@ -2847,7 +2939,7 @@ export class SqliteStorage {
2847
2939
  WITH input_kw(kw) AS (VALUES ${placeholders})
2848
2940
  SELECT sl.id, COUNT(DISTINCT ik.kw) AS shared_count
2849
2941
  FROM session_ledger sl,
2850
- json_each(sl.keywords) AS je,
2942
+ json_each(COALESCE(sl.keywords, '[]')) AS je,
2851
2943
  input_kw ik
2852
2944
  WHERE sl.user_id = ?
2853
2945
  AND sl.project = ?
@@ -15,7 +15,7 @@
15
15
  import { supabasePost, supabaseGet, supabaseRpc, supabasePatch, supabaseDelete, } from "../utils/supabaseApi.js";
16
16
  import { gzipSync, gunzipSync } from "node:zlib";
17
17
  import { debugLog } from "../utils/logger.js";
18
- import { PRISM_USER_ID, PRISM_SYNAPSE_ENABLED, PRISM_SYNAPSE_ITERATIONS, PRISM_SYNAPSE_SPREAD_FACTOR, PRISM_SYNAPSE_LATERAL_INHIBITION, PRISM_SYNAPSE_SOFT_CAP, } from "../config.js";
18
+ import { PRISM_USER_ID, PRISM_SYNAPSE_ENABLED, PRISM_SYNAPSE_ITERATIONS, PRISM_SYNAPSE_SPREAD_FACTOR, PRISM_SYNAPSE_LATERAL_INHIBITION, PRISM_SYNAPSE_SOFT_CAP, PRISM_VALENCE_ENABLED, } from "../config.js";
19
19
  import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
20
20
  import { runAutoMigrations } from "./supabaseMigrations.js";
21
21
  import { SafetyController } from "../darkfactory/safetyController.js";
@@ -60,12 +60,48 @@ export class SupabaseStorage {
60
60
  ...(entry.embedding_compressed !== undefined && { embedding_compressed: entry.embedding_compressed }),
61
61
  ...(entry.embedding_format !== undefined && { embedding_format: entry.embedding_format }),
62
62
  ...(entry.embedding_turbo_radius !== undefined && { embedding_turbo_radius: entry.embedding_turbo_radius }),
63
+ // v9.0: Affect-Tagged Memory
64
+ ...(entry.valence !== undefined && entry.valence !== null && { valence: entry.valence }),
63
65
  };
64
66
  return supabasePost("session_ledger", record);
65
67
  }
66
68
  async patchLedger(id, data) {
67
69
  await supabasePatch("session_ledger", data, { id: `eq.${id}` });
68
70
  }
71
+ // v9.0: Atomic delta-based budget persistence
72
+ // Supabase REST PATCH can't do arithmetic, so we use an RPC function.
73
+ // Falls back to read-modify-write if the RPC doesn't exist.
74
+ async patchHandoffBudgetDelta(project, userId, budgetDelta) {
75
+ try {
76
+ // Preferred: atomic delta via SQL RPC
77
+ await supabaseRpc("patch_budget_delta", {
78
+ p_project: project,
79
+ p_user_id: userId,
80
+ p_delta: budgetDelta,
81
+ });
82
+ }
83
+ catch (rpcErr) {
84
+ // Fallback: read-modify-write (non-atomic, acceptable for Supabase)
85
+ debugLog(`[SupabaseStorage] patch_budget_delta RPC unavailable, using fallback: ${rpcErr instanceof Error ? rpcErr.message : String(rpcErr)}`);
86
+ try {
87
+ const data = await supabaseGet("session_handoffs", {
88
+ project: `eq.${project}`,
89
+ user_id: `eq.${userId}`,
90
+ select: "cognitive_budget",
91
+ limit: "1",
92
+ });
93
+ const rows = Array.isArray(data) ? data : [];
94
+ const currentBudget = rows.length > 0 ? rows[0].cognitive_budget ?? 2000 : 2000;
95
+ await supabasePatch("session_handoffs", { cognitive_budget: Math.max(0, currentBudget + budgetDelta) }, {
96
+ project: `eq.${project}`,
97
+ user_id: `eq.${userId}`,
98
+ });
99
+ }
100
+ catch (fallbackErr) {
101
+ debugLog(`[SupabaseStorage] Budget delta fallback also failed: ${fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)}`);
102
+ }
103
+ }
104
+ }
69
105
  async getLedgerEntries(params) {
70
106
  const { ids, ...restParams } = params;
71
107
  // Construct PostgREST 'in.' payload for array of ids if present
@@ -354,7 +390,7 @@ export class SupabaseStorage {
354
390
  const anchorMap = new Map();
355
391
  for (const a of anchors)
356
392
  anchorMap.set(a.id, a.similarity ?? 1.0);
357
- const { results, telemetry } = await propagateActivation(anchorMap, async (nodeIds) => this.getLinksForNodes(nodeIds, userId), {
393
+ const { results, telemetry, flowWeights } = await propagateActivation(anchorMap, async (nodeIds) => this.getLinksForNodes(nodeIds, userId), {
358
394
  iterations: options.iterations ?? PRISM_SYNAPSE_ITERATIONS,
359
395
  spreadFactor: options.spreadFactor ?? PRISM_SYNAPSE_SPREAD_FACTOR,
360
396
  lateralInhibition: options.lateralInhibition ?? PRISM_SYNAPSE_LATERAL_INHIBITION,
@@ -374,7 +410,7 @@ export class SupabaseStorage {
374
410
  id: `in.(${missingIds.join(",")})`,
375
411
  user_id: `eq.${userId}`,
376
412
  deleted_at: "is.null",
377
- select: "id,project,summary,session_date,decisions,files_changed,is_rollup,importance,last_accessed_at",
413
+ select: "id,project,summary,session_date,decisions,files_changed,is_rollup,importance,last_accessed_at,valence",
378
414
  });
379
415
  for (const row of (Array.isArray(rows) ? rows : [])) {
380
416
  fullNodeMap.set(row.id, {
@@ -387,6 +423,7 @@ export class SupabaseStorage {
387
423
  is_rollup: Boolean(row.is_rollup),
388
424
  importance: Number(row.importance) || 0,
389
425
  last_accessed_at: row.last_accessed_at || null,
426
+ valence: row.valence != null ? Number(row.valence) : undefined,
390
427
  similarity: 0.0,
391
428
  });
392
429
  }
@@ -395,6 +432,31 @@ export class SupabaseStorage {
395
432
  debugLog(`[SupabaseStorage] applySynapse: failed to fetch missing nodes: ${e instanceof Error ? e.message : String(e)}`);
396
433
  }
397
434
  }
435
+ // ─── v9.0: Valence Propagation for discovered nodes ──────────
436
+ // Mirrors the SQLite implementation: batch propagateValence() call
437
+ // over the full results array with a valence lookup map.
438
+ let propagatedValenceMap = null;
439
+ if (PRISM_VALENCE_ENABLED) {
440
+ try {
441
+ const { propagateValence } = await import("../memory/valenceEngine.js");
442
+ // Build valence lookup from all known nodes
443
+ const valenceLookup = new Map();
444
+ for (const [id, node] of fullNodeMap) {
445
+ if (node.valence != null && Number.isFinite(node.valence)) {
446
+ valenceLookup.set(id, node.valence);
447
+ }
448
+ }
449
+ propagatedValenceMap = propagateValence(results, valenceLookup, flowWeights);
450
+ debugLog(`[SupabaseStorage] v9.0 valence propagation: ${propagatedValenceMap.size} nodes processed`);
451
+ }
452
+ catch (valErr) {
453
+ debugLog(`[SupabaseStorage] applySynapse: valence propagation failed (non-fatal): ${valErr instanceof Error ? valErr.message : String(valErr)}`);
454
+ }
455
+ }
456
+ // Import hybrid scoring if valence is enabled
457
+ const { computeHybridScoreWithValence } = PRISM_VALENCE_ENABLED
458
+ ? await import("../memory/valenceEngine.js")
459
+ : { computeHybridScoreWithValence: null };
398
460
  // Compute hybrid scores and build final result set
399
461
  const finalResults = [];
400
462
  for (const r of results) {
@@ -404,8 +466,15 @@ export class SupabaseStorage {
404
466
  node.activationScore = normEnergy;
405
467
  node.rawActivationEnergy = r.activationEnergy;
406
468
  node.isDiscovered = r.isDiscovered;
407
- // Hybrid blend: 70% original match relevance, 30% structural energy
408
- node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
469
+ // v9.0: Three-component hybrid blend with valence salience
470
+ const nodeValence = propagatedValenceMap?.get(r.id) ?? node.valence;
471
+ if (computeHybridScoreWithValence && nodeValence != null && Number.isFinite(nodeValence)) {
472
+ node.valence = nodeValence;
473
+ node.hybridScore = computeHybridScoreWithValence(node.similarity, normEnergy, nodeValence);
474
+ }
475
+ else {
476
+ node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
477
+ }
409
478
  finalResults.push(node);
410
479
  }
411
480
  }
@@ -773,6 +773,36 @@ export const MIGRATIONS = [
773
773
  CHECK (status IN ('PENDING', 'RUNNING', 'PAUSED', 'ABORTED', 'COMPLETED', 'FAILED'));
774
774
  `
775
775
  },
776
+ {
777
+ // ─── v9.0: Affect-Tagged Memory + Token-Economic Budget ──────────
778
+ //
779
+ // Two new columns:
780
+ // 1. session_ledger.valence — REAL [-1.0, +1.0], nullable
781
+ // Stores the affective "gut feeling" score for each memory entry.
782
+ // Derived deterministically from event_type at write time.
783
+ // Legacy entries remain NULL (neutral).
784
+ //
785
+ // 2. session_handoffs.cognitive_budget — REAL, nullable
786
+ // Persists the agent's current token-economic budget balance
787
+ // across sessions. Initialized on first spend; NULL before first use.
788
+ //
789
+ // Both are idempotent (ADD COLUMN IF NOT EXISTS) and non-breaking
790
+ // (nullable with no NOT NULL constraint). Existing data is untouched.
791
+ version: 42,
792
+ name: "v9_affect_tagged_memory_and_cognitive_budget",
793
+ sql: `
794
+ -- v9.0: Affect-Tagged Memory — valence column on session_ledger
795
+ ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS valence REAL DEFAULT NULL;
796
+
797
+ -- Partial index for valence-aware retrieval (non-null valence entries)
798
+ CREATE INDEX IF NOT EXISTS idx_ledger_valence
799
+ ON session_ledger(valence)
800
+ WHERE valence IS NOT NULL;
801
+
802
+ -- v9.0: Token-Economic Cognitive Budget — budget column on session_handoffs
803
+ ALTER TABLE session_handoffs ADD COLUMN IF NOT EXISTS cognitive_budget REAL DEFAULT NULL;
804
+ `,
805
+ },
776
806
  ];
777
807
  /**
778
808
  * Current schema version — derived from the MIGRATIONS array.
@@ -8,11 +8,15 @@
8
8
  */
9
9
  import { PRISM_STORAGE } from "../config.js";
10
10
  import { debugLog } from "../utils/logger.js";
11
+ import { getSetting } from "../storage/configStorage.js";
11
12
  let _bus = null;
12
13
  export async function getSyncBus() {
13
14
  if (_bus)
14
15
  return _bus;
15
- if (PRISM_STORAGE === "local") {
16
+ // DB-first, then env, then config.ts default (same priority as storage/index.ts)
17
+ const dbStorage = await getSetting("PRISM_STORAGE", "");
18
+ const resolvedStorage = dbStorage || process.env.PRISM_STORAGE || PRISM_STORAGE;
19
+ if (resolvedStorage === "local") {
16
20
  const { SqliteSyncBus } = await import("./sqliteSync.js");
17
21
  _bus = new SqliteSyncBus();
18
22
  }
@@ -57,6 +57,9 @@ import { HdcStateMachine } from "../sdm/stateMachine.js";
57
57
  import { ConceptDictionary } from "../sdm/conceptDictionary.js";
58
58
  import { PolicyGateway } from "../sdm/policyGateway.js";
59
59
  import { getSdmEngine } from "../sdm/sdmEngine.js";
60
+ // v9.0: Affect-Tagged Memory — valence-aware retrieval
61
+ import { formatValenceTag, generateValenceWarning, } from "../memory/valenceEngine.js";
62
+ import { PRISM_VALENCE_ENABLED } from "../config.js";
60
63
  import { PRISM_HDC_ENABLED, PRISM_HDC_EXPLAINABILITY_ENABLED, PRISM_HDC_POLICY_FALLBACK_THRESHOLD, PRISM_HDC_POLICY_CLARIFY_THRESHOLD, } from "../config.js";
61
64
  export async function knowledgeSearchHandler(args) {
62
65
  if (!isKnowledgeSearchArgs(args)) {
@@ -457,7 +460,11 @@ export async function sessionSearchMemoryHandler(args) {
457
460
  : "";
458
461
  // v8.0: Tag nodes discovered via Synapse multi-hop traversal
459
462
  const synapseTag = r.isDiscovered ? " [🌐 Synapse]" : "";
460
- return `[${i + 1}] ${simScore} similar${synapseTag} ${r.session_date || "unknown date"}\n` +
463
+ // v9.0: Valence tagaffect-tagged memory indicator
464
+ const valTag = PRISM_VALENCE_ENABLED && r.valence != null
465
+ ? ` ${formatValenceTag(r.valence)}`
466
+ : "";
467
+ return `[${i + 1}] ${simScore} similar${synapseTag}${valTag} — ${r.session_date || "unknown date"}\n` +
461
468
  ` Project: ${r.project}\n` +
462
469
  ` Summary: ${r.summary}\n` +
463
470
  importanceStr +
@@ -465,10 +472,25 @@ export async function sessionSearchMemoryHandler(args) {
465
472
  (r.decisions?.length ? ` Decisions: ${r.decisions.join("; ")}\n` : "") +
466
473
  (r.files_changed?.length ? ` Files: ${r.files_changed.join(", ")}\n` : "");
467
474
  }).join("\n");
475
+ // v9.0: Valence Warning — inject contextual warning when top results
476
+ // have historically negative affect (failures, corrections).
477
+ let valenceWarning = "";
478
+ if (PRISM_VALENCE_ENABLED && results.length > 0) {
479
+ const valenceValues = results
480
+ .map((r) => r.valence)
481
+ .filter((v) => v != null && Number.isFinite(v));
482
+ if (valenceValues.length > 0) {
483
+ const avgValence = valenceValues.reduce((a, b) => a + b, 0) / valenceValues.length;
484
+ const warning = generateValenceWarning(avgValence);
485
+ if (warning) {
486
+ valenceWarning = `\n\n${warning}`;
487
+ }
488
+ }
489
+ }
468
490
  // Phase 1: content[0] = human-readable results (unchanged from pre-Phase 1)
469
491
  const contentBlocks = [{
470
492
  type: "text",
471
- text: `🧠 Found ${results.length} semantically similar sessions:\n\n${formatted}`,
493
+ text: `🧠 Found ${results.length} semantically similar sessions:\n\n${formatted}${valenceWarning}`,
472
494
  }];
473
495
  // Phase 1: content[1] = machine-readable MemoryTrace (only when enable_trace=true)
474
496
  // topScore is read from results[0].similarity — this is the cosine distance
@@ -29,9 +29,13 @@ import { getLLMProvider } from "../utils/llm/factory.js";
29
29
  import { getCurrentGitState, getGitDrift } from "../utils/git.js";
30
30
  import { getSetting, getAllSettings } from "../storage/configStorage.js";
31
31
  import { mergeHandoff, dbToHandoffSchema, sanitizeForMerge } from "../utils/crdtMerge.js";
32
- import { GOOGLE_API_KEY, PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS } from "../config.js";
32
+ import { GOOGLE_API_KEY, PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS, PRISM_VALENCE_ENABLED, PRISM_VALENCE_WARNING_THRESHOLD, PRISM_COGNITIVE_BUDGET_ENABLED, } from "../config.js";
33
33
  import { captureLocalEnvironment } from "../utils/autoCapture.js";
34
34
  import { fireCaptionAsync } from "../utils/imageCaptioner.js";
35
+ // ─── v9.0: Affect-Tagged Memory + Token-Economic RL ──────────
36
+ import { deriveValence } from "../memory/valenceEngine.js";
37
+ import { estimateTokens, spendBudget, applyEarnings, formatBudgetDiagnostics, DEFAULT_BUDGET_SIZE, } from "../memory/cognitiveBudget.js";
38
+ import { computeVectorSurprisal } from "../memory/surprisalGate.js";
35
39
  import { isSessionSaveLedgerArgs, isSessionSaveHandoffArgs, isSessionLoadContextArgs, isMemoryHistoryArgs, isMemoryCheckoutArgs, // v2.2.0: health check type guard
36
40
  isSessionForgetMemoryArgs, // v3.1: TTL retention policy type guard
37
41
  // v4.0: Active Behavioral Memory type guards
@@ -83,6 +87,73 @@ export async function sessionSaveLedgerHandler(args) {
83
87
  const combinedText = [summary, ...(decisions || [])].join(" ");
84
88
  const keywords = toKeywordArray(combinedText);
85
89
  debugLog(`[session_save_ledger] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
90
+ // ── v9.0: Auto-derive valence from event_type ──────────────────
91
+ // Valence is a [-1, +1] real representing the affective charge of a memory.
92
+ // It's auto-derived at create-time from the event_type field so the agent
93
+ // doesn't need to manually classify emotional context.
94
+ let valence = null;
95
+ let valenceWarning = "";
96
+ if (PRISM_VALENCE_ENABLED) {
97
+ const eventType = args.event_type || "session";
98
+ valence = deriveValence(eventType);
99
+ if (valence !== null && valence < PRISM_VALENCE_WARNING_THRESHOLD) {
100
+ valenceWarning = `\n\n⚠️ **Negative Valence (${valence.toFixed(2)}):** This entry is tagged as a negative experience. ` +
101
+ `It will be prioritized in future retrievals to prevent repeating past mistakes.`;
102
+ }
103
+ debugLog(`[session_save_ledger] v9.0 valence derived: ${valence} (event_type=${eventType})`);
104
+ }
105
+ // ── v9.0: Token-Economic Budget ────────────────────────────────
106
+ // Charge the project's cognitive budget for this write operation.
107
+ // Budget exhaustion triggers warnings but NEVER blocks writes (graceful degradation).
108
+ let budgetDiagnostics = "";
109
+ let queryEmbedding = null;
110
+ if (PRISM_COGNITIVE_BUDGET_ENABLED) {
111
+ try {
112
+ // Load current budget from the project's handoff state
113
+ const handoff = await storage.loadContext(project, "quick", PRISM_USER_ID);
114
+ const currentBudget = handoff?.cognitive_budget ?? DEFAULT_BUDGET_SIZE;
115
+ // Apply UBI earnings before spending.
116
+ // NOTE: event_type is intentionally NOT passed to applyEarnings().
117
+ // The "infinite money glitch" — LLMs self-declare event_type: "success"
118
+ // to mint free tokens. Budget bonuses should only come from the
119
+ // Dark Factory adversarial evaluator. Valence derivation still uses
120
+ // event_type correctly for affect tagging.
121
+ const lastCreated = handoff?.updated_at ?? null;
122
+ const earnings = applyEarnings(currentBudget, lastCreated, undefined);
123
+ // v9.0: Compute real surprisal via vector similarity search.
124
+ // Uses the existing embedding pipeline — generates the embedding
125
+ // early so we can reuse it for the post-save embedding patch.
126
+ let surprisal = 0.5; // Fallback: neutral surprisal
127
+ if (GOOGLE_API_KEY) {
128
+ try {
129
+ const embeddingText = [summary, ...(decisions || [])].join("\n");
130
+ queryEmbedding = await getLLMProvider().generateEmbedding(embeddingText);
131
+ const surprisalResult = await computeVectorSurprisal(storage.searchMemory.bind(storage), JSON.stringify(queryEmbedding), project, PRISM_USER_ID);
132
+ surprisal = surprisalResult.surprisal;
133
+ debugLog(`[session_save_ledger] v9.0 surprisal: ${surprisal.toFixed(3)} (${surprisalResult.isBoilerplate ? 'boilerplate' : surprisalResult.isNovel ? 'novel' : 'standard'})`);
134
+ }
135
+ catch (surprErr) {
136
+ debugLog(`[session_save_ledger] Surprisal computation failed (using 0.5 fallback): ${surprErr instanceof Error ? surprErr.message : String(surprErr)}`);
137
+ }
138
+ }
139
+ const rawTokenCost = estimateTokens(summary);
140
+ const result = spendBudget(earnings.newBalance, rawTokenCost, surprisal);
141
+ // Format diagnostics for MCP response
142
+ budgetDiagnostics = "\n\n" + formatBudgetDiagnostics(result, DEFAULT_BUDGET_SIZE, earnings.ubiEarned, earnings.bonusEarned);
143
+ // v9.0: Persist budget using delta-based update to prevent concurrency race.
144
+ // If Agent A and Agent B both load budget=2000 concurrently, absolute writes
145
+ // cause Agent A's spend to be overwritten by Agent B's stale value.
146
+ // Delta update: UPDATE SET cognitive_budget = COALESCE(cognitive_budget, 2000) + delta
147
+ const budgetDelta = (earnings.ubiEarned + earnings.bonusEarned) - result.spent;
148
+ storage.patchHandoffBudgetDelta(project, PRISM_USER_ID, budgetDelta).catch((err) => {
149
+ debugLog(`[session_save_ledger] Budget persist failed (non-fatal): ${err.message}`);
150
+ });
151
+ debugLog(`[session_save_ledger] v9.0 budget: cost=${result.spent}, balance=${result.remaining}, delta=${budgetDelta}`);
152
+ }
153
+ catch (budgetErr) {
154
+ debugLog(`[session_save_ledger] Budget tracking failed (non-fatal): ${budgetErr instanceof Error ? budgetErr.message : String(budgetErr)}`);
155
+ }
156
+ }
86
157
  // Save via storage backend
87
158
  const effectiveRole = role || await getSetting("default_role", "global");
88
159
  const result = await storage.saveLedger({
@@ -95,14 +166,19 @@ export async function sessionSaveLedgerHandler(args) {
95
166
  decisions: decisions || [],
96
167
  keywords,
97
168
  role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
169
+ valence, // v9.0: Affect-tagged memory
98
170
  });
99
171
  // ─── Fire-and-forget embedding generation ───
100
172
  if (GOOGLE_API_KEY && result) {
101
- const embeddingText = [summary, ...(decisions || [])].join("\n");
102
173
  const savedEntry = Array.isArray(result) ? result[0] : result;
103
174
  const entryId = savedEntry?.id;
104
175
  if (entryId) {
105
- getLLMProvider().generateEmbedding(embeddingText)
176
+ // If embedding was already generated during surprisal computation, reuse it.
177
+ // Otherwise, generate it now (fire-and-forget).
178
+ const embeddingPromise = queryEmbedding
179
+ ? Promise.resolve(queryEmbedding)
180
+ : getLLMProvider().generateEmbedding([summary, ...(decisions || [])].join("\n"));
181
+ embeddingPromise
106
182
  .then(async (embedding) => {
107
183
  // Build atomic patch — float32 + TurboQuant in ONE DB update
108
184
  const patchData = {
@@ -194,7 +270,10 @@ export async function sessionSaveLedgerHandler(args) {
194
270
  (files_changed?.length ? `Files changed: ${files_changed.length}\n` : "") +
195
271
  (decisions?.length ? `Decisions: ${decisions.length}\n` : "") +
196
272
  (GOOGLE_API_KEY ? `📊 Embedding generation queued for semantic search.\n` : "") +
273
+ (valence !== null ? `🎭 Valence: ${valence.toFixed(2)}\n` : "") +
197
274
  repoPathWarning +
275
+ valenceWarning +
276
+ budgetDiagnostics +
198
277
  `\nRaw response: ${JSON.stringify(result)}`,
199
278
  }],
200
279
  isError: false,
@@ -720,8 +799,38 @@ export async function sessionLoadContextHandler(args) {
720
799
  debugLog(`[session_load_context] SDM Recall failed (non-fatal): ${err instanceof Error ? err.message : err}`);
721
800
  }
722
801
  }
802
+ // ─── v9.0: Cognitive Budget Diagnostics ──────────────────────
803
+ // Show the agent its current token-economic budget status at session start.
804
+ // This gives real-time feedback on spending capacity and health.
805
+ let budgetDiagBlock = "";
806
+ if (PRISM_COGNITIVE_BUDGET_ENABLED && level !== "quick") {
807
+ try {
808
+ const currentBudget = d.cognitive_budget ?? DEFAULT_BUDGET_SIZE;
809
+ const budgetSize = DEFAULT_BUDGET_SIZE;
810
+ const ratio = Math.max(0, Math.min(1, currentBudget / budgetSize));
811
+ const barLength = 20;
812
+ const fillLength = Math.round(ratio * barLength);
813
+ const bar = '█'.repeat(Math.max(0, fillLength)) + '░'.repeat(Math.max(0, barLength - fillLength));
814
+ let healthLabel;
815
+ if (ratio > 0.6)
816
+ healthLabel = "🟢 Healthy";
817
+ else if (ratio > 0.3)
818
+ healthLabel = "🟡 Moderate";
819
+ else if (ratio > 0.1)
820
+ healthLabel = "🟠 Low";
821
+ else
822
+ healthLabel = "🔴 Critical";
823
+ budgetDiagBlock = `\n\n[💰 COGNITIVE BUDGET]\n` +
824
+ `${bar} ${currentBudget}/${budgetSize} tokens — ${healthLabel}\n` +
825
+ `Budget replenishes via UBI (+5 tokens/hour) and event bonuses (success: +20, learning: +10).`;
826
+ debugLog(`[session_load_context] v9.0 budget diagnostics: ${currentBudget}/${budgetSize} (${(ratio * 100).toFixed(0)}%)`);
827
+ }
828
+ catch (budgetErr) {
829
+ debugLog(`[session_load_context] Budget diagnostics failed (non-fatal): ${budgetErr instanceof Error ? budgetErr.message : String(budgetErr)}`);
830
+ }
831
+ }
723
832
  // Build the response object before v4.0 augmentations
724
- let responseText = `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${sdmRecallBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`;
833
+ let responseText = `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${sdmRecallBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${budgetDiagBlock}${versionNote}`;
725
834
  // ─── v4.0: Behavioral Warnings Injection ───────────────────
726
835
  // If loadContext returned behavioral_warnings, add them to the
727
836
  // formatted output so the agent sees them prominently.
@@ -1090,6 +1199,14 @@ export async function sessionSaveExperienceHandler(args) {
1090
1199
  const keywords = toKeywordArray(summary);
1091
1200
  debugLog(`[session_save_experience] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
1092
1201
  const effectiveRole = role || await getSetting("default_role", "global");
1202
+ // v9.0: Experience events are the PRIMARY source of valence.
1203
+ // A failure event without valence = -0.8 is invisible to affective routing.
1204
+ // This was Bug #7 — the feature was wired in sessionSaveLedgerHandler but
1205
+ // missing in the handler that matters most for typed events.
1206
+ const valence = PRISM_VALENCE_ENABLED ? deriveValence(event_type, outcome) : null;
1207
+ if (valence !== null) {
1208
+ debugLog(`[session_save_experience] v9.0 valence derived: ${valence} (event_type=${event_type})`);
1209
+ }
1093
1210
  const result = await storage.saveLedger({
1094
1211
  project,
1095
1212
  conversation_id: "experience-event",
@@ -1107,6 +1224,7 @@ export async function sessionSaveExperienceHandler(args) {
1107
1224
  confidence_score: typeof confidence_score === "number" ? confidence_score : undefined,
1108
1225
  // Corrections start with importance 1 to jumpstart visibility
1109
1226
  importance: event_type === "correction" ? 1 : 0,
1227
+ valence, // v9.0: Affect-tagged memory — derived from event_type
1110
1228
  });
1111
1229
  // Fire-and-forget embedding generation
1112
1230
  if (GOOGLE_API_KEY && result) {