gitmem-mcp 1.6.4 → 1.6.6

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,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.6.6] - 2026-06-27
11
+
12
+ ### Changed
13
+ - **`recall` stubs low-confidence scars to cut wasted tokens**: scars scoring below the `0.55` similarity threshold (already flagged `[low confidence]`, ~66% N/A rate in that band) now render as a one-line stub — title, severity, score, short id, and the `[low confidence]` tag — instead of hydrating their full body (description, counter-arguments, applies-when, why-this-matters, action-protocol, self-check, related triples). High-confidence scars (≥ 0.55) are unchanged, and blocking-verification scars always render in full regardless of score. The `0.55` cutoff is now a single named constant (`LOW_CONFIDENCE_THRESHOLD`) shared by the tag and the stub so they can't drift apart. The scar's id stays visible, so an agent can still pull detail on demand. (GIT-49)
14
+
15
+ ## [1.6.5] - 2026-06-27
16
+
17
+ ### Fixed
18
+ - **`resolve_thread` can now resolve threads created by other sessions**: `list_threads` reads the Supabase source-of-truth, but `resolve_thread` previously matched only the local/session cache — so a thread created by another session showed up in `list_threads` yet returned "Thread not found" when you tried to resolve it. These "visible-but-unresolvable" threads piled up across sessions. `resolve_thread` now falls back to looking the thread up in Supabase (by ID, or by text for `text_match`) before failing, resolves it in the source-of-truth, and syncs the local cache. (GIT-46)
19
+
10
20
  ## [1.6.4] - 2026-06-11
11
21
 
12
22
  ### Fixed
@@ -63,6 +63,20 @@ export declare function resolveThreadInSupabase(threadId: string, options?: {
63
63
  resolutionNote?: string;
64
64
  resolvedBySession?: string;
65
65
  }): Promise<boolean>;
66
+ /**
67
+ * Fetch a single thread from Supabase by its thread_id ("t-XXXX").
68
+ *
69
+ * Used by resolve_thread's cross-session fallback (GIT-46): a thread can live
70
+ * in the Supabase source-of-truth (and show up in list_threads) while being
71
+ * absent from this session's local cache because another session created it.
72
+ *
73
+ * Deliberately NOT scoped by project — thread_id is globally unique, and
74
+ * scoping would re-introduce the "can only resolve threads I own" constraint
75
+ * this fallback exists to remove.
76
+ *
77
+ * Returns the mapped ThreadObject, or null if not found / Supabase unavailable.
78
+ */
79
+ export declare function getThreadFromSupabaseById(threadId: string): Promise<ThreadObject | null>;
66
80
  /**
67
81
  * List threads from Supabase with project filter.
68
82
  * Uses threads_lite view (no embedding column).
@@ -145,6 +145,38 @@ export async function resolveThreadInSupabase(threadId, options = {}) {
145
145
  return false;
146
146
  }
147
147
  }
148
+ /**
149
+ * Fetch a single thread from Supabase by its thread_id ("t-XXXX").
150
+ *
151
+ * Used by resolve_thread's cross-session fallback (GIT-46): a thread can live
152
+ * in the Supabase source-of-truth (and show up in list_threads) while being
153
+ * absent from this session's local cache because another session created it.
154
+ *
155
+ * Deliberately NOT scoped by project — thread_id is globally unique, and
156
+ * scoping would re-introduce the "can only resolve threads I own" constraint
157
+ * this fallback exists to remove.
158
+ *
159
+ * Returns the mapped ThreadObject, or null if not found / Supabase unavailable.
160
+ */
161
+ export async function getThreadFromSupabaseById(threadId) {
162
+ if (!hasSupabase() || !supabase.isConfigured()) {
163
+ return null;
164
+ }
165
+ try {
166
+ const rows = await supabase.directQuery(getTableName("threads_lite"), {
167
+ select: "*",
168
+ filters: { thread_id: threadId },
169
+ limit: 1,
170
+ });
171
+ if (rows.length === 0)
172
+ return null;
173
+ return rowToThreadObject(rows[0]);
174
+ }
175
+ catch (error) {
176
+ console.error("[thread-supabase] Failed to get thread by id:", error instanceof Error ? error.message : error);
177
+ return null;
178
+ }
179
+ }
148
180
  /**
149
181
  * List threads from Supabase with project filter.
150
182
  * Uses threads_lite view (no embedding column).
@@ -15,6 +15,16 @@
15
15
  import type { KnowledgeTriple } from "../services/supabase-client.js";
16
16
  import { type ScarWithVariant } from "../services/variant-assignment.js";
17
17
  import type { Project, PerformanceData } from "../types/index.js";
18
+ /**
19
+ * Confidence cutoff for scar rendering.
20
+ *
21
+ * Matches below this similarity are flagged `[low confidence]` and, per the
22
+ * 1.5.0 UX-audit calibration, are N/A ~66% of the time. They pass the pro-tier
23
+ * 0.45 inclusion filter but are rendered as stubs (header only) rather than full
24
+ * bodies — the tag boundary and the stub boundary share this constant so they
25
+ * can never drift apart.
26
+ */
27
+ export declare const LOW_CONFIDENCE_THRESHOLD = 0.55;
18
28
  /**
19
29
  * Parameters for recall tool
20
30
  */
