@jungjaehoon/mama-server 1.0.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/package.json +53 -0
- package/src/mama/config-loader.js +218 -0
- package/src/mama/db-adapter/README.md +105 -0
- package/src/mama/db-adapter/base-adapter.js +91 -0
- package/src/mama/db-adapter/index.js +31 -0
- package/src/mama/db-adapter/sqlite-adapter.js +342 -0
- package/src/mama/db-adapter/statement.js +127 -0
- package/src/mama/db-manager.js +584 -0
- package/src/mama/debug-logger.js +78 -0
- package/src/mama/decision-formatter.js +1180 -0
- package/src/mama/decision-tracker.js +565 -0
- package/src/mama/embedding-cache.js +221 -0
- package/src/mama/embeddings.js +265 -0
- package/src/mama/hook-metrics.js +403 -0
- package/src/mama/mama-api.js +913 -0
- package/src/mama/memory-inject.js +243 -0
- package/src/mama/memory-store.js +89 -0
- package/src/mama/ollama-client.js +387 -0
- package/src/mama/outcome-tracker.js +349 -0
- package/src/mama/query-intent.js +236 -0
- package/src/mama/relevance-scorer.js +283 -0
- package/src/mama/time-formatter.js +82 -0
- package/src/mama/transparency-banner.js +301 -0
- package/src/server.js +290 -0
- package/src/tools/checkpoint-tools.js +76 -0
- package/src/tools/index.js +54 -0
- package/src/tools/list-decisions.js +76 -0
- package/src/tools/recall-decision.js +75 -0
- package/src/tools/save-decision.js +113 -0
- package/src/tools/suggest-decision.js +84 -0
- package/src/tools/update-outcome.js +128 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook Metrics Module
|
|
3
|
+
*
|
|
4
|
+
* Story M2.5: Hook Performance Monitoring & Logging
|
|
5
|
+
*
|
|
6
|
+
* Provides structured metrics logging for MAMA hooks:
|
|
7
|
+
* - Per-hook latency tracking
|
|
8
|
+
* - Decision counts and tier state
|
|
9
|
+
* - Auto-save outcomes (accepted/rejected)
|
|
10
|
+
* - Privacy-aware logging (metadata only, sensitive data redacted)
|
|
11
|
+
* - JSONL format for analysis
|
|
12
|
+
*
|
|
13
|
+
* @module hook-metrics
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const { info, warn, error: logError } = require('./debug-logger');
|
|
21
|
+
|
|
22
|
+
// Metrics log directory
|
|
23
|
+
const LOG_DIR = path.join(os.homedir(), '.mama', 'logs');
|
|
24
|
+
const METRICS_FILE = path.join(LOG_DIR, 'hook-metrics.jsonl');
|
|
25
|
+
|
|
26
|
+
// Performance targets (from MAMA-PRD.md)
|
|
27
|
+
const PERFORMANCE_TARGETS = {
|
|
28
|
+
maxLatencyMs: 500, // p95 target
|
|
29
|
+
warningLatencyMs: 400 // Warning threshold
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure log directory exists
|
|
34
|
+
*/
|
|
35
|
+
function ensureLogDir() {
|
|
36
|
+
try {
|
|
37
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
38
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
warn(`[Metrics] Failed to create log directory: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Hash sensitive data for privacy
|
|
47
|
+
* AC: Privacy - logs redact sensitive reasoning/decision bodies
|
|
48
|
+
*
|
|
49
|
+
* @param {string} text - Text to hash
|
|
50
|
+
* @returns {string} SHA-256 hash
|
|
51
|
+
*/
|
|
52
|
+
function hashSensitiveData(text) {
|
|
53
|
+
if (!text) return null;
|
|
54
|
+
return crypto.createHash('sha256').update(text).digest('hex').substring(0, 16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Redact sensitive fields from object
|
|
59
|
+
* AC: Privacy - storing only metadata and hashed topics
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} data - Data object
|
|
62
|
+
* @returns {Object} Redacted data
|
|
63
|
+
*/
|
|
64
|
+
function redactSensitiveData(data) {
|
|
65
|
+
const redacted = { ...data };
|
|
66
|
+
|
|
67
|
+
// Redact sensitive fields
|
|
68
|
+
if (redacted.decision) {
|
|
69
|
+
redacted.decision_hash = hashSensitiveData(redacted.decision);
|
|
70
|
+
delete redacted.decision;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (redacted.reasoning) {
|
|
74
|
+
redacted.reasoning_hash = hashSensitiveData(redacted.reasoning);
|
|
75
|
+
delete redacted.reasoning;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (redacted.topic) {
|
|
79
|
+
redacted.topic_hash = hashSensitiveData(redacted.topic);
|
|
80
|
+
delete redacted.topic;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (redacted.query) {
|
|
84
|
+
redacted.query_hash = hashSensitiveData(redacted.query);
|
|
85
|
+
delete redacted.query;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return redacted;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Log hook metrics
|
|
93
|
+
* AC: Logging middleware captures per-hook timings, decision counts, tier state, outcomes
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} metrics - Metrics object
|
|
96
|
+
* @param {string} metrics.hookName - Hook name (UserPromptSubmit, PreToolUse, PostToolUse)
|
|
97
|
+
* @param {number} metrics.latencyMs - Hook execution latency
|
|
98
|
+
* @param {number} metrics.decisionCount - Number of decisions returned
|
|
99
|
+
* @param {number} metrics.tier - Current tier (1/2/3)
|
|
100
|
+
* @param {string} metrics.tierReason - Reason for current tier
|
|
101
|
+
* @param {string} metrics.outcome - Outcome (success/timeout/error/rate_limited)
|
|
102
|
+
* @param {Object} metrics.metadata - Additional metadata (optional)
|
|
103
|
+
* @returns {void}
|
|
104
|
+
*/
|
|
105
|
+
function logHookMetrics(metrics) {
|
|
106
|
+
try {
|
|
107
|
+
ensureLogDir();
|
|
108
|
+
|
|
109
|
+
const timestamp = new Date().toISOString();
|
|
110
|
+
|
|
111
|
+
// Build metrics entry
|
|
112
|
+
const entry = {
|
|
113
|
+
timestamp,
|
|
114
|
+
hook_name: metrics.hookName,
|
|
115
|
+
latency_ms: metrics.latencyMs,
|
|
116
|
+
decision_count: metrics.decisionCount || 0,
|
|
117
|
+
tier: metrics.tier,
|
|
118
|
+
tier_reason: metrics.tierReason,
|
|
119
|
+
outcome: metrics.outcome,
|
|
120
|
+
performance_target_met: metrics.latencyMs <= PERFORMANCE_TARGETS.maxLatencyMs,
|
|
121
|
+
performance_warning: metrics.latencyMs >= PERFORMANCE_TARGETS.warningLatencyMs
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Add optional metadata (redacted)
|
|
125
|
+
if (metrics.metadata) {
|
|
126
|
+
entry.metadata = redactSensitiveData(metrics.metadata);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// AC: Alerts/logs clearly show degraded Tier side effects
|
|
130
|
+
if (metrics.tier > 1) {
|
|
131
|
+
entry.degraded_mode = true;
|
|
132
|
+
entry.degraded_features = getDegradedFeatures(metrics.tier);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Write JSONL entry
|
|
136
|
+
const logLine = JSON.stringify(entry) + '\n';
|
|
137
|
+
fs.appendFileSync(METRICS_FILE, logLine, 'utf8');
|
|
138
|
+
|
|
139
|
+
// Log performance warnings
|
|
140
|
+
if (metrics.latencyMs >= PERFORMANCE_TARGETS.warningLatencyMs) {
|
|
141
|
+
warn(`[Metrics] ${metrics.hookName} latency warning: ${metrics.latencyMs}ms (target: ${PERFORMANCE_TARGETS.maxLatencyMs}ms)`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Log degraded tier
|
|
145
|
+
if (metrics.tier > 1) {
|
|
146
|
+
info(`[Metrics] ${metrics.hookName} running in degraded Tier ${metrics.tier}: ${metrics.tierReason}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
} catch (error) {
|
|
150
|
+
logError(`[Metrics] Failed to log hook metrics: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get degraded features for tier
|
|
156
|
+
* AC: Alerts/logs clearly show degraded Tier side effects
|
|
157
|
+
*
|
|
158
|
+
* @param {number} tier - Current tier
|
|
159
|
+
* @returns {Array<string>} Degraded features
|
|
160
|
+
*/
|
|
161
|
+
function getDegradedFeatures(tier) {
|
|
162
|
+
const features = [];
|
|
163
|
+
|
|
164
|
+
if (tier >= 2) {
|
|
165
|
+
features.push('vector_search_disabled');
|
|
166
|
+
features.push('semantic_similarity_unavailable');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (tier >= 3) {
|
|
170
|
+
features.push('graph_traversal_disabled');
|
|
171
|
+
features.push('keyword_search_disabled');
|
|
172
|
+
features.push('mama_fully_disabled');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return features;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Log auto-save outcome
|
|
180
|
+
* AC: outcomes (accepted/rejected auto-save)
|
|
181
|
+
*
|
|
182
|
+
* @param {string} action - Action taken (accept/modify/dismiss)
|
|
183
|
+
* @param {Object} metadata - Metadata (optional, will be redacted)
|
|
184
|
+
*/
|
|
185
|
+
function logAutoSaveOutcome(action, metadata = {}) {
|
|
186
|
+
try {
|
|
187
|
+
ensureLogDir();
|
|
188
|
+
|
|
189
|
+
const timestamp = new Date().toISOString();
|
|
190
|
+
|
|
191
|
+
const entry = {
|
|
192
|
+
timestamp,
|
|
193
|
+
event_type: 'auto_save_outcome',
|
|
194
|
+
action,
|
|
195
|
+
metadata: redactSensitiveData(metadata)
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const logLine = JSON.stringify(entry) + '\n';
|
|
199
|
+
fs.appendFileSync(METRICS_FILE, logLine, 'utf8');
|
|
200
|
+
|
|
201
|
+
info(`[Metrics] Auto-save outcome: ${action}`);
|
|
202
|
+
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logError(`[Metrics] Failed to log auto-save outcome: ${error.message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get metrics summary
|
|
210
|
+
* AC: Metrics can be surfaced via /mama-status or ad-hoc CLI command
|
|
211
|
+
*
|
|
212
|
+
* @param {Object} options - Filter options
|
|
213
|
+
* @param {number} options.limit - Maximum entries to return
|
|
214
|
+
* @param {string} options.hookName - Filter by hook name
|
|
215
|
+
* @param {number} options.tier - Filter by tier
|
|
216
|
+
* @param {string} options.outcome - Filter by outcome
|
|
217
|
+
* @returns {Object} Metrics summary
|
|
218
|
+
*/
|
|
219
|
+
function getMetricsSummary(options = {}) {
|
|
220
|
+
try {
|
|
221
|
+
if (!fs.existsSync(METRICS_FILE)) {
|
|
222
|
+
return {
|
|
223
|
+
total_entries: 0,
|
|
224
|
+
entries: [],
|
|
225
|
+
statistics: {}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const content = fs.readFileSync(METRICS_FILE, 'utf8');
|
|
230
|
+
let entries = content
|
|
231
|
+
.trim()
|
|
232
|
+
.split('\n')
|
|
233
|
+
.filter(Boolean)
|
|
234
|
+
.map(line => JSON.parse(line));
|
|
235
|
+
|
|
236
|
+
// Apply filters
|
|
237
|
+
if (options.hookName) {
|
|
238
|
+
entries = entries.filter(e => e.hook_name === options.hookName);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (options.tier !== undefined) {
|
|
242
|
+
entries = entries.filter(e => e.tier === options.tier);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (options.outcome) {
|
|
246
|
+
entries = entries.filter(e => e.outcome === options.outcome);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Calculate statistics
|
|
250
|
+
const hookMetrics = entries.filter(e => e.hook_name);
|
|
251
|
+
const statistics = {
|
|
252
|
+
total_hook_calls: hookMetrics.length,
|
|
253
|
+
avg_latency_ms: hookMetrics.length > 0
|
|
254
|
+
? Math.round(hookMetrics.reduce((sum, e) => sum + e.latency_ms, 0) / hookMetrics.length)
|
|
255
|
+
: 0,
|
|
256
|
+
p95_latency_ms: calculatePercentile(hookMetrics.map(e => e.latency_ms), 0.95),
|
|
257
|
+
p99_latency_ms: calculatePercentile(hookMetrics.map(e => e.latency_ms), 0.99),
|
|
258
|
+
performance_target_met_rate: hookMetrics.length > 0
|
|
259
|
+
? Math.round((hookMetrics.filter(e => e.performance_target_met).length / hookMetrics.length) * 100)
|
|
260
|
+
: 0,
|
|
261
|
+
tier_distribution: getTierDistribution(hookMetrics),
|
|
262
|
+
outcome_distribution: getOutcomeDistribution(hookMetrics),
|
|
263
|
+
degraded_mode_rate: hookMetrics.length > 0
|
|
264
|
+
? Math.round((hookMetrics.filter(e => e.degraded_mode).length / hookMetrics.length) * 100)
|
|
265
|
+
: 0
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// Limit entries
|
|
269
|
+
if (options.limit) {
|
|
270
|
+
entries = entries.slice(-options.limit);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
total_entries: entries.length,
|
|
275
|
+
entries: entries.reverse(), // Most recent first
|
|
276
|
+
statistics
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
} catch (error) {
|
|
280
|
+
logError(`[Metrics] Failed to get metrics summary: ${error.message}`);
|
|
281
|
+
return {
|
|
282
|
+
total_entries: 0,
|
|
283
|
+
entries: [],
|
|
284
|
+
statistics: {},
|
|
285
|
+
error: error.message
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Calculate percentile
|
|
292
|
+
*
|
|
293
|
+
* @param {Array<number>} values - Values
|
|
294
|
+
* @param {number} percentile - Percentile (0-1)
|
|
295
|
+
* @returns {number} Percentile value
|
|
296
|
+
*/
|
|
297
|
+
function calculatePercentile(values, percentile) {
|
|
298
|
+
if (values.length === 0) return 0;
|
|
299
|
+
|
|
300
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
301
|
+
const index = Math.ceil(sorted.length * percentile) - 1;
|
|
302
|
+
return sorted[Math.max(0, index)];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get tier distribution
|
|
307
|
+
*
|
|
308
|
+
* @param {Array<Object>} entries - Metrics entries
|
|
309
|
+
* @returns {Object} Tier distribution
|
|
310
|
+
*/
|
|
311
|
+
function getTierDistribution(entries) {
|
|
312
|
+
const distribution = { tier1: 0, tier2: 0, tier3: 0 };
|
|
313
|
+
|
|
314
|
+
entries.forEach(entry => {
|
|
315
|
+
if (entry.tier === 1) distribution.tier1++;
|
|
316
|
+
else if (entry.tier === 2) distribution.tier2++;
|
|
317
|
+
else if (entry.tier === 3) distribution.tier3++;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return distribution;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get outcome distribution
|
|
325
|
+
*
|
|
326
|
+
* @param {Array<Object>} entries - Metrics entries
|
|
327
|
+
* @returns {Object} Outcome distribution
|
|
328
|
+
*/
|
|
329
|
+
function getOutcomeDistribution(entries) {
|
|
330
|
+
const distribution = {};
|
|
331
|
+
|
|
332
|
+
entries.forEach(entry => {
|
|
333
|
+
const outcome = entry.outcome || 'unknown';
|
|
334
|
+
distribution[outcome] = (distribution[outcome] || 0) + 1;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return distribution;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Clear metrics log
|
|
342
|
+
* For testing purposes
|
|
343
|
+
*/
|
|
344
|
+
function clearMetrics() {
|
|
345
|
+
try {
|
|
346
|
+
if (fs.existsSync(METRICS_FILE)) {
|
|
347
|
+
fs.unlinkSync(METRICS_FILE);
|
|
348
|
+
}
|
|
349
|
+
} catch (error) {
|
|
350
|
+
warn(`[Metrics] Failed to clear metrics: ${error.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Format metrics for display
|
|
356
|
+
* AC: Metrics can be surfaced via /mama-status
|
|
357
|
+
*
|
|
358
|
+
* @param {Object} summary - Metrics summary
|
|
359
|
+
* @returns {string} Formatted metrics
|
|
360
|
+
*/
|
|
361
|
+
function formatMetricsDisplay(summary) {
|
|
362
|
+
const stats = summary.statistics;
|
|
363
|
+
|
|
364
|
+
let output = '\nš MAMA Hook Metrics\n';
|
|
365
|
+
output += 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n\n';
|
|
366
|
+
|
|
367
|
+
// Statistics
|
|
368
|
+
output += `Total Hook Calls: ${stats.total_hook_calls}\n`;
|
|
369
|
+
output += `Average Latency: ${stats.avg_latency_ms}ms\n`;
|
|
370
|
+
output += `P95 Latency: ${stats.p95_latency_ms}ms\n`;
|
|
371
|
+
output += `P99 Latency: ${stats.p99_latency_ms}ms\n`;
|
|
372
|
+
output += `Performance Target Met: ${stats.performance_target_met_rate}%\n`;
|
|
373
|
+
output += `Degraded Mode Rate: ${stats.degraded_mode_rate}%\n\n`;
|
|
374
|
+
|
|
375
|
+
// Tier distribution
|
|
376
|
+
output += 'Tier Distribution:\n';
|
|
377
|
+
output += ` š¢ Tier 1: ${stats.tier_distribution.tier1}\n`;
|
|
378
|
+
output += ` š” Tier 2: ${stats.tier_distribution.tier2}\n`;
|
|
379
|
+
output += ` š“ Tier 3: ${stats.tier_distribution.tier3}\n\n`;
|
|
380
|
+
|
|
381
|
+
// Outcome distribution
|
|
382
|
+
output += 'Outcome Distribution:\n';
|
|
383
|
+
Object.entries(stats.outcome_distribution).forEach(([outcome, count]) => {
|
|
384
|
+
output += ` ${outcome}: ${count}\n`;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
output += '\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n';
|
|
388
|
+
|
|
389
|
+
return output;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
logHookMetrics,
|
|
394
|
+
logAutoSaveOutcome,
|
|
395
|
+
getMetricsSummary,
|
|
396
|
+
formatMetricsDisplay,
|
|
397
|
+
clearMetrics,
|
|
398
|
+
hashSensitiveData,
|
|
399
|
+
redactSensitiveData,
|
|
400
|
+
getDegradedFeatures,
|
|
401
|
+
PERFORMANCE_TARGETS,
|
|
402
|
+
METRICS_FILE
|
|
403
|
+
};
|