@jungjaehoon/mama-core 1.3.2 → 1.4.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 +67 -19
- package/db/migrations/001-initial-decision-graph.sql +5 -0
- package/db/migrations/014-add-is-static-column.sql +5 -0
- package/db/migrations/015-add-fts5-search.sql +34 -0
- package/db/migrations/016-add-memory-v2-columns.sql +14 -0
- package/db/migrations/017-create-memory-scopes.sql +10 -0
- package/db/migrations/018-create-memory-scope-bindings.sql +15 -0
- package/db/migrations/019-create-memory-events.sql +18 -0
- package/db/migrations/020-create-audit-findings.sql +18 -0
- package/db/migrations/021-create-memory-truth.sql +20 -0
- package/db/migrations/022-create-channel-summaries.sql +9 -0
- package/db/migrations/023-create-channel-summary-state.sql +9 -0
- package/db/migrations/024-add-event-date.sql +9 -0
- package/dist/db-adapter/base-adapter.d.ts +1 -1
- package/dist/db-adapter/index.js +2 -8
- package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -1
- package/dist/db-adapter/node-sqlite-adapter.js +99 -45
- package/dist/db-manager.d.ts +30 -2
- package/dist/db-manager.js +131 -6
- package/dist/decision-formatter.d.ts +2 -0
- package/dist/embedding-server/mobile/session-manager.js +22 -7
- package/dist/index.d.ts +5 -1
- package/dist/index.js +31 -3
- package/dist/mama-api.d.ts +67 -2
- package/dist/mama-api.js +295 -80
- package/dist/memory/api.d.ts +81 -0
- package/dist/memory/api.js +972 -0
- package/dist/memory/bootstrap-builder.d.ts +10 -0
- package/dist/memory/bootstrap-builder.js +66 -0
- package/dist/memory/channel-summary-state-store.d.ts +19 -0
- package/dist/memory/channel-summary-state-store.js +223 -0
- package/dist/memory/channel-summary-store.d.ts +8 -0
- package/dist/memory/channel-summary-store.js +41 -0
- package/dist/memory/event-store.d.ts +5 -0
- package/dist/memory/event-store.js +72 -0
- package/dist/memory/evolution-engine.d.ts +22 -0
- package/dist/memory/evolution-engine.js +133 -0
- package/dist/memory/extraction-prompt.d.ts +4 -0
- package/dist/memory/extraction-prompt.js +89 -0
- package/dist/memory/finding-store.d.ts +5 -0
- package/dist/memory/finding-store.js +75 -0
- package/dist/memory/profile-builder.d.ts +3 -0
- package/dist/memory/profile-builder.js +23 -0
- package/dist/memory/scope-store.d.ts +5 -0
- package/dist/memory/scope-store.js +13 -0
- package/dist/memory/truth-store.d.ts +11 -0
- package/dist/memory/truth-store.js +102 -0
- package/dist/memory/types.d.ts +267 -0
- package/dist/memory/types.js +74 -0
- package/package.json +4 -1
package/dist/mama-api.js
CHANGED
|
@@ -22,6 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
22
22
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.listRecentMemoryEvents = exports.listOpenAuditFindings = exports.getChannelSummary = exports.upsertChannelSummary = exports.recordMemoryAudit = exports.createAuditAck = exports.buildMemoryBootstrap = exports.evolveMemory = exports.ingestMemory = exports.buildProfile = exports.recallMemory = exports.saveMemory = void 0;
|
|
25
26
|
exports.save = save;
|
|
26
27
|
exports.suggest = suggest;
|
|
27
28
|
exports.list = listDecisions;
|
|
@@ -56,12 +57,29 @@ const os_1 = __importDefault(require("os"));
|
|
|
56
57
|
const crypto_1 = __importDefault(require("crypto"));
|
|
57
58
|
// Internal modules
|
|
58
59
|
const decision_tracker_js_1 = require("./decision-tracker.js");
|
|
60
|
+
const db_manager_js_1 = require("./db-manager.js");
|
|
59
61
|
const memory_store_js_1 = require("./memory-store.js");
|
|
62
|
+
const db_manager_js_2 = require("./db-manager.js");
|
|
60
63
|
const decision_formatter_js_1 = require("./decision-formatter.js");
|
|
61
64
|
const progress_indicator_js_1 = require("./progress-indicator.js");
|
|
62
65
|
const embeddings_js_1 = require("./embeddings.js");
|
|
63
66
|
const ollama_client_js_1 = require("./ollama-client.js");
|
|
64
67
|
const debug_logger_js_1 = require("./debug-logger.js");
|
|
68
|
+
const api_js_1 = require("./memory/api.js");
|
|
69
|
+
Object.defineProperty(exports, "saveMemory", { enumerable: true, get: function () { return api_js_1.saveMemory; } });
|
|
70
|
+
Object.defineProperty(exports, "recallMemory", { enumerable: true, get: function () { return api_js_1.recallMemory; } });
|
|
71
|
+
Object.defineProperty(exports, "buildProfile", { enumerable: true, get: function () { return api_js_1.buildProfile; } });
|
|
72
|
+
Object.defineProperty(exports, "ingestMemory", { enumerable: true, get: function () { return api_js_1.ingestMemory; } });
|
|
73
|
+
Object.defineProperty(exports, "evolveMemory", { enumerable: true, get: function () { return api_js_1.evolveMemory; } });
|
|
74
|
+
Object.defineProperty(exports, "buildMemoryBootstrap", { enumerable: true, get: function () { return api_js_1.buildMemoryBootstrap; } });
|
|
75
|
+
Object.defineProperty(exports, "createAuditAck", { enumerable: true, get: function () { return api_js_1.createAuditAck; } });
|
|
76
|
+
Object.defineProperty(exports, "recordMemoryAudit", { enumerable: true, get: function () { return api_js_1.recordMemoryAudit; } });
|
|
77
|
+
Object.defineProperty(exports, "upsertChannelSummary", { enumerable: true, get: function () { return api_js_1.upsertChannelSummary; } });
|
|
78
|
+
Object.defineProperty(exports, "getChannelSummary", { enumerable: true, get: function () { return api_js_1.getChannelSummary; } });
|
|
79
|
+
const finding_store_js_1 = require("./memory/finding-store.js");
|
|
80
|
+
Object.defineProperty(exports, "listOpenAuditFindings", { enumerable: true, get: function () { return finding_store_js_1.listOpenAuditFindings; } });
|
|
81
|
+
const event_store_js_1 = require("./memory/event-store.js");
|
|
82
|
+
Object.defineProperty(exports, "listRecentMemoryEvents", { enumerable: true, get: function () { return event_store_js_1.listRecentMemoryEvents; } });
|
|
65
83
|
// Session-level warning cooldown cache (Story 1.1, 1.2)
|
|
66
84
|
// Prevents spam by tracking warned topics per session
|
|
67
85
|
const warnedTopicsCache = new Map();
|
|
@@ -93,7 +111,7 @@ const WARNING_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
93
111
|
* outcome: 'success'
|
|
94
112
|
* });
|
|
95
113
|
*/
|
|
96
|
-
async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user_decision', outcome = 'pending', failure_reason = null, limitation = null, trust_context = null, }) {
|
|
114
|
+
async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user_decision', outcome = 'pending', failure_reason = null, limitation = null, trust_context: _trust_context = null, is_static, scopes: inputScopes, event_date, }) {
|
|
97
115
|
// Validate required fields
|
|
98
116
|
if (!topic || typeof topic !== 'string') {
|
|
99
117
|
throw new Error('mama.save() requires topic (string)');
|
|
@@ -117,42 +135,49 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
|
|
|
117
135
|
if (outcome && !validOutcomes.includes(outcome)) {
|
|
118
136
|
throw new Error(`mama.save() outcome must be one of: ${validOutcomes.join(', ')} (got: ${outcome})`);
|
|
119
137
|
}
|
|
138
|
+
// Validate scopes shape
|
|
139
|
+
if (inputScopes !== undefined && inputScopes !== null) {
|
|
140
|
+
if (!Array.isArray(inputScopes)) {
|
|
141
|
+
throw new Error('mama.save() scopes must be an array');
|
|
142
|
+
}
|
|
143
|
+
const validKinds = ['global', 'user', 'channel', 'project'];
|
|
144
|
+
for (const scope of inputScopes) {
|
|
145
|
+
if (!scope ||
|
|
146
|
+
typeof scope !== 'object' ||
|
|
147
|
+
typeof scope.kind !== 'string' ||
|
|
148
|
+
typeof scope.id !== 'string') {
|
|
149
|
+
throw new Error('mama.save() each scope must have kind (string) and id (string)');
|
|
150
|
+
}
|
|
151
|
+
if (typeof scope.id === 'string' && scope.id.trim().length === 0) {
|
|
152
|
+
throw new Error('mama.save() each scope.id must be a non-empty string');
|
|
153
|
+
}
|
|
154
|
+
if (!validKinds.includes(scope.kind)) {
|
|
155
|
+
throw new Error(`mama.save() scope kind must be one of: ${validKinds.join(', ')} (got: ${scope.kind})`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
120
159
|
// Map type to user_involvement field
|
|
121
160
|
// Note: Current schema uses user_involvement ('requested', 'approved', 'rejected')
|
|
122
161
|
// Future: Will use decision_type column for proper distinction
|
|
123
162
|
const _userInvolvement = type === 'user_decision' ? 'approved' : null;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const detection = {
|
|
163
|
+
(0, progress_indicator_js_1.logProgress)(`Saving decision: ${topic.substring(0, 30)}...`);
|
|
164
|
+
const { id: decisionId } = await (0, api_js_1.saveMemory)({
|
|
127
165
|
topic,
|
|
128
|
-
decision,
|
|
129
|
-
|
|
166
|
+
kind: is_static === 1 ? 'preference' : 'decision',
|
|
167
|
+
summary: decision,
|
|
168
|
+
details: reasoning,
|
|
130
169
|
confidence,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
tool_input: { topic, decision },
|
|
139
|
-
exit_code: 0,
|
|
140
|
-
session_id: sessionId,
|
|
141
|
-
timestamp: Date.now(),
|
|
142
|
-
};
|
|
143
|
-
// Create session context
|
|
144
|
-
const sessionContext = {
|
|
145
|
-
session_id: sessionId,
|
|
146
|
-
latest_user_message: `Save ${type}: ${topic}`,
|
|
147
|
-
recent_exchange: `Claude: ${reasoning.substring(0, 100)}...`,
|
|
148
|
-
};
|
|
149
|
-
// Call internal learnDecision function
|
|
150
|
-
// Note: learnDecision returns { decisionId, notification }
|
|
151
|
-
(0, progress_indicator_js_1.logProgress)(`Saving decision: ${topic.substring(0, 30)}...`);
|
|
152
|
-
const { decisionId } = await (0, decision_tracker_js_1.learnDecision)(detection, toolExecution, sessionContext);
|
|
170
|
+
scopes: Array.isArray(inputScopes) && inputScopes.length > 0 ? inputScopes : [],
|
|
171
|
+
source: {
|
|
172
|
+
package: 'mama-core',
|
|
173
|
+
source_type: 'legacy_save',
|
|
174
|
+
},
|
|
175
|
+
eventDate: event_date ?? undefined,
|
|
176
|
+
});
|
|
153
177
|
(0, progress_indicator_js_1.logComplete)(`Decision saved: ${decisionId.substring(0, 20)}...`);
|
|
154
178
|
// Update user_involvement, outcome, failure_reason, limitation
|
|
155
179
|
// Note: learnDecision always sets 'requested', we need to override it
|
|
180
|
+
await (0, db_manager_js_2.initDB)();
|
|
156
181
|
const adapter = (0, memory_store_js_1.getAdapter)();
|
|
157
182
|
// Build UPDATE query dynamically based on what fields are provided
|
|
158
183
|
const updates = [];
|
|
@@ -189,6 +214,11 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
|
|
|
189
214
|
updates.push('limitation = ?');
|
|
190
215
|
values.push(limitation);
|
|
191
216
|
}
|
|
217
|
+
// is_static (user profile marker)
|
|
218
|
+
if (is_static !== undefined) {
|
|
219
|
+
updates.push('is_static = ?');
|
|
220
|
+
values.push(is_static);
|
|
221
|
+
}
|
|
192
222
|
// Execute UPDATE if we have any fields to update
|
|
193
223
|
if (updates.length > 0) {
|
|
194
224
|
values.push(decisionId); // WHERE id = ?
|
|
@@ -209,53 +239,58 @@ async function save({ topic, decision, reasoning, confidence = 0.5, type = 'user
|
|
|
209
239
|
let reasoning_graph = null;
|
|
210
240
|
// Only run auto-search for decisions (not checkpoints) with a topic
|
|
211
241
|
if (topic) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
// Skip global similarity search and reasoning graph for scoped saves
|
|
243
|
+
if (!inputScopes || !Array.isArray(inputScopes) || inputScopes.length === 0) {
|
|
244
|
+
try {
|
|
245
|
+
// Story 1.1: Auto-search using suggest()
|
|
246
|
+
// NOTE: suggest() searches globally and does not yet support scoped similarity search.
|
|
247
|
+
// Cross-scope suggestions are possible here. Track as a follow-up.
|
|
248
|
+
(0, progress_indicator_js_1.logSearching)('Searching for related decisions...');
|
|
249
|
+
const searchResults = await suggest(topic, {
|
|
250
|
+
limit: 3,
|
|
251
|
+
threshold: 0.7,
|
|
252
|
+
disableRecency: true, // Pure semantic similarity for comparison
|
|
253
|
+
});
|
|
254
|
+
// Handle suggest() result which can be string | null | object
|
|
255
|
+
if (searchResults && typeof searchResults === 'object' && 'results' in searchResults) {
|
|
256
|
+
// Filter out the decision we just saved
|
|
257
|
+
similar_decisions = searchResults.results
|
|
258
|
+
.filter((d) => d.id !== decisionId)
|
|
259
|
+
.map((d) => ({
|
|
260
|
+
id: d.id,
|
|
261
|
+
topic: d.topic,
|
|
262
|
+
decision: d.decision,
|
|
263
|
+
similarity: d.similarity,
|
|
264
|
+
created_at: d.created_at,
|
|
265
|
+
}));
|
|
266
|
+
if (similar_decisions.length > 0) {
|
|
267
|
+
(0, progress_indicator_js_1.logComplete)(`Found ${similar_decisions.length} related decision(s)`);
|
|
268
|
+
}
|
|
269
|
+
// Story 1.2: Warning logic (similarity >= 0.85)
|
|
270
|
+
const highSimilarity = similar_decisions.find((d) => (d.similarity ?? 0) >= 0.85);
|
|
271
|
+
if (highSimilarity && !_isTopicInCooldown(topic)) {
|
|
272
|
+
warning = `High similarity (${((highSimilarity.similarity ?? 0) * 100).toFixed(0)}%) with existing decision "${highSimilarity.decision.substring(0, 50)}..."`;
|
|
273
|
+
_markTopicWarned(topic);
|
|
274
|
+
}
|
|
275
|
+
// Story 1.2: Collaboration hint
|
|
276
|
+
if (similar_decisions.length > 0) {
|
|
277
|
+
collaboration_hint = _generateCollaborationHint(similar_decisions);
|
|
278
|
+
}
|
|
244
279
|
}
|
|
245
280
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
281
|
+
catch (error) {
|
|
282
|
+
// Story 1.1 AC3: Best-effort - save succeeds even if auto-search fails
|
|
283
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
284
|
+
(0, debug_logger_js_1.error)('Auto-search failed:', errMsg);
|
|
285
|
+
}
|
|
286
|
+
// Story 1.2: Reasoning graph info
|
|
287
|
+
try {
|
|
288
|
+
reasoning_graph = await _getReasoningGraphInfo(topic, decisionId);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
292
|
+
(0, debug_logger_js_1.error)('Reasoning graph query failed:', errMsg);
|
|
293
|
+
}
|
|
259
294
|
}
|
|
260
295
|
// Story 2.2: Parse reasoning for relationship edges (builds_on, debates, synthesizes)
|
|
261
296
|
if (reasoning) {
|
|
@@ -676,6 +711,71 @@ async function suggest(userQuestion, options = {}) {
|
|
|
676
711
|
disableRecency = false, // Set true to disable recency boosting entirely
|
|
677
712
|
} = options;
|
|
678
713
|
try {
|
|
714
|
+
const bundle = await (0, api_js_1.recallMemory)(userQuestion, {
|
|
715
|
+
includeProfile: false,
|
|
716
|
+
topicPrefix: options.topicPrefix,
|
|
717
|
+
...(options.scopes && { scopes: options.scopes }),
|
|
718
|
+
});
|
|
719
|
+
if (bundle.memories.length > 0) {
|
|
720
|
+
// recallMemory uses RRF fusion — confidence is overwritten with the normalized
|
|
721
|
+
// retrieval score (0-1 range, where 1.0 = best match in this result set).
|
|
722
|
+
// The original stored confidence is lost after RRF normalization.
|
|
723
|
+
// We capture the retrieval score separately so `similarity` reflects search
|
|
724
|
+
// relevance while `confidence` is passed through as-is from the bundle.
|
|
725
|
+
let filteredMemories = bundle.memories;
|
|
726
|
+
// Apply limit
|
|
727
|
+
filteredMemories = filteredMemories.slice(0, limit);
|
|
728
|
+
if (format === 'markdown') {
|
|
729
|
+
const context = filteredMemories
|
|
730
|
+
.map((memory, index) => `${index + 1}. [${memory.topic}] ${memory.summary}\n ${memory.details}`)
|
|
731
|
+
.join('\n');
|
|
732
|
+
return `🔍 Search method: memory_v2\n${context}`;
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
query: userQuestion,
|
|
736
|
+
results: filteredMemories.map((memory, idx) => ({
|
|
737
|
+
id: memory.id,
|
|
738
|
+
topic: memory.topic,
|
|
739
|
+
decision: memory.summary,
|
|
740
|
+
reasoning: memory.details,
|
|
741
|
+
confidence: memory.confidence,
|
|
742
|
+
// memory.confidence is the normalized RRF retrieval score (0-1) after
|
|
743
|
+
// recallMemory. Use it as similarity so the field reflects search relevance
|
|
744
|
+
// rather than the original stored trust score.
|
|
745
|
+
similarity: memory.confidence ?? 1 - idx * 0.01,
|
|
746
|
+
created_at: memory.created_at,
|
|
747
|
+
event_date: memory.event_date ?? null,
|
|
748
|
+
graph_source: 'primary',
|
|
749
|
+
graph_rank: 1,
|
|
750
|
+
related_to: null,
|
|
751
|
+
edge_reason: null,
|
|
752
|
+
})),
|
|
753
|
+
meta: {
|
|
754
|
+
count: filteredMemories.length,
|
|
755
|
+
search_method: 'memory_v2',
|
|
756
|
+
threshold: threshold || 'adaptive',
|
|
757
|
+
recency_boost: disableRecency
|
|
758
|
+
? null
|
|
759
|
+
: {
|
|
760
|
+
weight: recencyWeight,
|
|
761
|
+
scale: recencyScale,
|
|
762
|
+
decay: recencyDecay,
|
|
763
|
+
},
|
|
764
|
+
graph_expansion: {
|
|
765
|
+
total_results: filteredMemories.length,
|
|
766
|
+
primary_count: bundle.graph_context.primary.length,
|
|
767
|
+
expanded_count: bundle.graph_context.expanded.length,
|
|
768
|
+
sources: {
|
|
769
|
+
primary: bundle.graph_context.primary.length,
|
|
770
|
+
supersedes_chain: 0,
|
|
771
|
+
refines: 0,
|
|
772
|
+
refined_by: 0,
|
|
773
|
+
contradicts: 0,
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
}
|
|
679
779
|
// 1. Try vector search first (if sqlite-vss is available)
|
|
680
780
|
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-explicit-any
|
|
681
781
|
let results = [];
|
|
@@ -694,6 +794,37 @@ async function suggest(userQuestion, options = {}) {
|
|
|
694
794
|
results = await (0, memory_store_js_1.vectorSearch)(queryEmbedding, limit * 2, 0.5); // Get more candidates
|
|
695
795
|
// Filter by adaptive threshold
|
|
696
796
|
results = results.filter((r) => r.similarity >= adaptiveThreshold);
|
|
797
|
+
// Stage 1.4: Temporal boost — detect time-related queries and boost matching results
|
|
798
|
+
{
|
|
799
|
+
const temporalPatterns = [
|
|
800
|
+
// English
|
|
801
|
+
/\b(yesterday|today|last\s+(?:week|month|year)|(\d+)\s+(?:days?|weeks?|months?)\s+ago)\b/i,
|
|
802
|
+
/\b(before|after|since|until|during)\s+\w+/i,
|
|
803
|
+
/\b(how\s+long|when\s+did|what\s+date|what\s+day)\b/i,
|
|
804
|
+
// Korean
|
|
805
|
+
/(?:어제|오늘|그제|지난\s*(?:주|달|해)|(\d+)\s*(?:일|주|달|개월)\s*(?:전|후|뒤))/,
|
|
806
|
+
/(?:언제|얼마나|며칠|몇\s*(?:일|주|달|개월))/,
|
|
807
|
+
];
|
|
808
|
+
const isTemporalQuery = temporalPatterns.some((p) => p.test(userQuestion));
|
|
809
|
+
if (isTemporalQuery && results.length > 0) {
|
|
810
|
+
// Boost results that contain date/time references in their content
|
|
811
|
+
const datePatterns = [
|
|
812
|
+
/\d{4}[-/]\d{1,2}[-/]\d{1,2}/,
|
|
813
|
+
/(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\w*\s+\d/i,
|
|
814
|
+
/\d+\s*(?:일|월|년|주|시간|분)/,
|
|
815
|
+
/(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)/i,
|
|
816
|
+
/(?:월요일|화요일|수요일|목요일|금요일|토요일|일요일)/,
|
|
817
|
+
];
|
|
818
|
+
for (const result of results) {
|
|
819
|
+
const content = `${result.decision || ''} ${result.reasoning || ''}`;
|
|
820
|
+
const hasDateRef = datePatterns.some((p) => p.test(content));
|
|
821
|
+
if (hasDateRef) {
|
|
822
|
+
result.similarity = Math.min(1.0, (result.similarity || 0) + 0.1);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
|
|
826
|
+
}
|
|
827
|
+
}
|
|
697
828
|
// Stage 1.5: Apply recency boosting (Gaussian Decay)
|
|
698
829
|
// Allows Claude to adjust search strategy (recent vs historical)
|
|
699
830
|
if (results.length > 0 && !disableRecency) {
|
|
@@ -705,6 +836,52 @@ async function suggest(userQuestion, options = {}) {
|
|
|
705
836
|
});
|
|
706
837
|
searchMethod = 'vector+recency';
|
|
707
838
|
}
|
|
839
|
+
// Stage 1.7: FTS5 hybrid merge (Haiku Memory Layer)
|
|
840
|
+
{
|
|
841
|
+
try {
|
|
842
|
+
const ftsResults = await (0, db_manager_js_1.fts5Search)(userQuestion, limit * 2);
|
|
843
|
+
if (ftsResults.length > 0) {
|
|
844
|
+
// Normalize FTS5 ranks (BM25 returns negative values, closer to 0 = better)
|
|
845
|
+
const maxRank = Math.max(...ftsResults.map((r) => Math.abs(r.rank)));
|
|
846
|
+
const ftsMap = new Map(ftsResults.map((r) => [r.id, maxRank > 0 ? 1 - Math.abs(r.rank) / maxRank : 0.5]));
|
|
847
|
+
// Tunable hybrid weights (env: MAMA_VECTOR_WEIGHT, MAMA_FTS5_WEIGHT)
|
|
848
|
+
const vectorWeight = parseFloat(process.env.MAMA_VECTOR_WEIGHT || '0.6');
|
|
849
|
+
const fts5Weight = parseFloat(process.env.MAMA_FTS5_WEIGHT || '0.4');
|
|
850
|
+
// Merge: boost existing results that also matched FTS5
|
|
851
|
+
for (const result of results) {
|
|
852
|
+
const ftsScore = ftsMap.get(result.id);
|
|
853
|
+
if (ftsScore !== undefined) {
|
|
854
|
+
result.similarity = vectorWeight * result.similarity + fts5Weight * ftsScore;
|
|
855
|
+
ftsMap.delete(result.id);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// Add FTS5-only results (not in embedding results)
|
|
859
|
+
for (const [id, ftsScore] of ftsMap) {
|
|
860
|
+
const ftsResult = ftsResults.find((r) => r.id === id);
|
|
861
|
+
if (ftsResult) {
|
|
862
|
+
// Need to get full decision record
|
|
863
|
+
const adapter = (0, memory_store_js_1.getAdapter)();
|
|
864
|
+
const stmt = adapter.prepare('SELECT * FROM decisions WHERE id = ? AND superseded_by IS NULL');
|
|
865
|
+
const decision = stmt.get(id);
|
|
866
|
+
if (decision) {
|
|
867
|
+
results.push({
|
|
868
|
+
...decision,
|
|
869
|
+
similarity: fts5Weight * ftsScore, // Only FTS5 score component
|
|
870
|
+
graph_source: 'fts5',
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// Re-sort by similarity
|
|
876
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
877
|
+
results.sort((a, b) => (b.similarity || 0) - (a.similarity || 0));
|
|
878
|
+
searchMethod = disableRecency ? 'vector+fts5' : 'vector+recency+fts5';
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
// FTS5 not available, continue with embedding-only results
|
|
883
|
+
}
|
|
884
|
+
}
|
|
708
885
|
// Stage 2: Graph expansion (NEW - Phase 1)
|
|
709
886
|
// Expand candidates with supersedes chain and semantic edges
|
|
710
887
|
if (results.length > 0) {
|
|
@@ -712,6 +889,14 @@ async function suggest(userQuestion, options = {}) {
|
|
|
712
889
|
results = graphEnhanced;
|
|
713
890
|
searchMethod = disableRecency ? 'vector+graph' : 'vector+recency+graph';
|
|
714
891
|
}
|
|
892
|
+
// Stage 2.5: is_static boost (after graph expansion to preserve sort order)
|
|
893
|
+
for (const result of results) {
|
|
894
|
+
if (result.is_static === 1) {
|
|
895
|
+
result.final_score = Math.min(1.0, (result.final_score ?? result.similarity ?? 0) + 0.2);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
// Re-sort by final_score after is_static boost
|
|
899
|
+
results.sort((a, b) => (b.final_score ?? b.similarity ?? 0) - (a.final_score ?? a.similarity ?? 0));
|
|
715
900
|
}
|
|
716
901
|
catch (vectorError) {
|
|
717
902
|
// Fallback to keyword search if vector search unavailable
|
|
@@ -873,13 +1058,30 @@ async function listDecisions(options = {}) {
|
|
|
873
1058
|
const { limit = 10, format = 'json' } = options;
|
|
874
1059
|
try {
|
|
875
1060
|
const adapter = (0, memory_store_js_1.getAdapter)();
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1061
|
+
let decisions;
|
|
1062
|
+
if (options.scopes && options.scopes.length > 0) {
|
|
1063
|
+
// Scope-filtered query: JOIN memory_scope_bindings + memory_scopes
|
|
1064
|
+
const scopeIds = await Promise.all(options.scopes.map((s) => (0, db_manager_js_1.ensureMemoryScope)(s.kind, s.id)));
|
|
1065
|
+
const placeholders = scopeIds.map(() => '?').join(', ');
|
|
1066
|
+
const stmt = adapter.prepare(`
|
|
1067
|
+
SELECT DISTINCT d.* FROM decisions d
|
|
1068
|
+
JOIN memory_scope_bindings msb ON msb.memory_id = d.id
|
|
1069
|
+
WHERE msb.scope_id IN (${placeholders})
|
|
1070
|
+
AND d.superseded_by IS NULL
|
|
1071
|
+
ORDER BY d.created_at DESC
|
|
1072
|
+
LIMIT ?
|
|
1073
|
+
`);
|
|
1074
|
+
decisions = await stmt.all(...scopeIds, limit);
|
|
1075
|
+
}
|
|
1076
|
+
else {
|
|
1077
|
+
const stmt = adapter.prepare(`
|
|
1078
|
+
SELECT * FROM decisions
|
|
1079
|
+
WHERE superseded_by IS NULL
|
|
1080
|
+
ORDER BY created_at DESC
|
|
1081
|
+
LIMIT ?
|
|
1082
|
+
`);
|
|
1083
|
+
decisions = await stmt.all(limit);
|
|
1084
|
+
}
|
|
883
1085
|
if (format === 'markdown') {
|
|
884
1086
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
885
1087
|
return (0, decision_formatter_js_1.formatList)(decisions);
|
|
@@ -2158,9 +2360,21 @@ const mama = {
|
|
|
2158
2360
|
// Core functions (used by 4 MCP tools)
|
|
2159
2361
|
save,
|
|
2160
2362
|
suggest,
|
|
2363
|
+
saveMemory: api_js_1.saveMemory,
|
|
2364
|
+
recallMemory: api_js_1.recallMemory,
|
|
2161
2365
|
list: listDecisions,
|
|
2162
2366
|
listCheckpoints,
|
|
2163
2367
|
updateOutcome,
|
|
2368
|
+
buildProfile: api_js_1.buildProfile,
|
|
2369
|
+
ingestMemory: api_js_1.ingestMemory,
|
|
2370
|
+
evolveMemory: api_js_1.evolveMemory,
|
|
2371
|
+
buildMemoryBootstrap: api_js_1.buildMemoryBootstrap,
|
|
2372
|
+
createAuditAck: api_js_1.createAuditAck,
|
|
2373
|
+
recordMemoryAudit: api_js_1.recordMemoryAudit,
|
|
2374
|
+
upsertChannelSummary: api_js_1.upsertChannelSummary,
|
|
2375
|
+
getChannelSummary: api_js_1.getChannelSummary,
|
|
2376
|
+
listAuditFindings: finding_store_js_1.listOpenAuditFindings,
|
|
2377
|
+
listRecentMemoryEvents: event_store_js_1.listRecentMemoryEvents,
|
|
2164
2378
|
saveCheckpoint,
|
|
2165
2379
|
loadCheckpoint,
|
|
2166
2380
|
// Legacy functions (retained for internal use, not exposed via MCP)
|
|
@@ -2184,6 +2398,7 @@ const mama = {
|
|
|
2184
2398
|
verifyBackupExists,
|
|
2185
2399
|
deleteAutoLinks,
|
|
2186
2400
|
validateCleanupResult,
|
|
2401
|
+
expandWithGraph,
|
|
2187
2402
|
};
|
|
2188
2403
|
// Default export for backward compatibility
|
|
2189
2404
|
exports.default = mama;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { resolveMemoryEvolution } from './evolution-engine.js';
|
|
2
|
+
import { getChannelSummary, upsertChannelSummary } from './channel-summary-store.js';
|
|
3
|
+
import type { MemoryKind, MemoryAgentBootstrap, MemoryAuditAck, MemoryEdge, MemoryScopeRef, MemoryStatus, ProfileSnapshot, RecallBundle, IngestConversationInput, ExtractedMemoryUnit, IngestConversationResult } from './types.js';
|
|
4
|
+
interface SaveMemoryInput {
|
|
5
|
+
topic: string;
|
|
6
|
+
kind: MemoryKind;
|
|
7
|
+
summary: string;
|
|
8
|
+
details: string;
|
|
9
|
+
confidence?: number;
|
|
10
|
+
status?: MemoryStatus;
|
|
11
|
+
scopes: MemoryScopeRef[];
|
|
12
|
+
source: {
|
|
13
|
+
package: 'mama-core' | 'mcp-server' | 'standalone' | 'claude-code-plugin';
|
|
14
|
+
source_type: string;
|
|
15
|
+
user_id?: string;
|
|
16
|
+
channel_id?: string;
|
|
17
|
+
project_id?: string;
|
|
18
|
+
};
|
|
19
|
+
/** IDs to exclude from supersede candidates (e.g., sibling facts from same ingestion batch) */
|
|
20
|
+
excludeIds?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* ISO 8601 date string for when the event actually occurred (e.g. "2023-01-15").
|
|
23
|
+
* Stored in the event_date column. Defaults to created_at if omitted.
|
|
24
|
+
*/
|
|
25
|
+
eventDate?: string;
|
|
26
|
+
}
|
|
27
|
+
interface RecallMemoryOptions {
|
|
28
|
+
scopes?: MemoryScopeRef[];
|
|
29
|
+
includeProfile?: boolean;
|
|
30
|
+
includeHistory?: boolean;
|
|
31
|
+
skipGraphExpansion?: boolean;
|
|
32
|
+
topicPrefix?: string;
|
|
33
|
+
}
|
|
34
|
+
interface IngestMemoryInput {
|
|
35
|
+
content: string;
|
|
36
|
+
scopes?: MemoryScopeRef[];
|
|
37
|
+
source: SaveMemoryInput['source'];
|
|
38
|
+
eventDate?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare const LEXICAL_STOPWORDS: Set<string>;
|
|
41
|
+
export declare function getLexicalQueryTokens(query: string): string[];
|
|
42
|
+
export declare function loadEdgesForIds(ids: string[]): Promise<MemoryEdge[]>;
|
|
43
|
+
export declare function saveMemory(input: SaveMemoryInput): Promise<{
|
|
44
|
+
success: boolean;
|
|
45
|
+
id: string;
|
|
46
|
+
}>;
|
|
47
|
+
export declare function buildProfile(scopes: MemoryScopeRef[]): Promise<ProfileSnapshot>;
|
|
48
|
+
export declare function recallMemory(query: string, options?: RecallMemoryOptions): Promise<RecallBundle>;
|
|
49
|
+
export declare function ingestMemory(input: IngestMemoryInput): Promise<{
|
|
50
|
+
success: boolean;
|
|
51
|
+
id: string;
|
|
52
|
+
}>;
|
|
53
|
+
export declare function evolveMemory(input: Parameters<typeof resolveMemoryEvolution>[0]): Promise<import("./evolution-engine.js").EvolutionResult>;
|
|
54
|
+
export declare function buildMemoryBootstrap(params: {
|
|
55
|
+
scopes: MemoryScopeRef[];
|
|
56
|
+
channelKey?: string;
|
|
57
|
+
currentGoal?: string;
|
|
58
|
+
mainAgentState?: MemoryAgentBootstrap['main_agent_state'];
|
|
59
|
+
}): Promise<MemoryAgentBootstrap>;
|
|
60
|
+
export declare function createAuditAck(input: MemoryAuditAck): MemoryAuditAck;
|
|
61
|
+
export declare function recordMemoryAudit(input: {
|
|
62
|
+
channelKey: string;
|
|
63
|
+
turnId: string;
|
|
64
|
+
topic: string;
|
|
65
|
+
scopeRefs: MemoryScopeRef[];
|
|
66
|
+
ack: MemoryAuditAck;
|
|
67
|
+
savedMemories?: Array<{
|
|
68
|
+
id: string;
|
|
69
|
+
topic: string;
|
|
70
|
+
summary: string;
|
|
71
|
+
}>;
|
|
72
|
+
}): Promise<{
|
|
73
|
+
eventIds: string[];
|
|
74
|
+
findingIds: string[];
|
|
75
|
+
state: import("./types.js").ChannelSummaryStateRecord;
|
|
76
|
+
}>;
|
|
77
|
+
declare function callExtractionLLM(prompt: string, options: NonNullable<IngestConversationInput['extract']>): Promise<ExtractedMemoryUnit[]>;
|
|
78
|
+
export declare function setExtractionFn(fn: typeof callExtractionLLM | null): void;
|
|
79
|
+
export declare function ingestConversation(input: IngestConversationInput): Promise<IngestConversationResult>;
|
|
80
|
+
export { upsertChannelSummary, getChannelSummary };
|
|
81
|
+
//# sourceMappingURL=api.d.ts.map
|