@@ -27,6 +27,16 @@ import { getSessionPath } from "../services/gitmem-dir.js";
27
27
  import { wrapDisplay, SEV, dimText, ANSI } from "../services/display-protocol.js";
28
28
  import { formatNudgeHeader } from "../services/nudge-variants.js";
29
29
  import { fetchDismissalCounts } from "../services/behavioral-decay.js";
30
+ /**
31
+ * Confidence cutoff for scar rendering.
32
+ *
33
+ * Matches below this similarity are flagged `[low confidence]` and, per the
34
+ * 1.5.0 UX-audit calibration, are N/A ~66% of the time. They pass the pro-tier
35
+ * 0.45 inclusion filter but are rendered as stubs (header only) rather than full
36
+ * bodies — the tag boundary and the stub boundary share this constant so they
37
+ * can never drift apart.
38
+ */
39
+ export const LOW_CONFIDENCE_THRESHOLD = 0.55;
30
40
  /**
31
41
  * Format scars into a readable response for Claude
32
42
  */
@@ -80,8 +90,8 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
80
90
  for (const scar of scars) {
81
91
  const sev = SEV[scar.severity] || "[?]";
82
92
  const starterTag = scar.is_starter ? ` ${dimText("[starter]")}` : "";
83
- // Confidence tier: marginal matches (< 0.55) get flagged — 66% N/A rate in this range
84
- const confidenceTag = scar.similarity < 0.55 ? ` ${dimText("[low confidence]")}` : "";
93
+ // Confidence tier: marginal matches get flagged — 66% N/A rate below the threshold
94
+ const confidenceTag = scar.similarity < LOW_CONFIDENCE_THRESHOLD ? ` ${dimText("[low confidence]")}` : "";
85
95
  // Pro: decay tag for scars with reduced behavioral relevance
86
96
  const decayTag = hasProInsights() && scar.decay_multiplier !== undefined && scar.decay_multiplier < 0.8
87
97
  ? ` ${dimText(`[decay: ${Math.round(scar.decay_multiplier * 100)}%]`)}`
@@ -94,55 +104,61 @@ No past lessons match this plan closely enough. Scars accumulate as you work —
94
104
  lines.push(` _[dismissed ${counts.dismissed}/${counts.surfaced} times — re-evaluate whether this still applies]_`);
95
105
  }
96
106
  }
