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 +10 -0
- package/dist/services/thread-supabase.d.ts +14 -0
- package/dist/services/thread-supabase.js +32 -0
- package/dist/tools/recall.d.ts +10 -0
- package/dist/tools/recall.js +64 -48
- package/dist/tools/resolve-thread.js +48 -4
- package/package.json +1 -1
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).
|
package/dist/tools/recall.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/tools/recall.js
CHANGED
|
@@ -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
|
|
84
|
-
const confidenceTag = scar.similarity <
|
|
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
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|