gitmem-mcp 1.4.2 → 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 CHANGED
@@ -7,6 +7,15 @@ 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
+
10
19
  ## [1.4.2] - 2026-02-22
11
20
 
12
21
  ### Fixed
@@ -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: metrics.agent || null,
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,
@@ -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)
@@ -1033,14 +1057,41 @@ export async function sessionClose(params) {
1033
1057
  console.error("[session_close] Failed to prune threads.json (non-fatal):", err);
1034
1058
  }
1035
1059
  // Capture transcript if enabled (default true for CLI/DAC)
1060
+ // Split into two phases: sync ID extraction (fast) + async upload (fire-and-forget)
1036
1061
  let transcriptStatus;
1037
1062
  const shouldCaptureTranscript = params.capture_transcript !== false &&
1038
1063
  (agentIdentity === "cli" || agentIdentity === "desktop");
1039
1064
  if (shouldCaptureTranscript) {
1040
- const transcriptResult = await captureSessionTranscript(sessionId, params, existingSession, isRetroactive);
1041
- transcriptStatus = transcriptResult.status;
1042
- if (transcriptResult.claudeSessionId) {
1043
- sessionData.claude_code_session_id = transcriptResult.claudeSessionId;
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
+ });
1044
1095
  }
1045
1096
  }
1046
1097
  // Auto-bridge Q6 answers to scar_usage records
@@ -101,19 +101,20 @@ async function loadLastSession(agent, project) {
101
101
  };
102
102
  }
103
103
  try {
104
- // Use _lite view for performance (excludes embedding)
105
- // View now includes decisions/open_threads arrays
106
- const sessions = await supabase.listRecords({
107
- table: getTableName("sessions_lite"),
108
- filters: { agent, project },
109
- limit: 10, // Get several to find a closed one + aggregate threads
110
- orderBy: { column: "created_at", ascending: false },
111
- });
112
- // Try loading threads from Supabase (source of truth) first
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "mcpName": "io.github.gitmem-dev/gitmem",
5
5
  "description": "Persistent learning memory for AI coding agents. Memory that compounds.",
6
6
  "type": "module",