97
- // Use variant enforcement text if available (blind to variant name)
98
- if (scar.variant_info?.has_variants && scar.variant_info.variant) {
99
- const variantText = formatVariantEnforcement(scar.variant_info.variant, scar.title);
100
- lines.push(variantText);
101
- }
102
- else {
103
- // Legacy path: use original scar description
104
- lines.push(scar.description);
105
- }
106
- if (scar.counter_arguments.length > 0) {
107
- lines.push("");
108
- lines.push("*You might think:*");
109
- for (const counter of scar.counter_arguments.slice(0, 2)) {
110
- lines.push(` - ${counter}`);
107
+ // Stub low-confidence scars: render header only, skip the heavy body.
108
+ // ~66% of sub-threshold matches are N/A, so their full bodies are wasted
109
+ // tokens. Blocking-verification scars always render full regardless of score.
110
+ const isStub = scar.similarity < LOW_CONFIDENCE_THRESHOLD && !scar.required_verification?.blocking;
111
+ if (!isStub) {
112
+ // Use variant enforcement text if available (blind to variant name)
113
+ if (scar.variant_info?.has_variants && scar.variant_info.variant) {
114
+ const variantText = formatVariantEnforcement(scar.variant_info.variant, scar.title);
115
+ lines.push(variantText);
111
116
  }
112
- }
113
- if (scar.applies_when.length > 0) {
114
- lines.push("");
115
- lines.push("*Applies when:* " + scar.applies_when.slice(0, 3).join(", "));
116
- }
117
- // Render LLM-cooperative enforcement fields
118
- if (scar.why_this_matters) {
119
- lines.push("");
120
- lines.push(`**Why this matters:** ${scar.why_this_matters}`);
121
- }
122
- if (scar.action_protocol && scar.action_protocol.length > 0) {
123
- lines.push("");
124
- lines.push("**Action Protocol:**");
125
- scar.action_protocol.forEach((step, i) => {
126
- lines.push(` ${i + 1}. ${step}`);
127
- });
128
- }
129
- if (scar.self_check_criteria && scar.self_check_criteria.length > 0) {
130
- lines.push("");
131
- lines.push("**Self-Check:**");
132
- for (const criterion of scar.self_check_criteria) {
133
- lines.push(` - [ ] ${criterion}`);
117
+ else {
118
+ // Legacy path: use original scar description
119
+ lines.push(scar.description);
134
120
  }
135
- }
136
- // Render related knowledge triples
137
- if (scar.related_triples && scar.related_triples.length > 0) {
138
- lines.push("");
139
- lines.push("*Related knowledge:*");
140
- for (const triple of scar.related_triples) {
141
- lines.push(` - ${triple.subject} **${triple.predicate}** ${triple.object}`);
121
+ if (scar.counter_arguments.length > 0) {
122
+ lines.push("");
123
+ lines.push("*You might think:*");
124
+ for (const counter of scar.counter_arguments.slice(0, 2)) {
125
+ lines.push(` - ${counter}`);
126
+ }
127
+ }
128
+ if (scar.applies_when.length > 0) {
129
+ lines.push("");
130
+ lines.push("*Applies when:* " + scar.applies_when.slice(0, 3).join(", "));
131
+ }
132
+ // Render LLM-cooperative enforcement fields
133
+ if (scar.why_this_matters) {
134
+ lines.push("");
135
+ lines.push(`**Why this matters:** ${scar.why_this_matters}`);
136
+ }
137
+ if (scar.action_protocol && scar.action_protocol.length > 0) {
138
+ lines.push("");
139
+ lines.push("**Action Protocol:**");
140
+ scar.action_protocol.forEach((step, i) => {
141
+ lines.push(` ${i + 1}. ${step}`);
142
+ });
143
+ }
144
+ if (scar.self_check_criteria && scar.self_check_criteria.length > 0) {
145
+ lines.push("");
146
+ lines.push("**Self-Check:**");
147
+ for (const criterion of scar.self_check_criteria) {
148
+ lines.push(` - [ ] ${criterion}`);
149
+ }
150
+ }
151
+ // Render related knowledge triples
152
+ if (scar.related_triples && scar.related_triples.length > 0) {
153
+ lines.push("");
154
+ lines.push("*Related knowledge:*");
155
+ for (const triple of scar.related_triples) {
156
+ lines.push(` - ${triple.subject} **${triple.predicate}** ${triple.object}`);
157
+ }
158
+ }
159
+ if (scar.source_issue) {
160
+ lines.push(`*Source:* ${scar.source_issue}`);
142
161
  }
143
- }
144
- if (scar.source_issue) {
145
- lines.push(`*Source:* ${scar.source_issue}`);
146
162
  }
147
163
  lines.push("");
148
164
  lines.push("---");
@@ -13,9 +13,9 @@
13
13
  */
14
14
  import { v4 as uuidv4 } from "uuid";
15
15
  import { getTableName } from "../services/tier.js";
16
- import { getThreads, getCurrentSession } from "../services/session-state.js";
17
- import { resolveThread as resolveThreadInList, findThreadById, loadThreadsFile, saveThreadsFile, } from "../services/thread-manager.js";
18
- import { resolveThreadInSupabase } from "../services/thread-supabase.js";
16
+ import { getThreads, getCurrentSession, getProject } from "../services/session-state.js";
17
+ import { resolveThread as resolveThreadInList, findThreadById, findThreadByText, loadThreadsFile, saveThreadsFile, } from "../services/thread-manager.js";
18
+ import { resolveThreadInSupabase, getThreadFromSupabaseById, listThreadsFromSupabase, } from "../services/thread-supabase.js";
19
19
  import { writeTriplesForThreadResolution } from "../services/triple-writer.js";
20
20
  import { getEffectTracker } from "../services/effect-tracker.js";
21
21
  import { getAgentIdentity } from "../services/agent-detection.js";
@@ -66,12 +66,32 @@ export async function resolveThread(params) {
66
66
  effectiveTextMatch = undefined;
67
67
  }
68
68
  // Resolve the thread locally (in-memory / file)
69
- const resolved = resolveThreadInList(threads, {
69
+ let resolved = resolveThreadInList(threads, {
70
70
  threadId: effectiveThreadId,
71
71
  textMatch: effectiveTextMatch,
72
72
  sessionId,
73
73
  resolutionNote: params.resolution_note,
74
74
  });
75
+ // GIT-46: cross-session fallback. list_threads reads the Supabase
76
+ // source-of-truth, but the local match above only sees threads this session
77
+ // created/cached — so a thread created by another session is visible in
78
+ // list_threads yet "not found" here. Before failing, hydrate the thread from
79
+ // Supabase, splice it into the working set, and retry. The downstream flow
80
+ // (local cache write + Supabase resolve + metrics/triples) then proceeds
81
+ // unchanged, syncing the previously-diverged local cache back to the SOT.
82
+ if (!resolved) {
83
+ const hydrated = await hydrateThreadFromSupabase(effectiveThreadId, effectiveTextMatch);
84
+ if (hydrated) {
85
+ if (!findThreadById(threads, hydrated.id)) {
86
+ threads.push(hydrated);
87
+ }
88
+ resolved = resolveThreadInList(threads, {
89
+ threadId: hydrated.id,
90
+ sessionId,
91
+ resolutionNote: params.resolution_note,
92
+ });
93
+ }
94
+ }
75
95
  if (!resolved) {
76
96
  const latencyMs = timer.stop();
77
97
  const searchKey = params.thread_id || params.text_match;
@@ -172,4 +192,28 @@ export async function resolveThread(params) {
172
192
  display: wrapDisplay(resolveMsg),
173
193
  };
174
194
  }
195
+ /**
196
+ * GIT-46: fetch a thread from the Supabase source-of-truth when it isn't in
197
+ * the local/session cache (e.g. created by another session).
198
+ *
199
+ * - By thread_id: direct, project-agnostic lookup (thread_id is globally unique).
200
+ * - By text_match: scan the project's open threads and substring-match, mirroring
201
+ * the local findThreadByText behavior.
202
+ *
203
+ * Returns null when Supabase is unavailable or no thread matches — callers then
204
+ * fall through to the normal "Thread not found" path.
205
+ */
206
+ async function hydrateThreadFromSupabase(threadId, textMatch) {
207
+ if (threadId) {
208
+ return await getThreadFromSupabaseById(threadId);
209
+ }
210
+ if (textMatch) {
211
+ const project = getProject() || "default";
212
+ const openThreads = await listThreadsFromSupabase(project, { statusFilter: "open" });
213
+ if (openThreads) {
214
+ return findThreadByText(openThreads, textMatch);
215
+ }
216
+ }
217
+ return null;
218
+ }
175
219
  //# sourceMappingURL=resolve-thread.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitmem-mcp",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
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",