prism-mcp-server 5.2.0 → 5.5.0
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 +308 -218
- package/dist/backgroundScheduler.js +327 -0
- package/dist/config.js +29 -0
- package/dist/dashboard/server.js +246 -0
- package/dist/dashboard/ui.js +216 -6
- package/dist/hivemindWatchdog.js +206 -0
- package/dist/lifecycle.js +59 -4
- package/dist/scholar/freeSearch.js +78 -0
- package/dist/scholar/webScholar.js +258 -0
- package/dist/sdm/sdmDecoder.js +75 -0
- package/dist/sdm/sdmEngine.js +158 -0
- package/dist/server.js +173 -11
- package/dist/storage/sqlite.js +298 -47
- package/dist/storage/supabase.js +114 -1
- package/dist/tools/agentRegistryDefinitions.js +11 -4
- package/dist/tools/agentRegistryHandlers.js +23 -5
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +46 -1
- package/dist/tools/sessionMemoryHandlers.js +210 -38
- package/dist/utils/briefing.js +1 -1
- package/dist/utils/crdtMerge.js +152 -0
- package/dist/utils/healthCheck.js +15 -0
- package/dist/utils/llm/adapters/gemini.js +3 -3
- package/package.json +9 -2
|
@@ -112,12 +112,13 @@ export async function agentHeartbeatHandler(args) {
|
|
|
112
112
|
}
|
|
113
113
|
const effectiveRole = args.role || await getSetting("default_role", "global");
|
|
114
114
|
const storage = await getStorage();
|
|
115
|
-
await storage.heartbeatAgent(args.project, PRISM_USER_ID, effectiveRole, args.current_task);
|
|
115
|
+
await storage.heartbeatAgent(args.project, PRISM_USER_ID, effectiveRole, args.current_task, args.expected_duration_minutes);
|
|
116
116
|
return {
|
|
117
117
|
content: [{
|
|
118
118
|
type: "text",
|
|
119
119
|
text: `💓 Heartbeat updated for **${escapeMd(effectiveRole)}** on \`${escapeMd(args.project)}\`.` +
|
|
120
|
-
(args.current_task ? ` Task: ${escapeMd(args.current_task)}` : "")
|
|
120
|
+
(args.current_task ? ` Task: ${escapeMd(args.current_task)}` : "") +
|
|
121
|
+
(args.expected_duration_minutes ? ` (ETA: ${args.expected_duration_minutes}m)` : ""),
|
|
121
122
|
}],
|
|
122
123
|
};
|
|
123
124
|
}
|
|
@@ -138,25 +139,42 @@ export async function agentListTeamHandler(args) {
|
|
|
138
139
|
}],
|
|
139
140
|
};
|
|
140
141
|
}
|
|
142
|
+
// v5.3: Health-state indicator mapping
|
|
143
|
+
const statusIndicators = {
|
|
144
|
+
active: "🟢 Active",
|
|
145
|
+
idle: "💤 Idle",
|
|
146
|
+
shutdown: "⚫ Offline",
|
|
147
|
+
stale: "🟡 Stale",
|
|
148
|
+
frozen: "🔴 Frozen",
|
|
149
|
+
overdue: "⏰ Overdue",
|
|
150
|
+
looping: "🔄 Looping",
|
|
151
|
+
};
|
|
141
152
|
const lines = team.map(agent => {
|
|
142
153
|
const icon = getRoleIcon(agent.role);
|
|
143
154
|
const ago = agent.last_heartbeat
|
|
144
155
|
? getTimeAgo(agent.last_heartbeat)
|
|
145
156
|
: "unknown";
|
|
157
|
+
const healthIndicator = statusIndicators[agent.status] || `❓ ${agent.status}`;
|
|
146
158
|
// escapeMd() applied to all user-controlled fields to prevent
|
|
147
159
|
// markdown metacharacters in task descriptions from corrupting output
|
|
148
160
|
return (`${icon} **${escapeMd(agent.role)}**` +
|
|
149
161
|
(agent.agent_name ? ` (${escapeMd(agent.agent_name)})` : "") +
|
|
150
|
-
` — ${
|
|
162
|
+
` — ${healthIndicator}` +
|
|
151
163
|
(agent.current_task ? ` | Task: ${escapeMd(agent.current_task)}` : "") +
|
|
164
|
+
(agent.loop_count && agent.loop_count >= 3 ? ` | 🔄 Loop: ${agent.loop_count}x` : "") +
|
|
152
165
|
` | Last seen: ${ago}`);
|
|
153
166
|
});
|
|
167
|
+
// Count agents by health state for summary
|
|
168
|
+
const healthyCt = team.filter(a => a.status === "active" || a.status === "idle").length;
|
|
169
|
+
const warnCt = team.filter(a => ["stale", "overdue", "looping", "frozen"].includes(a.status)).length;
|
|
154
170
|
return {
|
|
155
171
|
content: [{
|
|
156
172
|
type: "text",
|
|
157
|
-
text: `## 🐝
|
|
173
|
+
text: `## 🐝 Hivemind Team — \`${escapeMd(args.project)}\`\n\n` +
|
|
158
174
|
lines.join("\n") +
|
|
159
|
-
`\n\n_${team.length} agent(s)
|
|
175
|
+
`\n\n_${team.length} agent(s) | ${healthyCt} healthy` +
|
|
176
|
+
(warnCt > 0 ? ` | ⚠️ ${warnCt} need attention` : "") +
|
|
177
|
+
`_\n_Watchdog monitoring active: health checked every 60s._`,
|
|
160
178
|
}],
|
|
161
179
|
};
|
|
162
180
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -26,8 +26,8 @@ export { webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, br
|
|
|
26
26
|
// This file always exports them — server.ts decides whether to include them in the tool list.
|
|
27
27
|
//
|
|
28
28
|
// v0.4.0: Added SESSION_COMPACT_LEDGER_TOOL and SESSION_SEARCH_MEMORY_TOOL
|
|
29
|
-
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL, DEEP_STORAGE_PURGE_TOOL, isDeepStoragePurgeArgs } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler, knowledgeSyncRulesHandler, sessionExportMemoryHandler, deepStoragePurgeHandler } from "./sessionMemoryHandlers.js";
|
|
29
|
+
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL, DEEP_STORAGE_PURGE_TOOL, SESSION_INTUITIVE_RECALL_TOOL, isDeepStoragePurgeArgs } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler, knowledgeSyncRulesHandler, sessionExportMemoryHandler, deepStoragePurgeHandler, sessionIntuitiveRecallHandler } from "./sessionMemoryHandlers.js";
|
|
31
31
|
// ── Compaction Handler (v0.4.0 — Enhancement #2) ──
|
|
32
32
|
// The compaction handler is in a separate file because it's significantly
|
|
33
33
|
// more complex than the other session memory handlers (chunked Gemini
|
|
@@ -55,6 +55,9 @@ export const SESSION_SAVE_HANDOFF_TOOL = {
|
|
|
55
55
|
description: "Upsert the latest project handoff state for the next session to consume on boot. " +
|
|
56
56
|
"This is the 'live context' that gets loaded when a new session starts. " +
|
|
57
57
|
"Calling this replaces the previous handoff for the same project (upsert on project).\n\n" +
|
|
58
|
+
"**v5.4 CRDT Merge**: On version conflict, a CRDT OR-Map engine automatically merges " +
|
|
59
|
+
"your changes with concurrent work (Add-Wins OR-Set for arrays, Last-Writer-Wins for scalars). " +
|
|
60
|
+
"Pass expected_version to enable concurrency control.\n\n" +
|
|
58
61
|
"**v0.4.0 OCC**: If you received a version number from session_load_context, " +
|
|
59
62
|
"/resume_session prompt, or memory resource attachment, you MUST pass it as " +
|
|
60
63
|
"expected_version to prevent overwriting another session's changes.",
|
|
@@ -92,6 +95,10 @@ export const SESSION_SAVE_HANDOFF_TOOL = {
|
|
|
92
95
|
type: "string",
|
|
93
96
|
description: "Optional. Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Omit to let the server auto-resolve from dashboard settings.",
|
|
94
97
|
},
|
|
98
|
+
disable_merge: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
description: "Set to true to disable automatic CRDT merging and fail strictly on version conflict (original OCC behavior). Default: false.",
|
|
101
|
+
},
|
|
95
102
|
},
|
|
96
103
|
required: ["project"],
|
|
97
104
|
},
|
|
@@ -369,6 +376,7 @@ export function isSessionSaveLedgerArgs(args) {
|
|
|
369
376
|
}
|
|
370
377
|
// REVIEWER NOTE: v0.4.0 adds expected_version to the type guard
|
|
371
378
|
// for optimistic concurrency control. It's optional for backward compat.
|
|
379
|
+
// v5.4: Added disable_merge for CRDT bypass.
|
|
372
380
|
export function isSessionSaveHandoffArgs(args) {
|
|
373
381
|
return (typeof args === "object" &&
|
|
374
382
|
args !== null &&
|
|
@@ -902,7 +910,44 @@ export function isDeepStoragePurgeArgs(args) {
|
|
|
902
910
|
return false;
|
|
903
911
|
if (a.older_than_days !== undefined && typeof a.older_than_days !== "number")
|
|
904
912
|
return false;
|
|
905
|
-
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
// ─── v5.5: SDM Intuitive Recall Tool ──────────────────────────
|
|
916
|
+
export const SESSION_INTUITIVE_RECALL_TOOL = {
|
|
917
|
+
name: "session_intuitive_recall",
|
|
918
|
+
description: "Manually trigger the Sparse Distributed Memory (SDM) Intuitive Recall to surface latent patterns " +
|
|
919
|
+
"and related memories for a given query without blowing up the context window. " +
|
|
920
|
+
"Uses high-speed JS-space Hamming distance scanning on compressed embeddings.",
|
|
921
|
+
inputSchema: {
|
|
922
|
+
type: "object",
|
|
923
|
+
properties: {
|
|
924
|
+
project: {
|
|
925
|
+
type: "string",
|
|
926
|
+
description: "Project identifier.",
|
|
927
|
+
},
|
|
928
|
+
query: {
|
|
929
|
+
type: "string",
|
|
930
|
+
description: "The text query or context to trigger the recall.",
|
|
931
|
+
},
|
|
932
|
+
limit: {
|
|
933
|
+
type: "integer",
|
|
934
|
+
description: "Maximum number of latent patterns to surface (default: 3).",
|
|
935
|
+
},
|
|
936
|
+
threshold: {
|
|
937
|
+
type: "number",
|
|
938
|
+
description: "Similarity threshold 0-1 (default: 0.55).",
|
|
939
|
+
},
|
|
940
|
+
},
|
|
941
|
+
required: ["project", "query"],
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
export function isSessionIntuitiveRecallArgs(args) {
|
|
945
|
+
if (typeof args !== "object" || args === null)
|
|
946
|
+
return false;
|
|
947
|
+
const a = args;
|
|
948
|
+
if (typeof a.project !== "string")
|
|
949
|
+
return false;
|
|
950
|
+
if (typeof a.query !== "string")
|
|
906
951
|
return false;
|
|
907
952
|
return true;
|
|
908
953
|
}
|
|
@@ -21,6 +21,7 @@ import { toKeywordArray } from "../utils/keywordExtractor.js";
|
|
|
21
21
|
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
22
22
|
import { getCurrentGitState, getGitDrift } from "../utils/git.js";
|
|
23
23
|
import { getSetting, getAllSettings } from "../storage/configStorage.js";
|
|
24
|
+
import { mergeHandoff, dbToHandoffSchema } from "../utils/crdtMerge.js";
|
|
24
25
|
// ─── Phase 1: Explainability & Memory Lineage ────────────────
|
|
25
26
|
// These utilities provide structured tracing metadata for search operations.
|
|
26
27
|
// When `enable_trace: true` is passed to session_search_memory or knowledge_search,
|
|
@@ -39,11 +40,13 @@ isSessionSaveExperienceArgs, isKnowledgeVoteArgs,
|
|
|
39
40
|
// v4.2: Sync Rules type guard
|
|
40
41
|
isKnowledgeSyncRulesArgs,
|
|
41
42
|
// v5.1: Deep Storage Mode type guard
|
|
42
|
-
isDeepStoragePurgeArgs,
|
|
43
|
+
isDeepStoragePurgeArgs,
|
|
44
|
+
// v5.5: SDM Intuitive Recall type guard
|
|
45
|
+
isSessionIntuitiveRecallArgs, } from "./sessionMemoryDefinitions.js";
|
|
43
46
|
// v4.2: File system access for knowledge_sync_rules
|
|
44
47
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
45
48
|
import { existsSync } from "node:fs";
|
|
46
|
-
import { join, dirname, resolve, isAbsolute,
|
|
49
|
+
import { join, dirname, resolve, isAbsolute, relative } from "node:path";
|
|
47
50
|
// v3.1: In-memory debounce lock for auto-compaction.
|
|
48
51
|
// Prevents multiple concurrent Gemini compaction tasks for the same project
|
|
49
52
|
// when many agents call session_save_ledger at the same time.
|
|
@@ -201,7 +204,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
201
204
|
`(expected_version=${expected_version ?? "none"})`);
|
|
202
205
|
// Auto-extract keywords from summary + context for knowledge accumulation
|
|
203
206
|
const combinedText = [last_summary || "", key_context || ""].filter(Boolean).join(" ");
|
|
204
|
-
|
|
207
|
+
let keywords = combinedText ? toKeywordArray(combinedText) : undefined;
|
|
205
208
|
if (keywords) {
|
|
206
209
|
debugLog(`[session_save_handoff] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
|
|
207
210
|
}
|
|
@@ -215,7 +218,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
215
218
|
}
|
|
216
219
|
// Save via storage backend (OCC-aware)
|
|
217
220
|
const effectiveRole = role || await getSetting("default_role", "global");
|
|
218
|
-
|
|
221
|
+
let data = await storage.saveHandoff({
|
|
219
222
|
project,
|
|
220
223
|
user_id: PRISM_USER_ID,
|
|
221
224
|
last_summary: last_summary ?? null,
|
|
@@ -227,32 +230,99 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
227
230
|
metadata,
|
|
228
231
|
role: effectiveRole, // v3.0: Hivemind role scoping (dashboard fallback)
|
|
229
232
|
}, expected_version ?? null);
|
|
230
|
-
// ───
|
|
233
|
+
// ─── v5.4: CRDT Auto-Merge Resolution Loop ──────────────────
|
|
234
|
+
//
|
|
235
|
+
// Instead of returning a conflict error, we now:
|
|
236
|
+
// 1. Fetch the base state (the version the incoming agent read)
|
|
237
|
+
// 2. Fetch the current DB state (what beat the incoming agent)
|
|
238
|
+
// 3. Run a 3-way CRDT merge (OR-Set for arrays, LWW for scalars)
|
|
239
|
+
// 4. Retry the save with the merged state
|
|
240
|
+
//
|
|
241
|
+
// This converts what was previously an error into an automatic merge.
|
|
242
|
+
// The loop handles the rare case where ANOTHER save sneaks in during
|
|
243
|
+
// our merge (up to MAX_ATTEMPTS retries before giving up).
|
|
244
|
+
const MAX_MERGE_ATTEMPTS = 3;
|
|
245
|
+
let mergeAttempts = 0;
|
|
246
|
+
let isMerged = false;
|
|
247
|
+
let mergeStrategy = null;
|
|
248
|
+
while (data.status === "conflict" && mergeAttempts < MAX_MERGE_ATTEMPTS) {
|
|
249
|
+
// If the user explicitly disabled CRDT merging, return old OCC error
|
|
250
|
+
if (args.disable_merge) {
|
|
251
|
+
debugLog(`[session_save_handoff] VERSION CONFLICT for "${project}": ` +
|
|
252
|
+
`expected=${expected_version}, current=${data.current_version} (merge disabled)`);
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `⚠️ Version conflict detected for project "${project}"!\n\n` +
|
|
257
|
+
`You sent version ${expected_version}, but the current version is ${data.current_version}.\n` +
|
|
258
|
+
`Auto-merge is disabled. Please call session_load_context to see the latest changes, ` +
|
|
259
|
+
`then manually merge your updates and try saving again.`,
|
|
260
|
+
}],
|
|
261
|
+
isError: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
debugLog(`[session_save_handoff] CRDT merge attempt ${mergeAttempts + 1}/${MAX_MERGE_ATTEMPTS} ` +
|
|
265
|
+
`for "${project}" (expected=${expected_version}, current=${data.current_version})`);
|
|
266
|
+
// Step 1: Fetch the base state (what the incoming agent originally read)
|
|
267
|
+
const baseDbState = expected_version
|
|
268
|
+
? await storage.getHandoffAtVersion(project, expected_version, PRISM_USER_ID)
|
|
269
|
+
: null;
|
|
270
|
+
const baseState = dbToHandoffSchema(baseDbState);
|
|
271
|
+
// Step 2: Fetch current DB state (what beat us to the save)
|
|
272
|
+
const currentDbState = await storage.loadContext(project, "standard", PRISM_USER_ID);
|
|
273
|
+
const currentState = dbToHandoffSchema(currentDbState);
|
|
274
|
+
if (!currentState || !currentDbState) {
|
|
275
|
+
debugLog("[session_save_handoff] CRDT merge failed: could not load current state");
|
|
276
|
+
break; // Safety fallback — can't merge without both sides
|
|
277
|
+
}
|
|
278
|
+
// Step 3: Build the incoming state from the original args
|
|
279
|
+
const incomingState = {
|
|
280
|
+
summary: last_summary || "",
|
|
281
|
+
active_branch: active_branch,
|
|
282
|
+
key_context: key_context,
|
|
283
|
+
pending_todo: open_todos,
|
|
284
|
+
active_decisions: undefined,
|
|
285
|
+
keywords: keywords,
|
|
286
|
+
};
|
|
287
|
+
// Step 4: Run 3-way CRDT merge
|
|
288
|
+
const crdt = mergeHandoff(baseState, incomingState, currentState);
|
|
289
|
+
mergeStrategy = crdt.strategy;
|
|
290
|
+
isMerged = true;
|
|
291
|
+
debugLog(`[session_save_handoff] CRDT merge strategy: ${JSON.stringify(crdt.strategy)}`);
|
|
292
|
+
// Step 5: Build merged handoff and retry save
|
|
293
|
+
const mergedExpectedVersion = currentDbState.version;
|
|
294
|
+
data = await storage.saveHandoff({
|
|
295
|
+
project,
|
|
296
|
+
user_id: PRISM_USER_ID,
|
|
297
|
+
last_summary: crdt.merged.summary ?? null,
|
|
298
|
+
pending_todo: crdt.merged.pending_todo ?? null,
|
|
299
|
+
active_decisions: crdt.merged.active_decisions ?? null,
|
|
300
|
+
keywords: crdt.merged.keywords ?? null,
|
|
301
|
+
key_context: crdt.merged.key_context ?? null,
|
|
302
|
+
active_branch: crdt.merged.active_branch ?? null,
|
|
303
|
+
metadata: {
|
|
304
|
+
...metadata,
|
|
305
|
+
crdt_merge_count: (currentDbState.metadata?.crdt_merge_count || 0) + 1,
|
|
306
|
+
last_merge_strategy: crdt.strategy,
|
|
307
|
+
},
|
|
308
|
+
role: effectiveRole,
|
|
309
|
+
}, mergedExpectedVersion ?? null);
|
|
310
|
+
// Update these for the snapshot/notification blocks below
|
|
311
|
+
if (data.status !== "conflict") {
|
|
312
|
+
// Merge succeeded — update local vars for the success path
|
|
313
|
+
keywords = crdt.merged.keywords ?? keywords;
|
|
314
|
+
}
|
|
315
|
+
mergeAttempts++;
|
|
316
|
+
}
|
|
317
|
+
// After all merge attempts exhausted, still a conflict → give up
|
|
231
318
|
if (data.status === "conflict") {
|
|
232
|
-
debugLog(`[session_save_handoff]
|
|
233
|
-
`expected=${expected_version}, current=${data.current_version}`);
|
|
319
|
+
debugLog(`[session_save_handoff] CRDT merge exhausted after ${MAX_MERGE_ATTEMPTS} attempts for "${project}"`);
|
|
234
320
|
return {
|
|
235
321
|
content: [{
|
|
236
322
|
type: "text",
|
|
237
|
-
text: `⚠️
|
|
238
|
-
`
|
|
239
|
-
`
|
|
240
|
-
`Please call session_load_context to see what changed, then merge ` +
|
|
241
|
-
`it with your attempted updates:\n` +
|
|
242
|
-
(last_summary
|
|
243
|
-
? ` Your attempted summary: ${last_summary}\n`
|
|
244
|
-
: "") +
|
|
245
|
-
(open_todos?.length
|
|
246
|
-
? ` Your attempted TODOs: ${JSON.stringify(open_todos)}\n`
|
|
247
|
-
: "") +
|
|
248
|
-
(key_context
|
|
249
|
-
? ` Your attempted key_context: ${key_context}\n`
|
|
250
|
-
: "") +
|
|
251
|
-
(active_branch
|
|
252
|
-
? ` Your attempted active_branch: ${active_branch}\n`
|
|
253
|
-
: "") +
|
|
254
|
-
`\nAfter reviewing the latest state, call session_save_handoff again ` +
|
|
255
|
-
`with the updated expected_version.`,
|
|
323
|
+
text: `⚠️ CRDT auto-merge failed for "${project}" after ${MAX_MERGE_ATTEMPTS} attempts ` +
|
|
324
|
+
`due to high contention. Please run session_load_context to see the latest state ` +
|
|
325
|
+
`and try saving again.`,
|
|
256
326
|
}],
|
|
257
327
|
isError: true,
|
|
258
328
|
};
|
|
@@ -388,16 +458,24 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
388
458
|
// Dynamic import itself failed — module not found or similar
|
|
389
459
|
console.error("[FactMerger] Module load failed (non-fatal): " + err));
|
|
390
460
|
}
|
|
461
|
+
// Build response text based on whether a CRDT merge occurred
|
|
462
|
+
const responseText = isMerged
|
|
463
|
+
? `🔄 Auto-merged conflict for "${project}" (v${expected_version} → v${newVersion})\n` +
|
|
464
|
+
`Strategy: ${JSON.stringify(mergeStrategy)}\n` +
|
|
465
|
+
(last_summary ? `Summary: ${last_summary}\n` : "") +
|
|
466
|
+
`\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
|
|
467
|
+
`to maintain concurrency control.`
|
|
468
|
+
: `✅ Handoff ${data.status || "saved"} for project "${project}" ` +
|
|
469
|
+
`(version: ${newVersion})\n` +
|
|
470
|
+
(last_summary ? `Last summary: ${last_summary}\n` : "") +
|
|
471
|
+
(open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
|
|
472
|
+
(active_branch ? `Active branch: ${active_branch}\n` : "") +
|
|
473
|
+
`\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
|
|
474
|
+
`to maintain concurrency control.`;
|
|
391
475
|
return {
|
|
392
476
|
content: [{
|
|
393
477
|
type: "text",
|
|
394
|
-
text:
|
|
395
|
-
`(version: ${newVersion})\n` +
|
|
396
|
-
(last_summary ? `Last summary: ${last_summary}\n` : "") +
|
|
397
|
-
(open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
|
|
398
|
-
(active_branch ? `Active branch: ${active_branch}\n` : "") +
|
|
399
|
-
`\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
|
|
400
|
-
`to maintain concurrency control.`,
|
|
478
|
+
text: responseText,
|
|
401
479
|
}],
|
|
402
480
|
isError: false,
|
|
403
481
|
};
|
|
@@ -587,8 +665,36 @@ export async function sessionLoadContextHandler(args) {
|
|
|
587
665
|
const skillPart = skillLoaded ? ` · 📜 \`${effectiveRole}\` skill loaded` : (effectiveRole ? " · 📜 No skill configured" : "");
|
|
588
666
|
greetingBlock = `\n\n[👤 AGENT IDENTITY]\n${namePart}${rolePart}${skillPart}`;
|
|
589
667
|
}
|
|
668
|
+
// ─── SDM Intuitive Recall (v5.5) ───
|
|
669
|
+
// Generate embedding of current context and fetch latent SDM patterns
|
|
670
|
+
let sdmRecallBlock = "";
|
|
671
|
+
if (level !== "quick" && GOOGLE_API_KEY) {
|
|
672
|
+
try {
|
|
673
|
+
const activeText = [d.last_summary, d.key_context, ...(d.keywords || [])].filter(Boolean).join(" ");
|
|
674
|
+
if (activeText.length > 10) {
|
|
675
|
+
// v2.1 LLM factory handles the API call
|
|
676
|
+
const queryVector = await getLLMProvider().generateEmbedding(activeText);
|
|
677
|
+
// Lazy-load to avoid blocking server boot
|
|
678
|
+
const { getSdmEngine } = await import("../sdm/sdmEngine.js");
|
|
679
|
+
const { decodeSdmVector } = await import("../sdm/sdmDecoder.js");
|
|
680
|
+
const sdmEngine = getSdmEngine(project);
|
|
681
|
+
const targetVector = sdmEngine.read(new Float32Array(queryVector));
|
|
682
|
+
const topMatches = await decodeSdmVector(project, targetVector, 3, 0.55);
|
|
683
|
+
if (topMatches.length > 0) {
|
|
684
|
+
sdmRecallBlock = `\n\n[🧠 INTUITIVE RECALL]\nThe deeper Superposed Memory matrix resonated with your current task and surfaced these latent patterns:\n`;
|
|
685
|
+
for (const match of topMatches) {
|
|
686
|
+
sdmRecallBlock += `- [Sim: ${(match.similarity * 100).toFixed(1)}%] ${match.summary}\n`;
|
|
687
|
+
}
|
|
688
|
+
debugLog(`[session_load_context] SDM Recall surfaced ${topMatches.length} latent patterns`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
debugLog(`[session_load_context] SDM Recall failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
590
696
|
// Build the response object before v4.0 augmentations
|
|
591
|
-
let responseText = `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`;
|
|
697
|
+
let responseText = `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${sdmRecallBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`;
|
|
592
698
|
// ─── v4.0: Behavioral Warnings Injection ───────────────────
|
|
593
699
|
// If loadContext returned behavioral_warnings, add them to the
|
|
594
700
|
// formatted output so the agent sees them prominently.
|
|
@@ -1059,10 +1165,13 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
1059
1165
|
"embedding": "is.null",
|
|
1060
1166
|
"archived_at": "is.null",
|
|
1061
1167
|
user_id: `eq.${PRISM_USER_ID}`,
|
|
1062
|
-
order: "
|
|
1168
|
+
order: "id.asc",
|
|
1063
1169
|
limit: String(safeLimit),
|
|
1064
1170
|
select: "id,summary,decisions,project",
|
|
1065
1171
|
};
|
|
1172
|
+
if (args._cursor_id) {
|
|
1173
|
+
params.id = `gt.${args._cursor_id}`;
|
|
1174
|
+
}
|
|
1066
1175
|
if (project) {
|
|
1067
1176
|
params.project = `eq.${project}`;
|
|
1068
1177
|
}
|
|
@@ -1143,6 +1252,7 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
1143
1252
|
: `All entries now have embeddings for semantic search.`),
|
|
1144
1253
|
}],
|
|
1145
1254
|
isError: false,
|
|
1255
|
+
_stats: { repaired, failed, last_id: entries[entries.length - 1]?.id },
|
|
1146
1256
|
};
|
|
1147
1257
|
}
|
|
1148
1258
|
// ─── Memory History Handler (v2.0 — Time Travel) ─────────────
|
|
@@ -1482,8 +1592,28 @@ export async function sessionHealthCheckHandler(args) {
|
|
|
1482
1592
|
if (embeddingIssue && embeddingIssue.count > 0) {
|
|
1483
1593
|
debugLog("[Health Check] Auto-fixing " + embeddingIssue.count + " missing embeddings...");
|
|
1484
1594
|
try {
|
|
1485
|
-
|
|
1486
|
-
|
|
1595
|
+
let hasMore = true;
|
|
1596
|
+
let cursorId = undefined;
|
|
1597
|
+
while (hasMore) {
|
|
1598
|
+
const result = await backfillEmbeddingsHandler({ dry_run: false, limit: 50, _cursor_id: cursorId });
|
|
1599
|
+
const stats = result._stats;
|
|
1600
|
+
if (stats) {
|
|
1601
|
+
fixedCount += stats.repaired;
|
|
1602
|
+
if (stats.last_id) {
|
|
1603
|
+
cursorId = stats.last_id;
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
hasMore = false;
|
|
1607
|
+
}
|
|
1608
|
+
// If we repaired + failed less than 50, we're done
|
|
1609
|
+
if ((stats.repaired + stats.failed) < 50) {
|
|
1610
|
+
hasMore = false;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
else {
|
|
1614
|
+
hasMore = false; // Fallback if no stats returned
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1487
1617
|
debugLog("[Health Check] Backfill complete.");
|
|
1488
1618
|
}
|
|
1489
1619
|
catch (err) {
|
|
@@ -1940,9 +2070,10 @@ export async function knowledgeSyncRulesHandler(args) {
|
|
|
1940
2070
|
// Resolve both paths to their canonical forms, then assert containment
|
|
1941
2071
|
const resolvedRepo = resolve(normalizedRepoPath);
|
|
1942
2072
|
const targetPath = resolve(resolvedRepo, target_file);
|
|
2073
|
+
const relativePath = relative(resolvedRepo, targetPath);
|
|
1943
2074
|
// Ensure the resolved target is strictly inside the repo root
|
|
1944
2075
|
// (handles "../../../etc/hosts" style traversal)
|
|
1945
|
-
if (
|
|
2076
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
1946
2077
|
return {
|
|
1947
2078
|
content: [{
|
|
1948
2079
|
type: "text",
|
|
@@ -2224,3 +2355,44 @@ export async function deepStoragePurgeHandler(args) {
|
|
|
2224
2355
|
isError: false,
|
|
2225
2356
|
};
|
|
2226
2357
|
}
|
|
2358
|
+
// ─── v5.5: SDM Intuitive Recall Handler ───────────────────────
|
|
2359
|
+
export async function sessionIntuitiveRecallHandler(args) {
|
|
2360
|
+
if (!isSessionIntuitiveRecallArgs(args)) {
|
|
2361
|
+
return {
|
|
2362
|
+
content: [{ type: "text", text: "Invalid arguments for session_intuitive_recall" }],
|
|
2363
|
+
isError: true,
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
try {
|
|
2367
|
+
const { getSdmEngine } = await import("../sdm/sdmEngine.js");
|
|
2368
|
+
const { decodeSdmVector } = await import("../sdm/sdmDecoder.js");
|
|
2369
|
+
const queryVector = await getLLMProvider().generateEmbedding(args.query);
|
|
2370
|
+
const sdmEngine = getSdmEngine(args.project);
|
|
2371
|
+
const targetVector = sdmEngine.read(new Float32Array(queryVector));
|
|
2372
|
+
const limit = args.limit ?? 3;
|
|
2373
|
+
const threshold = args.threshold ?? 0.55;
|
|
2374
|
+
const topMatches = await decodeSdmVector(args.project, targetVector, limit, threshold);
|
|
2375
|
+
let recallBlock = `🧠 **SDM Intuitive Recall for "${args.project}"**\n\n`;
|
|
2376
|
+
recallBlock += `Query: "${args.query}"\n`;
|
|
2377
|
+
recallBlock += `Target vector generated. Scanning ${topMatches.length > 0 ? topMatches.length + " latents surfaced." : "No strong patterns surfaced."}\n\n`;
|
|
2378
|
+
if (topMatches.length === 0) {
|
|
2379
|
+
recallBlock += `*No stored patterns resonated above the ${(threshold * 100).toFixed(1)}% similarity threshold.*`;
|
|
2380
|
+
}
|
|
2381
|
+
else {
|
|
2382
|
+
for (const match of topMatches) {
|
|
2383
|
+
recallBlock += `- [Similarity: ${(match.similarity * 100).toFixed(1)}%] ${match.summary}\n`;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return {
|
|
2387
|
+
content: [{ type: "text", text: recallBlock }],
|
|
2388
|
+
isError: false,
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
catch (err) {
|
|
2392
|
+
debugLog(`[session_intuitive_recall] Failed: ${err}`);
|
|
2393
|
+
return {
|
|
2394
|
+
content: [{ type: "text", text: `Error triggering Intuitive Recall: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2395
|
+
isError: true,
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
}
|
package/dist/utils/briefing.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* ═══════════════════════════════════════════════════════════════════
|
|
8
8
|
* DESIGN DECISIONS:
|
|
9
|
-
* - Uses gemini-2.
|
|
9
|
+
* - Uses gemini-2.5-flash for max speed (~2-3s generation)
|
|
10
10
|
* - Graceful fallback if no API key or Gemini call fails
|
|
11
11
|
* - Prompt is tuned for brevity — exactly 3 bullets, no fluff
|
|
12
12
|
* - Reuses GOOGLE_API_KEY from config.ts (same key as embeddings)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRDT Handoff Merge Engine (v5.4 Phase 2)
|
|
3
|
+
*
|
|
4
|
+
* Provides a zero-dependency, in-memory OR-Map implementation for resolving
|
|
5
|
+
* concurrent agent state changes deterministically.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Arrays (TODOs, decisions, keywords) use Add-Wins OR-Set semantics.
|
|
9
|
+
* - Tombstones are ephemeral: calculated via 3-way diff against the base state.
|
|
10
|
+
* - Scalars (summary, context) use Last-Writer-Wins (LWW) Register semantics.
|
|
11
|
+
*
|
|
12
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
13
|
+
* REVIEWER NOTE (v5.4):
|
|
14
|
+
*
|
|
15
|
+
* WHY BESPOKE, NOT YJS/AUTOMERGE?
|
|
16
|
+
* - Our merge surface is tiny: 6 fields (3 arrays, 2 scalars, 1 optional map).
|
|
17
|
+
* - Yjs adds ~50-200KB to the bundle and requires a stateful document model.
|
|
18
|
+
* - Automerge's WASM runtime would break MCP cold-start (<1s requirement).
|
|
19
|
+
* - This module is ~100 LOC, zero deps, and fully deterministic.
|
|
20
|
+
*
|
|
21
|
+
* TOMBSTONE STRATEGY:
|
|
22
|
+
* Tombstones are computed in-memory by diffing the `base` state against each
|
|
23
|
+
* agent's submission. They exist only for the duration of the merge operation —
|
|
24
|
+
* no database columns, no growing tombstone tables, no cleanup cron jobs.
|
|
25
|
+
* This works because handoff state is a live document (upserted, not appended).
|
|
26
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
27
|
+
*/
|
|
28
|
+
// ─── OR-Set Logic (Add-Wins) ────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// 3-way set merge:
|
|
31
|
+
// added_by_incoming = incoming - base
|
|
32
|
+
// removed_by_incoming = base - incoming
|
|
33
|
+
// added_by_current = current - base
|
|
34
|
+
// removed_by_current = base - current
|
|
35
|
+
// result = (base - removals) ∪ all_adds
|
|
36
|
+
//
|
|
37
|
+
// "Add-Wins" means: if Agent A removes "X" and Agent B adds "X",
|
|
38
|
+
// the add wins. This is safe for TODOs (better to have a duplicate
|
|
39
|
+
// than lose work) and keywords (idempotent).
|
|
40
|
+
function mergeArray(b = [], i = [], c = []) {
|
|
41
|
+
const bSet = new Set(b);
|
|
42
|
+
const iSet = new Set(i);
|
|
43
|
+
const cSet = new Set(c);
|
|
44
|
+
const removals = new Set();
|
|
45
|
+
const adds = new Set();
|
|
46
|
+
// Items explicitly removed by either agent
|
|
47
|
+
for (const item of bSet) {
|
|
48
|
+
if (!iSet.has(item))
|
|
49
|
+
removals.add(item);
|
|
50
|
+
if (!cSet.has(item))
|
|
51
|
+
removals.add(item);
|
|
52
|
+
}
|
|
53
|
+
// Items freshly added by either agent
|
|
54
|
+
for (const item of iSet)
|
|
55
|
+
if (!bSet.has(item))
|
|
56
|
+
adds.add(item);
|
|
57
|
+
for (const item of cSet)
|
|
58
|
+
if (!bSet.has(item))
|
|
59
|
+
adds.add(item);
|
|
60
|
+
// Final state: (Base - Removals) ∪ Adds
|
|
61
|
+
const result = new Set();
|
|
62
|
+
for (const item of bSet)
|
|
63
|
+
if (!removals.has(item))
|
|
64
|
+
result.add(item);
|
|
65
|
+
for (const item of adds)
|
|
66
|
+
result.add(item);
|
|
67
|
+
return Array.from(result);
|
|
68
|
+
}
|
|
69
|
+
// ─── LWW Register Logic ────────────────────────────────────────
|
|
70
|
+
//
|
|
71
|
+
// If the incoming agent changed a scalar, its value wins (latest intent).
|
|
72
|
+
// If the incoming agent left it untouched, the current DB value wins.
|
|
73
|
+
// If both changed it, incoming wins (the caller is the latest writer).
|
|
74
|
+
function mergeScalar(b, i, c) {
|
|
75
|
+
const incomingChanged = i !== b;
|
|
76
|
+
const currentChanged = c !== b;
|
|
77
|
+
if (!incomingChanged && !currentChanged) {
|
|
78
|
+
return { value: c, winner: "no-change" };
|
|
79
|
+
}
|
|
80
|
+
if (incomingChanged) {
|
|
81
|
+
return { value: i, winner: "lww-incoming" };
|
|
82
|
+
}
|
|
83
|
+
return { value: c, winner: "lww-current" };
|
|
84
|
+
}
|
|
85
|
+
// ─── Main Merge Function ────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* 3-Way CRDT Merge for Handoff State.
|
|
88
|
+
*
|
|
89
|
+
* @param base The state both agents read (at the conflicting version).
|
|
90
|
+
* If null, treated as an empty state (first handoff).
|
|
91
|
+
* @param incoming The state submitted by the agent that lost the OCC race.
|
|
92
|
+
* @param current The state currently in the DB (the OCC winner).
|
|
93
|
+
* @returns The conflict-free merged state + per-field audit trail.
|
|
94
|
+
*/
|
|
95
|
+
export function mergeHandoff(base, incoming, current) {
|
|
96
|
+
const safeBase = base || { summary: "" };
|
|
97
|
+
const strategy = {};
|
|
98
|
+
// ─── Scalars (LWW) ───
|
|
99
|
+
const summaryMerge = mergeScalar(safeBase.summary, incoming.summary, current.summary);
|
|
100
|
+
const branchMerge = mergeScalar(safeBase.active_branch, incoming.active_branch, current.active_branch);
|
|
101
|
+
const contextMerge = mergeScalar(safeBase.key_context, incoming.key_context, current.key_context);
|
|
102
|
+
strategy.summary = summaryMerge.winner;
|
|
103
|
+
strategy.active_branch = branchMerge.winner;
|
|
104
|
+
strategy.key_context = contextMerge.winner;
|
|
105
|
+
// ─── Arrays (OR-Set) ───
|
|
106
|
+
const mergedTodos = mergeArray(safeBase.pending_todo || [], incoming.pending_todo || [], current.pending_todo || []);
|
|
107
|
+
const mergedDecisions = mergeArray(safeBase.active_decisions || [], incoming.active_decisions || [], current.active_decisions || []);
|
|
108
|
+
const mergedKeywords = mergeArray(safeBase.keywords || [], incoming.keywords || [], current.keywords || []);
|
|
109
|
+
strategy.pending_todo = "or-set-union";
|
|
110
|
+
strategy.active_decisions = "or-set-union";
|
|
111
|
+
strategy.keywords = "or-set-union";
|
|
112
|
+
const merged = {
|
|
113
|
+
summary: summaryMerge.value || current.summary || "",
|
|
114
|
+
active_branch: branchMerge.value,
|
|
115
|
+
key_context: contextMerge.value,
|
|
116
|
+
pending_todo: mergedTodos,
|
|
117
|
+
active_decisions: mergedDecisions,
|
|
118
|
+
keywords: mergedKeywords,
|
|
119
|
+
};
|
|
120
|
+
return { merged, strategy };
|
|
121
|
+
}
|
|
122
|
+
// ─── Adapter: DB Record → HandoffSchema ─────────────────────────
|
|
123
|
+
//
|
|
124
|
+
// Extracts the merge-relevant fields from either a loadContext result
|
|
125
|
+
// or a history snapshot. Tolerant of both {last_summary} and {summary}
|
|
126
|
+
// naming conventions (handler vs. DB column inconsistency).
|
|
127
|
+
export function dbToHandoffSchema(dbState) {
|
|
128
|
+
if (!dbState)
|
|
129
|
+
return null;
|
|
130
|
+
const toStringArray = (v) => {
|
|
131
|
+
if (Array.isArray(v))
|
|
132
|
+
return v;
|
|
133
|
+
if (typeof v === "string") {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = JSON.parse(v);
|
|
136
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
summary: dbState.last_summary || dbState.summary || "",
|
|
146
|
+
active_branch: dbState.active_branch ?? null,
|
|
147
|
+
key_context: dbState.key_context ?? null,
|
|
148
|
+
pending_todo: toStringArray(dbState.pending_todo),
|
|
149
|
+
active_decisions: toStringArray(dbState.active_decisions),
|
|
150
|
+
keywords: toStringArray(dbState.keywords),
|
|
151
|
+
};
|
|
152
|
+
}
|