gitmem-mcp 1.4.1 → 1.4.3
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/CHANGELOG.md +17 -0
- package/dist/services/metrics.js +4 -1
- package/dist/services/session-state.js +29 -1
- package/dist/tools/session-close.js +69 -18
- package/dist/tools/session-start.js +11 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.3] - 2026-02-24
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **NULL agent values in query metrics eliminated**: `recordMetrics()` now auto-detects agent via `getAgentIdentity()` when callers don't provide it. Previously 15 of 18 tools omitted the agent field, resulting in NULL values in `gitmem_query_metrics`.
|
|
14
|
+
|
|
15
|
+
### Performance
|
|
16
|
+
- **session_start ~200-300ms faster**: Sessions and threads queries now run in parallel (`Promise.all`) instead of sequentially inside `loadLastSession`.
|
|
17
|
+
- **session_close transcript upload no longer blocks**: Transcript save moved from blocking `await` to fire-and-forget via effect tracker. Removes 500-5000ms variable cost from `latency_ms`. Claude session ID extraction remains synchronous.
|
|
18
|
+
|
|
19
|
+
## [1.4.2] - 2026-02-22
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **Scar usage `execution_successful` nulls eliminated**: N_A confirmations now record `true` (was null/undefined). Q6 text matches now include `execution_successful: true` (was omitted). Fixes 80% null rate in scar effectiveness data.
|
|
23
|
+
- **Auto-bridge fires on all session closes**: Previously required Q6 `scars_applied` to be non-empty. Now fires whenever no explicit `scars_to_record` is provided, ensuring confirmations from `confirm_scars` always get recorded.
|
|
24
|
+
- **Surfaced scars survive MCP restart**: `getSurfacedScars()` now recovers from the active-sessions registry when `currentSession` is null after MCP restart. Scars surfaced early in a session are no longer silently lost.
|
|
25
|
+
- **Session close display shows scar titles**: `reference_context` now leads with the scar title instead of boilerplate. Display uses +/! indicators for applied/refuted scars.
|
|
26
|
+
|
|
10
27
|
## [1.4.1] - 2026-02-22
|
|
11
28
|
|
|
12
29
|
### Added
|
package/dist/services/metrics.js
CHANGED
|
@@ -8,6 +8,7 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
8
8
|
import * as supabase from "./supabase-client.js";
|
|
9
9
|
import { getEffectTracker } from "./effect-tracker.js";
|
|
10
10
|
import { hasSupabase } from "./tier.js";
|
|
11
|
+
import { getAgentIdentity } from "./agent-detection.js";
|
|
11
12
|
/**
|
|
12
13
|
* Performance targets
|
|
13
14
|
*/
|
|
@@ -64,10 +65,12 @@ export class Timer {
|
|
|
64
65
|
export async function recordMetrics(metrics) {
|
|
65
66
|
if (!hasSupabase())
|
|
66
67
|
return; // No-op on free tier — don't record failures
|
|
68
|
+
// Auto-detect agent if not provided by caller
|
|
69
|
+
const agent = metrics.agent || getAgentIdentity() || null;
|
|
67
70
|
const record = {
|
|
68
71
|
id: metrics.id,
|
|
69
72
|
session_id: metrics.session_id || null,
|
|
70
|
-
agent
|
|
73
|
+
agent,
|
|
71
74
|
tool_name: metrics.tool_name,
|
|
72
75
|
query_text: metrics.query_text || null,
|
|
73
76
|
tables_searched: metrics.tables_searched || null,
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* This allows recall() to always assign variants even without explicit parameters.
|
|
13
13
|
*/
|
|
14
14
|
import fs from "fs";
|
|
15
|
+
import * as os from "os";
|
|
15
16
|
import { getSessionPath } from "./gitmem-dir.js";
|
|
17
|
+
import { listActiveSessions } from "./active-sessions.js";
|
|
16
18
|
// Global session state (single active session per MCP server instance)
|
|
17
19
|
let currentSession = null;
|
|
18
20
|
/**
|
|
@@ -105,7 +107,7 @@ export function getSurfacedScars() {
|
|
|
105
107
|
if (currentSession?.surfacedScars && currentSession.surfacedScars.length > 0) {
|
|
106
108
|
return currentSession.surfacedScars;
|
|
107
109
|
}
|
|
108
|
-
// Fallback: recover from per-session file if in-memory was lost (MCP restart)
|
|
110
|
+
// Fallback 1: recover from per-session file if in-memory was lost (MCP restart)
|
|
109
111
|
if (currentSession?.sessionId) {
|
|
110
112
|
try {
|
|
111
113
|
const sessionFilePath = getSessionPath(currentSession.sessionId, "session.json");
|
|
@@ -122,6 +124,32 @@ export function getSurfacedScars() {
|
|
|
122
124
|
console.warn("[session-state] Failed to recover surfaced scars from file:", error);
|
|
123
125
|
}
|
|
124
126
|
}
|
|
127
|
+
// Fallback 2: if currentSession is null (MCP restarted completely), find session from registry.
|
|
128
|
+
// After MCP restart the PID changes, but the active-sessions registry may still have
|
|
129
|
+
// the previous session entry (pruneStale adopts dead-PID sessions on same hostname).
|
|
130
|
+
if (!currentSession) {
|
|
131
|
+
try {
|
|
132
|
+
const hostname = os.hostname();
|
|
133
|
+
const sessions = listActiveSessions();
|
|
134
|
+
const candidates = sessions
|
|
135
|
+
.filter(s => s.hostname === hostname)
|
|
136
|
+
.sort((a, b) => new Date(b.started_at).getTime() - new Date(a.started_at).getTime());
|
|
137
|
+
if (candidates.length > 0) {
|
|
138
|
+
const recoveredId = candidates[0].session_id;
|
|
139
|
+
const sessionFilePath = getSessionPath(recoveredId, "session.json");
|
|
140
|
+
if (fs.existsSync(sessionFilePath)) {
|
|
141
|
+
const data = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
|
|
142
|
+
if (data.surfaced_scars && Array.isArray(data.surfaced_scars) && data.surfaced_scars.length > 0) {
|
|
143
|
+
console.error(`[session-state] Recovered ${data.surfaced_scars.length} surfaced scars from registry session ${recoveredId.slice(0, 8)}`);
|
|
144
|
+
return data.surfaced_scars;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.warn("[session-state] Failed to recover surfaced scars from registry:", error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
125
153
|
return [];
|
|
126
154
|
}
|
|
127
155
|
/**
|
|
@@ -53,6 +53,30 @@ function normalizeScarsApplied(scarsApplied) {
|
|
|
53
53
|
function countScarsApplied(scarsApplied) {
|
|
54
54
|
return normalizeScarsApplied(scarsApplied).length;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Find transcript file path: explicit param or auto-detect from Claude Code projects dir
|
|
58
|
+
*/
|
|
59
|
+
function findTranscriptPath(explicitPath) {
|
|
60
|
+
if (explicitPath) {
|
|
61
|
+
if (fs.existsSync(explicitPath)) {
|
|
62
|
+
console.error(`[session_close] Using explicit transcript path: ${explicitPath}`);
|
|
63
|
+
return explicitPath;
|
|
64
|
+
}
|
|
65
|
+
console.warn(`[session_close] Explicit transcript path does not exist: ${explicitPath}`);
|
|
66
|
+
}
|
|
67
|
+
const homeDir = os.homedir();
|
|
68
|
+
const projectsDir = path.join(homeDir, ".claude", "projects");
|
|
69
|
+
const cwd = process.cwd();
|
|
70
|
+
const projectDirName = path.basename(cwd);
|
|
71
|
+
const found = findMostRecentTranscript(projectsDir, projectDirName, cwd);
|
|
72
|
+
if (found) {
|
|
73
|
+
console.error(`[session_close] Auto-detected transcript: ${found}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
console.error(`[session_close] No transcript file found in ${projectsDir}`);
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
56
80
|
/**
|
|
57
81
|
* Find the most recently modified transcript file in Claude Code projects directory
|
|
58
82
|
* Search by recency, not by filename matching (supports post-compaction)
|
|
@@ -288,15 +312,12 @@ function formatCloseDisplay(sessionId, compliance, params, learningsCount, succe
|
|
|
288
312
|
lines.push(` ${dimText("d")} ${truncate(d.title, 68)}`);
|
|
289
313
|
}
|
|
290
314
|
}
|
|
291
|
-
// Scars applied —
|
|
315
|
+
// Scars applied — show titles with status indicator
|
|
292
316
|
if (scarsApplied > 0) {
|
|
293
317
|
lines.push("");
|
|
294
318
|
for (const s of params.scars_to_record.filter(s => s.reference_type !== "none")) {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
s.reference_type === "acknowledged" ? "ack'd" :
|
|
298
|
-
s.reference_type === "refuted" ? `${ANSI.yellow}REFUTED${ANSI.reset}` : (s.reference_type || "?");
|
|
299
|
-
lines.push(` ${dimText(ref.padEnd(8))} ${truncate(s.reference_context || "", 60)}`);
|
|
319
|
+
const indicator = s.reference_type === "refuted" ? `${ANSI.yellow}!${ANSI.reset}` : STATUS.pass;
|
|
320
|
+
lines.push(` ${indicator} ${truncate(s.reference_context || s.scar_identifier || "", 70)}`);
|
|
300
321
|
}
|
|
301
322
|
}
|
|
302
323
|
// Transcript — only on failure
|
|
@@ -599,12 +620,13 @@ function bridgeScarsToUsageRecords(normalizedScarsApplied, sessionId, agentIdent
|
|
|
599
620
|
if (reflection) {
|
|
600
621
|
// Reflection provides definitive signal
|
|
601
622
|
executionSuccessful = reflection.outcome === "OBEYED" ? true : false;
|
|
602
|
-
context =
|
|
623
|
+
context = `${scar.scar_title.slice(0, 60)} (${reflection.outcome})`;
|
|
603
624
|
}
|
|
604
625
|
else {
|
|
605
|
-
// Fall back to confirmation-based default
|
|
606
|
-
|
|
607
|
-
|
|
626
|
+
// Fall back to confirmation-based default
|
|
627
|
+
// APPLYING/N_A = task proceeded normally (true), REFUTED = outcome unknown (null)
|
|
628
|
+
executionSuccessful = confirmation.decision === "REFUTED" ? undefined : true;
|
|
629
|
+
context = `${scar.scar_title.slice(0, 60)} (${confirmation.decision})`;
|
|
608
630
|
}
|
|
609
631
|
autoBridgedScars.push({
|
|
610
632
|
scar_identifier: scar.scar_id,
|
|
@@ -636,7 +658,8 @@ function bridgeScarsToUsageRecords(normalizedScarsApplied, sessionId, agentIdent
|
|
|
636
658
|
agent: agentIdentity,
|
|
637
659
|
surfaced_at: match.surfaced_at,
|
|
638
660
|
reference_type: "acknowledged",
|
|
639
|
-
reference_context:
|
|
661
|
+
reference_context: `${match.scar_title.slice(0, 60)} (Q6 match)`,
|
|
662
|
+
execution_successful: true,
|
|
640
663
|
variant_id: match.variant_id,
|
|
641
664
|
});
|
|
642
665
|
}
|
|
@@ -651,7 +674,7 @@ function bridgeScarsToUsageRecords(normalizedScarsApplied, sessionId, agentIdent
|
|
|
651
674
|
agent: agentIdentity,
|
|
652
675
|
surfaced_at: scar.surfaced_at,
|
|
653
676
|
reference_type: "none",
|
|
654
|
-
reference_context:
|
|
677
|
+
reference_context: `${scar.scar_title.slice(0, 60)} (not addressed)`,
|
|
655
678
|
execution_successful: false,
|
|
656
679
|
variant_id: scar.variant_id,
|
|
657
680
|
});
|
|
@@ -1034,20 +1057,48 @@ export async function sessionClose(params) {
|
|
|
1034
1057
|
console.error("[session_close] Failed to prune threads.json (non-fatal):", err);
|
|
1035
1058
|
}
|
|
1036
1059
|
// Capture transcript if enabled (default true for CLI/DAC)
|
|
1060
|
+
// Split into two phases: sync ID extraction (fast) + async upload (fire-and-forget)
|
|
1037
1061
|
let transcriptStatus;
|
|
1038
1062
|
const shouldCaptureTranscript = params.capture_transcript !== false &&
|
|
1039
1063
|
(agentIdentity === "cli" || agentIdentity === "desktop");
|
|
1040
1064
|
if (shouldCaptureTranscript) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
if (
|
|
1044
|
-
|
|
1065
|
+
// Phase 1: Find transcript and extract Claude session ID (sync, ~10ms)
|
|
1066
|
+
const transcriptFilePath = findTranscriptPath(params.transcript_path);
|
|
1067
|
+
if (transcriptFilePath) {
|
|
1068
|
+
const transcriptContent = fs.readFileSync(transcriptFilePath, "utf-8");
|
|
1069
|
+
const claudeSessionId = extractClaudeSessionId(transcriptContent, transcriptFilePath) || undefined;
|
|
1070
|
+
if (claudeSessionId) {
|
|
1071
|
+
sessionData.claude_code_session_id = claudeSessionId;
|
|
1072
|
+
console.error(`[session_close] Extracted Claude session ID: ${claudeSessionId}`);
|
|
1073
|
+
}
|
|
1074
|
+
// Phase 2: Upload transcript (fire-and-forget — was blocking ~500-5000ms)
|
|
1075
|
+
const transcriptProject = isRetroactive ? "default" : existingSession?.project;
|
|
1076
|
+
getEffectTracker().track("transcript", "session_close", async () => {
|
|
1077
|
+
const saveResult = await saveTranscript({
|
|
1078
|
+
session_id: sessionId,
|
|
1079
|
+
transcript: transcriptContent,
|
|
1080
|
+
format: "json",
|
|
1081
|
+
project: transcriptProject,
|
|
1082
|
+
});
|
|
1083
|
+
if (saveResult.success && saveResult.transcript_path) {
|
|
1084
|
+
console.error(`[session_close] Transcript saved: ${saveResult.transcript_path} (${saveResult.size_kb}KB)`);
|
|
1085
|
+
// Process transcript for semantic search (chained fire-and-forget)
|
|
1086
|
+
processTranscript(sessionId, transcriptContent, transcriptProject)
|
|
1087
|
+
.then(result => {
|
|
1088
|
+
if (result.success) {
|
|
1089
|
+
console.error(`[session_close] Transcript processed: ${result.chunksCreated} chunks`);
|
|
1090
|
+
}
|
|
1091
|
+
})
|
|
1092
|
+
.catch((err) => console.error("[session_close] Transcript processing failed:", err instanceof Error ? err.message : err));
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1045
1095
|
}
|
|
1046
1096
|
}
|
|
1047
1097
|
// Auto-bridge Q6 answers to scar_usage records
|
|
1048
1098
|
const normalizedScarsApplied = normalizeScarsApplied(params.closing_reflection?.scars_applied);
|
|
1049
|
-
|
|
1050
|
-
|
|
1099
|
+
// Auto-bridge surfaced scars to usage records whenever no explicit scars_to_record.
|
|
1100
|
+
// Fires even when Q6 is empty — Pass 1 matches confirmations, Pass 3 catches ignored scars.
|
|
1101
|
+
if (!params.scars_to_record || params.scars_to_record.length === 0) {
|
|
1051
1102
|
const bridgedScars = bridgeScarsToUsageRecords(normalizedScarsApplied, sessionId, agentIdentity);
|
|
1052
1103
|
if (bridgedScars.length > 0) {
|
|
1053
1104
|
params = { ...params, scars_to_record: bridgedScars };
|
|
@@ -101,19 +101,20 @@ async function loadLastSession(agent, project) {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
try {
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
const sessions = await
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
104
|
+
// Parallel load: sessions + threads are independent queries
|
|
105
|
+
// (was sequential — ~200-300ms saved by parallelizing)
|
|
106
|
+
const [sessions, supabaseThreads] = await Promise.all([
|
|
107
|
+
supabase.listRecords({
|
|
108
|
+
table: getTableName("sessions_lite"),
|
|
109
|
+
filters: { agent, project },
|
|
110
|
+
limit: 10,
|
|
111
|
+
orderBy: { column: "created_at", ascending: false },
|
|
112
|
+
}),
|
|
113
|
+
loadActiveThreadsFromSupabase(project),
|
|
114
|
+
]);
|
|
113
115
|
let aggregated_open_threads;
|
|
114
116
|
let displayInfo = [];
|
|
115
117
|
let threadsFromSupabase = false;
|
|
116
|
-
const supabaseThreads = await loadActiveThreadsFromSupabase(project);
|
|
117
118
|
if (supabaseThreads !== null) {
|
|
118
119
|
// Supabase is source of truth for threads
|
|
119
120
|
aggregated_open_threads = supabaseThreads.open;
|