prism-mcp-server 20.21.24 → 20.21.25
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 +17 -0
- package/dist/backgroundScheduler.js +4 -2
- package/dist/config.js +1 -1
- package/dist/dashboard/server.js +4 -2
- package/dist/dashboard/settingsPolicy.js +1 -1
- package/dist/dashboard/ui.js +33 -0
- package/dist/server.js +2 -0
- package/dist/storage/actrSupabaseMigration.js +79 -0
- package/dist/storage/supabase.js +1 -1
- package/dist/storage/supabaseMigrations.js +2 -0
- package/dist/storage/synalux.js +77 -0
- package/dist/tools/graphHandlers.js +42 -20
- package/dist/utils/actrEnablement.js +29 -0
- package/dist/utils/cognitiveMemory.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,12 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
|
|
|
61
61
|
|
|
62
62
|
## Get started
|
|
63
63
|
|
|
64
|
+
Memory search automatically applies ACT-R ranking: relevance plus recency,
|
|
65
|
+
frequency and memory associations. No upvote or prompt instruction is required.
|
|
66
|
+
To opt out, use **Account & Settings → Preferences → Automatic Memory Ranking
|
|
67
|
+
(ACT-R)**, then restart your AI client. `PRISM_ACTR_ENABLED=false` also disables
|
|
68
|
+
it. This ranking is separate from the neural graph's stored importance filter.
|
|
69
|
+
|
|
64
70
|
```bash
|
|
65
71
|
npm install -g prism-mcp-server
|
|
66
72
|
prism connect
|
|
@@ -164,6 +170,17 @@ or by re-enabling after each run.
|
|
|
164
170
|
<details>
|
|
165
171
|
<summary>Release history (optional)</summary>
|
|
166
172
|
|
|
173
|
+
## What's New in v20.21.25
|
|
174
|
+
|
|
175
|
+
Semantic memory search now automatically applies ACT-R ranking, combining
|
|
176
|
+
relevance with recency, frequency and associations. **Account & Settings →
|
|
177
|
+
Preferences → Automatic Memory Ranking (ACT-R)** lets you opt out after an AI
|
|
178
|
+
client restart; an explicit environment value takes precedence.
|
|
179
|
+
|
|
180
|
+
Synalux Cloud uses authenticated, owner-scoped access-history and candidate-link
|
|
181
|
+
APIs. Cloud deployment requires the matching Portal history migration. Stored
|
|
182
|
+
importance thresholds remain separate, and HRR drift detection remains independent.
|
|
183
|
+
|
|
167
184
|
## What's New in v20.21.24
|
|
168
185
|
|
|
169
186
|
- Synalux cloud preserves initial importance and handles votes, graduated
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* storage backends during maintenance windows.
|
|
18
18
|
*/
|
|
19
19
|
import { getStorage } from "./storage/index.js";
|
|
20
|
-
import { PRISM_USER_ID, PRISM_SCHOLAR_ENABLED, PRISM_SCHOLAR_INTERVAL_MS, PRISM_GRAPH_PRUNING_ENABLED, PRISM_GRAPH_PRUNE_MIN_STRENGTH, PRISM_GRAPH_PRUNE_PROJECT_COOLDOWN_MS, PRISM_GRAPH_PRUNE_SWEEP_BUDGET_MS, PRISM_GRAPH_PRUNE_MAX_PROJECTS_PER_SWEEP,
|
|
20
|
+
import { PRISM_USER_ID, PRISM_SCHOLAR_ENABLED, PRISM_SCHOLAR_INTERVAL_MS, PRISM_GRAPH_PRUNING_ENABLED, PRISM_GRAPH_PRUNE_MIN_STRENGTH, PRISM_GRAPH_PRUNE_PROJECT_COOLDOWN_MS, PRISM_GRAPH_PRUNE_SWEEP_BUDGET_MS, PRISM_GRAPH_PRUNE_MAX_PROJECTS_PER_SWEEP, PRISM_ACTR_ACCESS_LOG_RETENTION_DAYS, } from "./config.js";
|
|
21
21
|
import { randomBytes } from "node:crypto";
|
|
22
22
|
import { debugLog } from "./utils/logger.js";
|
|
23
23
|
import { runWebScholar } from "./scholar/webScholar.js";
|
|
@@ -103,7 +103,9 @@ export const DEFAULT_SCHEDULER_CONFIG = {
|
|
|
103
103
|
graphPruneProjectCooldownMs: PRISM_GRAPH_PRUNE_PROJECT_COOLDOWN_MS,
|
|
104
104
|
graphPruneSweepBudgetMs: PRISM_GRAPH_PRUNE_SWEEP_BUDGET_MS,
|
|
105
105
|
graphPruneMaxProjectsPerSweep: PRISM_GRAPH_PRUNE_MAX_PROJECTS_PER_SWEEP,
|
|
106
|
-
|
|
106
|
+
// Default-on retrieval must not silently opt users into destructive maintenance.
|
|
107
|
+
// Preserve the existing explicit environment opt-in for access-log pruning.
|
|
108
|
+
enableAccessLogPrune: process.env.PRISM_ACTR_ENABLED === "true",
|
|
107
109
|
accessLogRetentionDays: PRISM_ACTR_ACCESS_LOG_RETENTION_DAYS,
|
|
108
110
|
};
|
|
109
111
|
// ─── Scheduler State ─────────────────────────────────────────
|
package/dist/config.js
CHANGED
|
@@ -275,7 +275,7 @@ export const PRISM_GRAPH_PRUNE_MAX_PROJECTS_PER_SWEEP = parseInt(process.env.PRI
|
|
|
275
275
|
// cognitive architecture. Replaces simple Ebbinghaus decay with
|
|
276
276
|
// a composite similarity + activation model.
|
|
277
277
|
/** Master switch for ACT-R activation-based re-ranking. */
|
|
278
|
-
export const PRISM_ACTR_ENABLED = process.env.PRISM_ACTR_ENABLED
|
|
278
|
+
export const PRISM_ACTR_ENABLED = process.env.PRISM_ACTR_ENABLED !== "false";
|
|
279
279
|
/** ACT-R decay parameter d in t^(-d). Higher = faster forgetting. (Paper default: 0.5) */
|
|
280
280
|
export const PRISM_ACTR_DECAY = parseFloat(process.env.PRISM_ACTR_DECAY || "0.5");
|
|
281
281
|
/** Weight of cosine similarity in composite score. (Default: 0.7 — similarity dominates) */
|
package/dist/dashboard/server.js
CHANGED
|
@@ -41,6 +41,7 @@ import { DASHBOARD_READ_MESSAGES } from "./readMessages.js";
|
|
|
41
41
|
import { isDashboardSettingKeyAllowed, isDashboardSettingValueAllowed } from "./settingsPolicy.js";
|
|
42
42
|
import { isTrustedRequest, isRebindGuardedPath } from "./hostGuard.js";
|
|
43
43
|
import { darkFactoryEnablementState, DARK_FACTORY_SETTINGS_UNAVAILABLE } from "../darkfactory/enablement.js";
|
|
44
|
+
import { actrEnablementState } from "../utils/actrEnablement.js";
|
|
44
45
|
import { resolveDashboardToken, requestHasToken, buildTokenCookie, dashboardTokenCookieName, isDashboardTokenProtectedPath, } from "./dashboardToken.js";
|
|
45
46
|
import { safeCompare, generateToken, isAuthenticated, createRateLimiter, initJWKS, } from "./authUtils.js";
|
|
46
47
|
const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
|
|
@@ -730,10 +731,11 @@ return false;}
|
|
|
730
731
|
// ─── API: Settings — GET (v3.0 Dashboard Settings) ───
|
|
731
732
|
if (url.pathname === "/api/settings" && req.method === "GET") {
|
|
732
733
|
try {
|
|
733
|
-
const { getAllSettings } = await import("../storage/configStorage.js");
|
|
734
|
+
const { getAllSettings, initConfigStorage } = await import("../storage/configStorage.js");
|
|
735
|
+
await initConfigStorage();
|
|
734
736
|
const settings = await getAllSettings();
|
|
735
737
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
736
|
-
return res.end(JSON.stringify({ settings, darkFactory: darkFactoryEnablementState() }));
|
|
738
|
+
return res.end(JSON.stringify({ settings, darkFactory: darkFactoryEnablementState(), actr: actrEnablementState() }));
|
|
737
739
|
}
|
|
738
740
|
catch {
|
|
739
741
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -29,7 +29,7 @@ export function isDashboardSettingValueAllowed(key, value) {
|
|
|
29
29
|
if (["auto_capture", "hivemind_enabled", "task_router_enabled"].includes(key)) {
|
|
30
30
|
return value === "true" || value === "false";
|
|
31
31
|
}
|
|
32
|
-
if (key === "PRISM_DARK_FACTORY_ENABLED")
|
|
32
|
+
if (key === "PRISM_DARK_FACTORY_ENABLED" || key === "PRISM_ACTR_ENABLED")
|
|
33
33
|
return value === "true" || value === "false";
|
|
34
34
|
if (key === "agent_name")
|
|
35
35
|
return value.length <= 80 && !/[<>]/.test(value);
|
package/dist/dashboard/ui.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { ANALYTICS_DATE_PATTERN, ANALYTICS_METRIC_IDS, DASHBOARD_READ_MESSAGES } from './readMessages.js';
|
|
17
17
|
import { DARK_FACTORY_SETTINGS_UNAVAILABLE } from '../darkfactory/enablement.js';
|
|
18
|
+
import { ACTR_SETTINGS_UNAVAILABLE, ACTR_UI } from '../utils/actrEnablement.js';
|
|
18
19
|
export function renderDashboardLocalOpenHTML() {
|
|
19
20
|
return `<!DOCTYPE html>
|
|
20
21
|
<html lang="en">
|
|
@@ -1302,6 +1303,15 @@ export function renderDashboardHTML(version) {
|
|
|
1302
1303
|
|
|
1303
1304
|
<div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
|
|
1304
1305
|
|
|
1306
|
+
<div class="setting-row">
|
|
1307
|
+
<div>
|
|
1308
|
+
<div class="setting-label">${ACTR_UI.label}</div>
|
|
1309
|
+
<div class="setting-desc">${ACTR_UI.description}</div>
|
|
1310
|
+
<div id="actr-state" style="color:var(--text-muted);font-size:0.78rem;margin-top:0.25rem">${ACTR_UI.loading}</div>
|
|
1311
|
+
</div>
|
|
1312
|
+
<button class="toggle" id="toggle-actr" type="button" role="switch" aria-label="${ACTR_UI.label}" aria-checked="false" disabled onclick="toggleBootSetting('PRISM_ACTR_ENABLED', this)"></button>
|
|
1313
|
+
</div>
|
|
1314
|
+
|
|
1305
1315
|
<div class="setting-row">
|
|
1306
1316
|
<div>
|
|
1307
1317
|
<div class="setting-label">Hivemind Mode</div>
|
|
@@ -4009,6 +4019,7 @@ function loadSettings() {
|
|
|
4009
4019
|
case 2:
|
|
4010
4020
|
data = _a.sent();
|
|
4011
4021
|
if (!data.darkFactory) throw new Error(DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE);
|
|
4022
|
+
loadActrSettingState(data.actr);
|
|
4012
4023
|
s = data.settings || {};
|
|
4013
4024
|
// Runtime toggles
|
|
4014
4025
|
if (s.auto_capture === 'true')
|
|
@@ -4069,6 +4080,8 @@ function loadSettings() {
|
|
|
4069
4080
|
case 3:
|
|
4070
4081
|
e_19 = _a.sent();
|
|
4071
4082
|
console.warn('Settings load failed:', e_19);
|
|
4083
|
+
document.getElementById('actr-state').textContent = ACTR_SETTINGS_UNAVAILABLE_MESSAGE;
|
|
4084
|
+
document.getElementById('toggle-actr').disabled = true;
|
|
4072
4085
|
document.getElementById('dark-factory-state').textContent = DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE;
|
|
4073
4086
|
document.getElementById('toggle-dark-factory').disabled = true;
|
|
4074
4087
|
document.getElementById('toggle-dark-factory').setAttribute('aria-disabled', 'true');
|
|
@@ -4122,11 +4135,30 @@ function toggleSetting(key, el) {
|
|
|
4122
4135
|
el.classList.toggle('active'); // rollback on failure
|
|
4123
4136
|
});
|
|
4124
4137
|
}
|
|
4138
|
+
function loadActrSettingState(ar) {
|
|
4139
|
+
var toggle = document.getElementById('toggle-actr');
|
|
4140
|
+
if (!ar) {
|
|
4141
|
+
toggle.disabled = true;
|
|
4142
|
+
document.getElementById('actr-state').textContent = ACTR_SETTINGS_UNAVAILABLE_MESSAGE;
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
var effective = ar.envOverride === null ? ar.saved : ar.envOverride;
|
|
4146
|
+
toggle.disabled = ar.envOverride !== null;
|
|
4147
|
+
toggle.setAttribute('aria-disabled', toggle.disabled ? 'true' : 'false');
|
|
4148
|
+
toggle.classList.toggle('active', effective);
|
|
4149
|
+
toggle.setAttribute('aria-checked', effective ? 'true' : 'false');
|
|
4150
|
+
document.getElementById('actr-state').textContent = ar.envOverride === null
|
|
4151
|
+
? 'Saved: ' + (ar.saved ? 'on' : 'off') + ' · Active in this process: ' + (ar.active ? 'on' : 'off') + (ar.pendingRestart ? ' · restart required' : '')
|
|
4152
|
+
: 'Environment override: ' + (ar.envOverride ? 'on' : 'off') + ' · saved preference: ' + (ar.saved ? 'on' : 'off');
|
|
4153
|
+
}
|
|
4125
4154
|
function toggleBootSetting(key, el) {
|
|
4155
|
+
if (el.disabled) return;
|
|
4156
|
+
if (key === 'PRISM_ACTR_ENABLED') el.disabled = true;
|
|
4126
4157
|
var isActive = el.classList.toggle('active');
|
|
4127
4158
|
el.setAttribute('aria-checked', isActive ? 'true' : 'false');
|
|
4128
4159
|
saveSetting(key, isActive ? 'true' : 'false').then(function (ok) {
|
|
4129
4160
|
if (!ok) {
|
|
4161
|
+
if (key === 'PRISM_ACTR_ENABLED') el.disabled = false;
|
|
4130
4162
|
el.classList.toggle('active'); // rollback on failure
|
|
4131
4163
|
el.setAttribute('aria-checked', isActive ? 'false' : 'true');
|
|
4132
4164
|
}
|
|
@@ -4137,6 +4169,7 @@ function toggleBootSetting(key, el) {
|
|
|
4137
4169
|
});
|
|
4138
4170
|
}
|
|
4139
4171
|
var DARK_FACTORY_SETTINGS_UNAVAILABLE_MESSAGE = ${JSON.stringify(DARK_FACTORY_SETTINGS_UNAVAILABLE)};
|
|
4172
|
+
var ACTR_SETTINGS_UNAVAILABLE_MESSAGE = ${JSON.stringify(ACTR_SETTINGS_UNAVAILABLE)};
|
|
4140
4173
|
var DARK_FACTORY_RESTART_MESSAGE = 'Saved. Restart your AI client for this to take effect.';
|
|
4141
4174
|
function saveBootSetting(key, value) {
|
|
4142
4175
|
saveSetting(key, value).then(function (ok) {
|
package/dist/server.js
CHANGED
|
@@ -62,6 +62,7 @@ ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequ
|
|
|
62
62
|
SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
63
63
|
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND, WATCHDOG_INTERVAL_MS, WATCHDOG_STALE_MIN, WATCHDOG_FROZEN_MIN, WATCHDOG_OFFLINE_MIN, WATCHDOG_LOOP_THRESHOLD, PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS, PRISM_HDC_ENABLED, PRISM_TASK_ROUTER_ENABLED_ENV, YDC_API_KEY, } from "./config.js";
|
|
64
64
|
import { initializeDarkFactoryEnabled, isDarkFactoryEnabled } from "./darkfactory/enablement.js";
|
|
65
|
+
import { initializeActrEnabled } from "./utils/actrEnablement.js";
|
|
65
66
|
import { startWatchdog, drainAlerts } from "./hivemindWatchdog.js";
|
|
66
67
|
import { startScheduler } from "./backgroundScheduler.js";
|
|
67
68
|
import { startDarkFactoryRunner } from "./darkfactory/runner.js";
|
|
@@ -1290,6 +1291,7 @@ export async function startServer() {
|
|
|
1290
1291
|
// initConfigStorage() is local SQLite only (~5ms), safe to await.
|
|
1291
1292
|
await initConfigStorage();
|
|
1292
1293
|
initializeDarkFactoryEnabled();
|
|
1294
|
+
initializeActrEnabled();
|
|
1293
1295
|
// v4.6.0: Initialize OTel AFTER the settings cache is warm so that
|
|
1294
1296
|
// initTelemetry() can read otel_enabled/otel_endpoint from getSettingSync()
|
|
1295
1297
|
// synchronously. This is a synchronous call — no await needed.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Canonical runtime SQL; parity with migration 040 is checked by a unit test.
|
|
2
|
+
export const ACTR_AUTH_SQL = String.raw `
|
|
3
|
+
-- Bind legacy self-hosted access history to signed identity. Preserve legitimate own-user access.
|
|
4
|
+
-- The automatic upgrader must never execute caller-supplied SQL for tenant clients.
|
|
5
|
+
CREATE OR REPLACE FUNCTION public.prism_apply_ddl(p_version integer, p_name text, p_sql text)
|
|
6
|
+
RETURNS json LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp
|
|
7
|
+
AS $$
|
|
8
|
+
DECLARE existing_name text;
|
|
9
|
+
BEGIN
|
|
10
|
+
IF p_version IS NULL OR p_version < 1 OR p_name IS NULL OR length(btrim(p_name)) = 0
|
|
11
|
+
OR p_sql IS NULL OR length(btrim(p_sql)) = 0 THEN
|
|
12
|
+
RAISE EXCEPTION 'Invalid migration identity or SQL' USING ERRCODE = '22023';
|
|
13
|
+
END IF;
|
|
14
|
+
PERFORM pg_advisory_xact_lock(hashtext('prism_apply_ddl'));
|
|
15
|
+
SELECT name INTO existing_name FROM public.prism_schema_versions WHERE version = p_version;
|
|
16
|
+
IF FOUND THEN
|
|
17
|
+
IF existing_name IS DISTINCT FROM p_name THEN
|
|
18
|
+
RAISE EXCEPTION 'Migration version/name conflict' USING ERRCODE = '22023';
|
|
19
|
+
END IF;
|
|
20
|
+
RETURN json_build_object('status','already_applied','version',p_version,'name',p_name);
|
|
21
|
+
END IF;
|
|
22
|
+
EXECUTE p_sql;
|
|
23
|
+
INSERT INTO public.prism_schema_versions(version,name) VALUES (p_version,p_name);
|
|
24
|
+
RETURN json_build_object('status','applied','version',p_version,'name',p_name);
|
|
25
|
+
END;
|
|
26
|
+
$$;
|
|
27
|
+
REVOKE ALL ON FUNCTION public.prism_apply_ddl(integer,text,text) FROM PUBLIC, anon, authenticated;
|
|
28
|
+
GRANT EXECUTE ON FUNCTION public.prism_apply_ddl(integer,text,text) TO service_role;
|
|
29
|
+
CREATE OR REPLACE FUNCTION public.prism_log_access(
|
|
30
|
+
p_user_id text, p_entry_id uuid, p_accessed_at timestamptz DEFAULT now(), p_context_hash text DEFAULT NULL
|
|
31
|
+
)
|
|
32
|
+
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp
|
|
33
|
+
AS $$
|
|
34
|
+
BEGIN
|
|
35
|
+
IF p_context_hash IS NOT NULL AND p_context_hash !~ '^[a-f0-9]{1,64}$' THEN
|
|
36
|
+
RAISE EXCEPTION 'Access context must be a hash' USING ERRCODE = '22023';
|
|
37
|
+
END IF;
|
|
38
|
+
INSERT INTO public.memory_access_log(entry_id,accessed_at,context_hash)
|
|
39
|
+
SELECT ledger.id, clock_timestamp(), p_context_hash FROM public.session_ledger AS ledger
|
|
40
|
+
WHERE ledger.id = p_entry_id AND ledger.user_id = p_user_id
|
|
41
|
+
AND ledger.deleted_at IS NULL AND (to_jsonb(ledger)->>'archived_at') IS NULL
|
|
42
|
+
AND (auth.role() = 'service_role' OR (p_user_id = auth.uid()::text
|
|
43
|
+
AND ((to_jsonb(ledger)->>'workspace_id') IS NULL
|
|
44
|
+
OR to_jsonb(ledger)->>'workspace_id' = auth.jwt()->>'workspace_id')));
|
|
45
|
+
END;
|
|
46
|
+
$$;
|
|
47
|
+
CREATE OR REPLACE FUNCTION public.prism_get_access_log(
|
|
48
|
+
p_user_id text, p_entry_ids uuid[], p_max_per_entry integer DEFAULT 50
|
|
49
|
+
)
|
|
50
|
+
RETURNS TABLE (entry_id uuid, accessed_at timestamptz)
|
|
51
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public, pg_temp
|
|
52
|
+
AS $$
|
|
53
|
+
BEGIN
|
|
54
|
+
IF p_entry_ids IS NULL OR cardinality(p_entry_ids) NOT BETWEEN 1 AND 50
|
|
55
|
+
OR p_max_per_entry IS NULL OR p_max_per_entry NOT BETWEEN 1 AND 50 THEN
|
|
56
|
+
RAISE EXCEPTION 'Invalid access history bounds' USING ERRCODE = '22023';
|
|
57
|
+
END IF;
|
|
58
|
+
RETURN QUERY
|
|
59
|
+
SELECT ledger.id, history.accessed_at FROM public.session_ledger AS ledger
|
|
60
|
+
CROSS JOIN LATERAL (
|
|
61
|
+
SELECT access.accessed_at FROM public.memory_access_log AS access
|
|
62
|
+
WHERE access.entry_id = ledger.id ORDER BY access.accessed_at DESC LIMIT p_max_per_entry
|
|
63
|
+
) AS history
|
|
64
|
+
WHERE ledger.id = ANY(p_entry_ids) AND ledger.user_id = p_user_id
|
|
65
|
+
AND ledger.deleted_at IS NULL AND (to_jsonb(ledger)->>'archived_at') IS NULL
|
|
66
|
+
AND (auth.role() = 'service_role' OR (p_user_id = auth.uid()::text
|
|
67
|
+
AND ((to_jsonb(ledger)->>'workspace_id') IS NULL
|
|
68
|
+
OR to_jsonb(ledger)->>'workspace_id' = auth.jwt()->>'workspace_id')));
|
|
69
|
+
END;
|
|
70
|
+
$$;
|
|
71
|
+
REVOKE ALL ON FUNCTION public.prism_log_access(text,uuid,timestamptz,text) FROM PUBLIC, anon;
|
|
72
|
+
REVOKE ALL ON FUNCTION public.prism_get_access_log(text,uuid[],integer) FROM PUBLIC, anon;
|
|
73
|
+
GRANT EXECUTE ON FUNCTION public.prism_log_access(text,uuid,timestamptz,text) TO service_role, authenticated;
|
|
74
|
+
GRANT EXECUTE ON FUNCTION public.prism_get_access_log(text,uuid[],integer) TO service_role, authenticated;
|
|
75
|
+
-- Global retention cannot be a permission held by ordinary tenant clients.
|
|
76
|
+
REVOKE ALL ON FUNCTION public.prism_prune_access_log(integer) FROM PUBLIC, anon, authenticated;
|
|
77
|
+
GRANT EXECUTE ON FUNCTION public.prism_prune_access_log(integer) TO service_role;
|
|
78
|
+
NOTIFY pgrst, 'reload schema';
|
|
79
|
+
`;
|
package/dist/storage/supabase.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* ═══════════════════════════════════════════════════════════════════
|
|
22
22
|
*/
|
|
23
23
|
import { supabaseRpc } from "../utils/supabaseApi.js";
|
|
24
|
+
import { ACTR_AUTH_SQL } from "./actrSupabaseMigration.js";
|
|
24
25
|
/**
|
|
25
26
|
* All Supabase DDL migrations.
|
|
26
27
|
*
|
|
@@ -805,6 +806,7 @@ export const MIGRATIONS = [
|
|
|
805
806
|
CREATE INDEX IF NOT EXISTS idx_research_tasks_status ON public.research_tasks(status, created_at ASC);
|
|
806
807
|
`
|
|
807
808
|
},
|
|
809
|
+
{ version: 40, name: "actr_authenticated_history", sql: ACTR_AUTH_SQL },
|
|
808
810
|
];
|
|
809
811
|
/**
|
|
810
812
|
* Current schema version — derived from the MIGRATIONS array.
|
package/dist/storage/synalux.js
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* - adjustImportance → POST /api/v1/prism/memory action=adjust_importance
|
|
23
23
|
* - getGraduatedInsights → POST /api/v1/prism/memory action=graduated_insights
|
|
24
24
|
* - decayImportance → POST /api/v1/prism/memory action=decay_importance
|
|
25
|
+
* - logAccess / recordAccesses / updateLastAccessed → action=record_access
|
|
26
|
+
* - getAccessLog → POST /api/v1/prism/memory action=get_access_log
|
|
25
27
|
* - saveHandoff → POST /api/v1/prism/memory action=save_handoff
|
|
26
28
|
* - saveHistorySnapshot → POST /api/v1/prism/memory action=save_history_snapshot
|
|
27
29
|
* - loadContext → POST /api/v1/prism/memory action=load_context
|
|
@@ -69,10 +71,85 @@ function resolveKnowledgeScope(callerScope) {
|
|
|
69
71
|
}
|
|
70
72
|
/** Refresh JWT this many ms before expiry to avoid edge-case 401s. */
|
|
71
73
|
const JWT_REFRESH_LEEWAY_MS = 60_000;
|
|
74
|
+
const ACCESS_BATCH_LIMIT = 50;
|
|
75
|
+
const ACCESS_ID_RE = /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i;
|
|
76
|
+
const ACCESS_MESSAGES = {
|
|
77
|
+
invalid: "Invalid memory access batch",
|
|
78
|
+
write: "Invalid memory access response",
|
|
79
|
+
read: "Invalid memory access history response",
|
|
80
|
+
failed: "[SynaluxStorage] Memory access recording failed",
|
|
81
|
+
};
|
|
82
|
+
function accessIds(ids) {
|
|
83
|
+
if (!Array.isArray(ids) || ids.some(id => typeof id !== "string" || !ACCESS_ID_RE.test(id))) {
|
|
84
|
+
throw new Error(ACCESS_MESSAGES.invalid);
|
|
85
|
+
}
|
|
86
|
+
return [...new Set(ids.map(id => id.toLowerCase()))];
|
|
87
|
+
}
|
|
72
88
|
function buildContextLoadKey(project, level, userId, role) {
|
|
73
89
|
return JSON.stringify({ project, level, userId, role: role ?? "" });
|
|
74
90
|
}
|
|
75
91
|
export class SynaluxStorage extends SupabaseStorage {
|
|
92
|
+
logAccess(entryId, contextHash) {
|
|
93
|
+
void this.recordAccesses([entryId], contextHash).catch(() => console.error(ACCESS_MESSAGES.failed));
|
|
94
|
+
}
|
|
95
|
+
async getCandidateLinks(entryIds) {
|
|
96
|
+
const ids = accessIds(entryIds);
|
|
97
|
+
if (ids.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
if (ids.length > ACCESS_BATCH_LIMIT)
|
|
100
|
+
throw new Error(ACCESS_MESSAGES.invalid);
|
|
101
|
+
const response = await this.portalPost("/api/v1/prism/memory", { action: "get_candidate_links", entry_ids: ids });
|
|
102
|
+
if (!Array.isArray(response.links) || response.links.length > ids.length * 20
|
|
103
|
+
|| response.links.some(row => !row || !ids.includes(row.source_id) || !ids.includes(row.target_id)
|
|
104
|
+
|| !Number.isFinite(row.strength) || row.strength < 0 || row.strength > 1))
|
|
105
|
+
throw new Error(ACCESS_MESSAGES.read);
|
|
106
|
+
return response.links;
|
|
107
|
+
}
|
|
108
|
+
async recordAccesses(entryIds, contextHash) {
|
|
109
|
+
await this.writeAccesses(entryIds, contextHash, true);
|
|
110
|
+
}
|
|
111
|
+
async updateLastAccessed(entryIds) {
|
|
112
|
+
await this.writeAccesses(entryIds, undefined, false);
|
|
113
|
+
}
|
|
114
|
+
async writeAccesses(entryIds, contextHash, recordHistory) {
|
|
115
|
+
const ids = accessIds(entryIds);
|
|
116
|
+
if (contextHash !== undefined && !/^[a-f0-9]{1,64}$/.test(contextHash))
|
|
117
|
+
throw new Error(ACCESS_MESSAGES.invalid);
|
|
118
|
+
for (let offset = 0; offset < ids.length; offset += ACCESS_BATCH_LIMIT) {
|
|
119
|
+
const batch = ids.slice(offset, offset + ACCESS_BATCH_LIMIT);
|
|
120
|
+
const response = await this.portalPost("/api/v1/prism/memory", {
|
|
121
|
+
action: "record_access", entry_ids: batch, context_hash: contextHash ?? null, record_history: recordHistory,
|
|
122
|
+
});
|
|
123
|
+
if (!Number.isInteger(response.recorded) || Number(response.recorded) < 0 || Number(response.recorded) > batch.length) {
|
|
124
|
+
throw new Error(ACCESS_MESSAGES.write);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async getAccessLog(entryIds, maxPerEntry = ACCESS_BATCH_LIMIT) {
|
|
129
|
+
const ids = accessIds(entryIds);
|
|
130
|
+
if (!Number.isInteger(maxPerEntry) || maxPerEntry < 1 || maxPerEntry > ACCESS_BATCH_LIMIT)
|
|
131
|
+
throw new Error(ACCESS_MESSAGES.invalid);
|
|
132
|
+
const result = new Map();
|
|
133
|
+
for (let offset = 0; offset < ids.length; offset += ACCESS_BATCH_LIMIT) {
|
|
134
|
+
const batch = ids.slice(offset, offset + ACCESS_BATCH_LIMIT);
|
|
135
|
+
const response = await this.portalPost("/api/v1/prism/memory", { action: "get_access_log", entry_ids: batch, max_per_entry: maxPerEntry });
|
|
136
|
+
if (!Array.isArray(response.accesses) || response.accesses.length > batch.length * maxPerEntry)
|
|
137
|
+
throw new Error(ACCESS_MESSAGES.read);
|
|
138
|
+
for (const row of response.accesses) {
|
|
139
|
+
if (!row || !batch.includes(row.entry_id) || typeof row.accessed_at !== "string")
|
|
140
|
+
throw new Error(ACCESS_MESSAGES.read);
|
|
141
|
+
const timestamp = new Date(row.accessed_at);
|
|
142
|
+
if (!Number.isFinite(timestamp.getTime()))
|
|
143
|
+
throw new Error(ACCESS_MESSAGES.read);
|
|
144
|
+
const history = result.get(row.entry_id) ?? [];
|
|
145
|
+
if (history.length >= maxPerEntry)
|
|
146
|
+
throw new Error(ACCESS_MESSAGES.read);
|
|
147
|
+
history.push(timestamp);
|
|
148
|
+
result.set(row.entry_id, history);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
76
153
|
baseUrl;
|
|
77
154
|
refreshToken;
|
|
78
155
|
cachedJwt = null;
|
|
@@ -51,7 +51,9 @@ const activeCompactions = new Set();
|
|
|
51
51
|
*/
|
|
52
52
|
import { computeEffectiveImportance, recordMemoryAccess } from "../utils/cognitiveMemory.js";
|
|
53
53
|
import { baseLevelActivation, candidateScopedSpreadingActivation, compositeRetrievalScore, } from "../utils/actrActivation.js";
|
|
54
|
-
import {
|
|
54
|
+
import { PRISM_ACTR_DECAY, PRISM_ACTR_WEIGHT_SIMILARITY, PRISM_ACTR_WEIGHT_ACTIVATION, PRISM_ACTR_SIGMOID_MIDPOINT, PRISM_ACTR_SIGMOID_STEEPNESS, PRISM_ACTR_MAX_ACCESSES_PER_ENTRY, } from "../config.js";
|
|
55
|
+
import { isActrEnabled } from "../utils/actrEnablement.js";
|
|
56
|
+
import { createHash } from "node:crypto";
|
|
55
57
|
import { HdcStateMachine } from "../sdm/stateMachine.js";
|
|
56
58
|
import { ConceptDictionary } from "../sdm/conceptDictionary.js";
|
|
57
59
|
import { PolicyGateway } from "../sdm/policyGateway.js";
|
|
@@ -361,7 +363,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
361
363
|
enable_trace = false,
|
|
362
364
|
// v5.2: Context-Weighted Retrieval — biases search toward active work context
|
|
363
365
|
context_boost = false, activation, } = args;
|
|
364
|
-
debugLog(`[session_search_memory] Semantic search: query
|
|
366
|
+
debugLog(`[session_search_memory] Semantic search: queryHash=${createHash("sha256").update(query).digest("hex")}, ` +
|
|
365
367
|
`project=${project || "all"}, limit=${limit}, threshold=${similarity_threshold}` +
|
|
366
368
|
`${context_boost ? ", context_boost=ON" : ""}`);
|
|
367
369
|
// Phase 1: Start total latency timer BEFORE any work (embedding + storage)
|
|
@@ -395,7 +397,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
395
397
|
}
|
|
396
398
|
if (contextParts.length > 0) {
|
|
397
399
|
effectiveQuery = `[${contextParts.join("; ")}] ${query}`;
|
|
398
|
-
debugLog(`[session_search_memory] Context boost applied
|
|
400
|
+
debugLog(`[session_search_memory] Context boost applied (${effectiveQuery.length} characters)`);
|
|
399
401
|
}
|
|
400
402
|
}
|
|
401
403
|
catch {
|
|
@@ -435,7 +437,8 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
435
437
|
// If we only fetch `limit` rows, the re-ranker can only shuffle those exact
|
|
436
438
|
// results — a memory at rank #(limit+1) by similarity but accessed 500 times
|
|
437
439
|
// would never surface. Fetch 4× or minimum 20, then slice after re-ranking.
|
|
438
|
-
const
|
|
440
|
+
const actrEnabled = isActrEnabled();
|
|
441
|
+
const candidateLimit = actrEnabled
|
|
439
442
|
? Math.min(Math.max(limit * 4, 20), 50)
|
|
440
443
|
: Math.min(limit, 20);
|
|
441
444
|
const results = await storage.searchMemory({
|
|
@@ -483,28 +486,38 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
483
486
|
}
|
|
484
487
|
// ── v7.0: ACT-R Re-Ranking Pipeline ──────────────────────────
|
|
485
488
|
const resultIds = results.map((r) => r.id).filter(Boolean);
|
|
489
|
+
const policyCandidates = [...results].sort((a, b) => (b.similarity ?? 0) - (a.similarity ?? 0)).slice(0, limit);
|
|
486
490
|
const now = new Date();
|
|
487
491
|
// Accumulate ACT-R metrics for trace output
|
|
488
492
|
let actrMetrics = null;
|
|
489
|
-
if (
|
|
493
|
+
if (actrEnabled && resultIds.length > 0) {
|
|
490
494
|
try {
|
|
491
495
|
// Step A: Bulk-fetch access logs for all candidate IDs
|
|
492
496
|
const accessLogMap = await storage.getAccessLog(resultIds, PRISM_ACTR_MAX_ACCESSES_PER_ENTRY);
|
|
493
497
|
// Step B: Fetch outbound links for spreading activation
|
|
494
498
|
const candidateIdSet = new Set(resultIds);
|
|
495
499
|
const linksMap = new Map();
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
linksMap.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
})));
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
linksMap.set(id, []);
|
|
500
|
+
if (storage.getCandidateLinks) {
|
|
501
|
+
const links = await storage.getCandidateLinks(resultIds);
|
|
502
|
+
for (const link of links) {
|
|
503
|
+
const outgoing = linksMap.get(link.source_id) ?? [];
|
|
504
|
+
outgoing.push({ target_id: link.target_id, strength: link.strength });
|
|
505
|
+
linksMap.set(link.source_id, outgoing);
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
|
+
else
|
|
509
|
+
for (const id of resultIds) {
|
|
510
|
+
try {
|
|
511
|
+
const links = await storage.getLinksFrom(id, PRISM_USER_ID, 0.0, 20);
|
|
512
|
+
linksMap.set(id, links.map((l) => ({
|
|
513
|
+
target_id: l.target_id,
|
|
514
|
+
strength: l.strength ?? 1.0,
|
|
515
|
+
})));
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
linksMap.set(id, []);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
508
521
|
// Step C: Compute activation for each result and re-rank
|
|
509
522
|
actrMetrics = { baseLevels: [], spreadings: [], sigmoids: [], composites: [] };
|
|
510
523
|
for (const r of results) {
|
|
@@ -541,6 +554,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
541
554
|
`mean composite=${(actrMetrics.composites.reduce((a, b) => a + b, 0) / actrMetrics.composites.length).toFixed(3)}`);
|
|
542
555
|
}
|
|
543
556
|
catch (actrErr) {
|
|
557
|
+
actrMetrics = null;
|
|
544
558
|
// ACT-R failures are non-fatal — degrade to similarity-only ordering
|
|
545
559
|
debugLog(`[session_search_memory] ACT-R re-ranking failed (non-fatal): ${actrErr instanceof Error ? actrErr.message : String(actrErr)}`);
|
|
546
560
|
}
|
|
@@ -550,13 +564,15 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
550
564
|
// so we only log access for results actually delivered to the LLM.
|
|
551
565
|
results.splice(limit);
|
|
552
566
|
if (PRISM_HDC_ENABLED && results.length > 0) {
|
|
553
|
-
|
|
554
|
-
const
|
|
567
|
+
// Policy thresholds are calibrated to semantic relevance, not activation.
|
|
568
|
+
const topScore = policyCandidates[0]?.similarity;
|
|
569
|
+
const secondScore = policyCandidates.length > 1 ? policyCandidates[1].similarity : 0;
|
|
555
570
|
const gapDistance = (topScore || 0) - (secondScore || 0);
|
|
556
571
|
const fallbackThreshold = PRISM_HDC_POLICY_FALLBACK_THRESHOLD || 0.85;
|
|
557
572
|
const clarifyThreshold = PRISM_HDC_POLICY_CLARIFY_THRESHOLD || 0.95;
|
|
558
573
|
const gapThreshold = clarifyThreshold - fallbackThreshold;
|
|
559
|
-
if ((topScore || 0) < fallbackThreshold || (
|
|
574
|
+
if ((topScore || 0) < fallbackThreshold || (policyCandidates.length > 1 && gapDistance < gapThreshold)
|
|
575
|
+
|| (results[0].similarity ?? 0) < fallbackThreshold) {
|
|
560
576
|
return {
|
|
561
577
|
content: [{
|
|
562
578
|
type: "text",
|
|
@@ -576,7 +592,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
576
592
|
// v7.0: Writes to both access_log buffer AND legacy last_accessed_at
|
|
577
593
|
const finalIds = results.map((r) => r.id).filter(Boolean);
|
|
578
594
|
if (finalIds.length > 0) {
|
|
579
|
-
const queryHash = query.
|
|
595
|
+
const queryHash = createHash("sha256").update(query).digest("hex");
|
|
580
596
|
recordMemoryAccess(finalIds, queryHash);
|
|
581
597
|
}
|
|
582
598
|
// Format results with similarity scores + effective importance + ACT-R
|
|
@@ -605,6 +621,9 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
605
621
|
type: "text",
|
|
606
622
|
text: `${searchResultsHeader(results.length, isHybridSearchResults(results))}\n\n${formatted}`,
|
|
607
623
|
}];
|
|
624
|
+
if (actrEnabled && resultIds.length > 0 && !actrMetrics) {
|
|
625
|
+
contentBlocks.push({ type: "text", text: "Automatic memory ranking unavailable for this search; returning relevance ordering." });
|
|
626
|
+
}
|
|
608
627
|
// Phase 1: content[1] = machine-readable MemoryTrace (only when enable_trace=true)
|
|
609
628
|
// topScore is read from results[0].similarity — this is the cosine distance
|
|
610
629
|
// already returned by SemanticSearchResult in the storage interface.
|
|
@@ -626,7 +645,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
626
645
|
totalMs,
|
|
627
646
|
project: project || null,
|
|
628
647
|
// v7.0: ACT-R observability
|
|
629
|
-
actrEnabled:
|
|
648
|
+
actrEnabled: actrMetrics !== null,
|
|
630
649
|
actrBaseLevelMean: actrMetrics ? mean(actrMetrics.baseLevels) : undefined,
|
|
631
650
|
actrSpreadingMean: actrMetrics ? mean(actrMetrics.spreadings) : undefined,
|
|
632
651
|
actrCompositeMean: actrMetrics ? mean(actrMetrics.composites) : undefined,
|
|
@@ -682,6 +701,9 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
682
701
|
text: contentBlocks[0].text +
|
|
683
702
|
`\n\n🔗 Graph-connected memories (${enrichedEntries.length} via 1-hop expansion):\n\n${graphFormatted}`,
|
|
684
703
|
};
|
|
704
|
+
const deliveredGraphIds = enrichedEntries.map((entry) => entry.id).filter(Boolean);
|
|
705
|
+
if (deliveredGraphIds.length > 0)
|
|
706
|
+
recordMemoryAccess(deliveredGraphIds, createHash("sha256").update(query).digest("hex"));
|
|
685
707
|
// Fire-and-forget: reinforce traversed links
|
|
686
708
|
for (const directId of directIds) {
|
|
687
709
|
storage.getLinksFrom(directId, PRISM_USER_ID, 0.3, 5)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getSettingSync } from "../storage/configStorage.js";
|
|
2
|
+
const SETTING_KEY = "PRISM_ACTR_ENABLED";
|
|
3
|
+
const DEFAULT_ENABLED = true;
|
|
4
|
+
export const ACTR_SETTINGS_UNAVAILABLE = "Automatic memory ranking settings unavailable. Retry when the server is reachable.";
|
|
5
|
+
export const ACTR_UI = {
|
|
6
|
+
label: "Automatic Memory Ranking (ACT-R)",
|
|
7
|
+
description: "Rank retrieved memories by relevance, recency, frequency and associations. Enabled by default. Restart your AI client after changing this setting.",
|
|
8
|
+
loading: "Loading ranking state…",
|
|
9
|
+
};
|
|
10
|
+
let startupDecision;
|
|
11
|
+
export function resolveActrEnabled(env, saved) {
|
|
12
|
+
if (env === "true" || env === "false")
|
|
13
|
+
return env === "true";
|
|
14
|
+
return saved === "true" || saved === "false" ? saved === "true" : DEFAULT_ENABLED;
|
|
15
|
+
}
|
|
16
|
+
export function initializeActrEnabled() {
|
|
17
|
+
startupDecision = resolveActrEnabled(process.env.PRISM_ACTR_ENABLED, getSettingSync(SETTING_KEY, String(DEFAULT_ENABLED)));
|
|
18
|
+
return startupDecision;
|
|
19
|
+
}
|
|
20
|
+
export function isActrEnabled() {
|
|
21
|
+
return startupDecision ?? initializeActrEnabled();
|
|
22
|
+
}
|
|
23
|
+
export function actrEnablementState() {
|
|
24
|
+
const env = process.env.PRISM_ACTR_ENABLED;
|
|
25
|
+
const envOverride = env === "true" || env === "false" ? env === "true" : null;
|
|
26
|
+
const saved = resolveActrEnabled(undefined, getSettingSync(SETTING_KEY, String(DEFAULT_ENABLED)));
|
|
27
|
+
const active = isActrEnabled();
|
|
28
|
+
return { saved, active, envOverride, pendingRestart: envOverride === null && saved !== active };
|
|
29
|
+
}
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
import { getStorage } from "../storage/index.js";
|
|
24
24
|
import { debugLog } from "./logger.js";
|
|
25
25
|
import { baseLevelActivation, parameterizedSigmoid, } from "./actrActivation.js";
|
|
26
|
-
import {
|
|
26
|
+
import { PRISM_ACTR_DECAY, PRISM_ACTR_SIGMOID_MIDPOINT, PRISM_ACTR_SIGMOID_STEEPNESS, } from "../config.js";
|
|
27
|
+
import { isActrEnabled } from "./actrEnablement.js";
|
|
27
28
|
/**
|
|
28
29
|
* Computes the effective importance of a memory, combining the semantic
|
|
29
30
|
* importance score with a recency-based activation factor.
|
|
@@ -53,7 +54,7 @@ export function computeEffectiveImportance(baseImportance, lastAccessedStr, crea
|
|
|
53
54
|
// Fallback to creation date if it has never been accessed
|
|
54
55
|
const referenceDateStr = lastAccessedStr || createdAtStr;
|
|
55
56
|
const referenceDate = new Date(referenceDateStr);
|
|
56
|
-
if (!
|
|
57
|
+
if (!isActrEnabled()) {
|
|
57
58
|
// ── Legacy Ebbinghaus Decay (v5.2, deprecated) ──
|
|
58
59
|
const diffMs = Math.max(0, now.getTime() - referenceDate.getTime());
|
|
59
60
|
const daysSinceAccess = diffMs / (1000 * 60 * 60 * 24);
|
|
@@ -89,6 +90,9 @@ export function recordMemoryAccess(ids, contextHash) {
|
|
|
89
90
|
return;
|
|
90
91
|
// Fire and forget, don't block execution
|
|
91
92
|
getStorage().then(storage => {
|
|
93
|
+
if (storage.recordAccesses) {
|
|
94
|
+
return storage.recordAccesses(ids, contextHash);
|
|
95
|
+
}
|
|
92
96
|
// v7.0: Log access events to memory_access_log via buffer
|
|
93
97
|
for (const id of ids) {
|
|
94
98
|
storage.logAccess(id, contextHash);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.21.
|
|
3
|
+
"version": "20.21.25",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Persistent session memory for AI coding agents that never leaves your machine — including the on-device model that reasons over it. Restores your prior decisions, open TODOs, and changed files across sessions; adds associative recall of related past work, semantic drift detection, and local inference. Local-first by default. Works with Claude Code, Cursor, and Codex.",
|
|
6
6
|
"module": "index.ts",
|