gitmem-mcp 1.4.2 → 1.4.4

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,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.4] - 2026-03-31
11
+
12
+ ### Fixed
13
+ - **Project drift on session resume eliminated**: When resuming an existing session (same hostname+PID), the stored project now overrides whatever the agent passes. Previously, context compaction could cause agents to send the wrong project (e.g., `orchestra_dev` instead of `weekend_warrior`), creating a session under the wrong project with wrong threads and decisions. The active-sessions registry already stored the correct project — it just wasn't used on resume.
14
+ - **`closing_reflection` array coercion**: Values passed as arrays in `closing_reflection` are now coerced to strings, preventing schema validation errors on session close.
15
+ - **`create_thread` no longer triggers false enforcement warnings**: Removed from `CONSEQUENTIAL_TOOLS` list — creating threads is lightweight and shouldn't require prior recall.
16
+
17
+ ## [1.4.3] - 2026-02-24
18
+
19
+ ### Fixed
20
+ - **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`.
21
+
22
+ ### Performance
23
+ - **session_start ~200-300ms faster**: Sessions and threads queries now run in parallel (`Promise.all`) instead of sequentially inside `loadLastSession`.
24
+ - **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.
25
+
10
26
  ## [1.4.2] - 2026-02-22
11
27
 
12
28
  ### Fixed
package/bin/gitmem.js CHANGED
@@ -154,9 +154,10 @@ async function cmdInit() {
154
154
  // Merge: skip scars that already exist by id
155
155
  const existingIds = new Set(existing.map((s) => s.id));
156
156
  let added = 0;
157
+ const now = new Date().toISOString();
157
158
  for (const scar of starterScars) {
158
159
  if (!existingIds.has(scar.id)) {
159
- existing.push(scar);
160
+ existing.push({ ...scar, created_at: now, source_date: now.slice(0, 10) });
160
161
  added++;
161
162
  console.log(` + ${scar.title}`);
162
163
  } else {
@@ -13,11 +13,11 @@ export declare const ClosingReflectionSchema: z.ZodObject<{
13
13
  wrong_assumption: z.ZodString;
14
14
  scars_applied: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
15
15
  /** Q7: What from this session should be captured as institutional memory? */
16
- institutional_memory_items: z.ZodOptional<z.ZodString>;
16
+ institutional_memory_items: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
17
17
  /** Q8: How did the human prefer to work this session? */
18
- collaborative_dynamic: z.ZodOptional<z.ZodString>;
18
+ collaborative_dynamic: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
19
19
  /** Q9: What collaborative dynamic worked or didn't work? */
20
- rapport_notes: z.ZodOptional<z.ZodString>;
20
+ rapport_notes: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
21
21
  }, "strip", z.ZodTypeAny, {
22
22
  what_broke: string;
23
23
  what_took_longer: string;
@@ -35,9 +35,9 @@ export declare const ClosingReflectionSchema: z.ZodObject<{
35
35
  what_worked: string;
36
36
  wrong_assumption: string;
37
37
  scars_applied: string | string[];
38
- institutional_memory_items?: string | undefined;
39
- collaborative_dynamic?: string | undefined;
40
- rapport_notes?: string | undefined;
38
+ institutional_memory_items?: string | string[] | undefined;
39
+ collaborative_dynamic?: string | string[] | undefined;
40
+ rapport_notes?: string | string[] | undefined;
41
41
  }>;
42
42
  export type ClosingReflection = z.infer<typeof ClosingReflectionSchema>;
43
43
  /**
@@ -154,11 +154,11 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
154
154
  wrong_assumption: z.ZodString;
155
155
  scars_applied: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
156
156
  /** Q7: What from this session should be captured as institutional memory? */
157
- institutional_memory_items: z.ZodOptional<z.ZodString>;
157
+ institutional_memory_items: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
158
158
  /** Q8: How did the human prefer to work this session? */
159
- collaborative_dynamic: z.ZodOptional<z.ZodString>;
159
+ collaborative_dynamic: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
160
160
  /** Q9: What collaborative dynamic worked or didn't work? */
161
- rapport_notes: z.ZodOptional<z.ZodString>;
161
+ rapport_notes: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodEffects<z.ZodArray<z.ZodString, "many">, string, string[]>]>>;
162
162
  }, "strip", z.ZodTypeAny, {
163
163
  what_broke: string;
164
164
  what_took_longer: string;
@@ -176,9 +176,9 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
176
176
  what_worked: string;
177
177
  wrong_assumption: string;
178
178
  scars_applied: string | string[];
179
- institutional_memory_items?: string | undefined;
180
- collaborative_dynamic?: string | undefined;
181
- rapport_notes?: string | undefined;
179
+ institutional_memory_items?: string | string[] | undefined;
180
+ collaborative_dynamic?: string | string[] | undefined;
181
+ rapport_notes?: string | string[] | undefined;
182
182
  }>>;
183
183
  human_corrections: z.ZodOptional<z.ZodString>;
184
184
  decisions: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -343,9 +343,9 @@ export declare const SessionCloseParamsSchema: z.ZodObject<{
343
343
  what_worked: string;
344
344
  wrong_assumption: string;
345
345
  scars_applied: string | string[];
346
- institutional_memory_items?: string | undefined;
347
- collaborative_dynamic?: string | undefined;
348
- rapport_notes?: string | undefined;
346
+ institutional_memory_items?: string | string[] | undefined;
347
+ collaborative_dynamic?: string | string[] | undefined;
348
+ rapport_notes?: string | string[] | undefined;
349
349
  } | undefined;
350
350
  human_corrections?: string | undefined;
351
351
  scars_to_record?: {
@@ -15,11 +15,11 @@ export const ClosingReflectionSchema = z.object({
15
15
  wrong_assumption: z.string(),
16
16
  scars_applied: z.union([z.string(), z.array(z.string())]),
17
17
  /** Q7: What from this session should be captured as institutional memory? */
18
- institutional_memory_items: z.string().optional(),
18
+ institutional_memory_items: z.union([z.string(), z.array(z.string()).transform((arr) => arr.join(". "))]).optional(),
19
19
  /** Q8: How did the human prefer to work this session? */
20
- collaborative_dynamic: z.string().optional(),
20
+ collaborative_dynamic: z.union([z.string(), z.array(z.string()).transform((arr) => arr.join(". "))]).optional(),
21
21
  /** Q9: What collaborative dynamic worked or didn't work? */
22
- rapport_notes: z.string().optional(),
22
+ rapport_notes: z.union([z.string(), z.array(z.string()).transform((arr) => arr.join(". "))]).optional(),
23
23
  });
24
24
  /**
25
25
  * Task completion proof schema
@@ -37,7 +37,6 @@ const SESSION_REQUIRED_TOOLS = new Set([
37
37
  const CONSEQUENTIAL_TOOLS = new Set([
38
38
  "create_learning", "gitmem-cl", "gm-scar",
39
39
  "create_decision", "gitmem-cd",
40
- "create_thread", "gitmem-ct", "gm-thread-new",
41
40
  "session_close", "gitmem-sc", "gm-close",
42
41
  ]);
43
42
  /**
@@ -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;
@@ -502,6 +503,7 @@ function restoreSessionState(existing, fallbackAgent) {
502
503
  agent: existing.agent || fallbackAgent,
503
504
  linearIssue: existing.linear_issue,
504
505
  startedAt,
506
+ project: existing.project,
505
507
  };
506
508
  }
507
509
  /**
@@ -742,10 +744,20 @@ export async function sessionStart(params) {
742
744
  // 1. Detect agent (or use provided)
743
745
  const env = detectAgent();
744
746
  const agent = params.agent_identity || env.agent;
745
- const project = params.project || getConfigProject() || "default";
747
+ let project = params.project || getConfigProject() || "default";
746
748
  // Check for existing active session — reuse session_id but still load full context
747
749
  const existingSession = checkExistingSession(agent, params.force);
748
750
  const isResuming = existingSession !== null;
751
+ // When resuming, prefer the stored project from the existing session.
752
+ // This prevents project drift after context compaction — the agent may pass
753
+ // the wrong project (e.g., from CLAUDE.md defaults) but the stored session
754
+ // knows the real project.
755
+ if (isResuming && existingSession?.project) {
756
+ if (existingSession.project !== project) {
757
+ console.error(`[session_start] Project override on resume: ${project} → ${existingSession.project} (from stored session)`);
758
+ }
759
+ project = existingSession.project;
760
+ }
749
761
  // t-f7c2fa01: When force:true kills an existing session, carry forward its startedAt
750
762
  // so session_close duration reflects the full conversation, not just the new session.
751
763
  // Also carry forward activity counts (recalls, observations) so standard close isn't rejected.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
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",