gitmem-mcp 1.0.0 → 1.0.1
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/CLAUDE.md.template +63 -55
- package/README.md +60 -29
- package/bin/gitmem.js +233 -109
- package/bin/init-wizard.js +642 -0
- package/bin/uninstall.js +288 -0
- package/dist/commands/check.js +20 -20
- package/dist/commands/check.js.map +1 -1
- package/dist/constants/closing-questions.d.ts +6 -0
- package/dist/constants/closing-questions.d.ts.map +1 -1
- package/dist/constants/closing-questions.js +65 -0
- package/dist/constants/closing-questions.js.map +1 -1
- package/dist/hooks/format-utils.d.ts +52 -0
- package/dist/hooks/format-utils.d.ts.map +1 -0
- package/dist/hooks/format-utils.js +89 -0
- package/dist/hooks/format-utils.js.map +1 -0
- package/dist/hooks/quick-retrieve.d.ts +30 -0
- package/dist/hooks/quick-retrieve.d.ts.map +1 -0
- package/dist/hooks/quick-retrieve.js +149 -0
- package/dist/hooks/quick-retrieve.js.map +1 -0
- package/dist/index.js +0 -0
- package/dist/schemas/active-sessions.d.ts +8 -8
- package/dist/schemas/analyze.d.ts +3 -3
- package/dist/schemas/common.d.ts +2 -2
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/schemas/common.js +1 -1
- package/dist/schemas/common.js.map +1 -1
- package/dist/schemas/create-decision.d.ts +3 -3
- package/dist/schemas/create-learning.d.ts +13 -13
- package/dist/schemas/log.d.ts +3 -3
- package/dist/schemas/prepare-context.d.ts +3 -3
- package/dist/schemas/recall.d.ts +3 -3
- package/dist/schemas/record-scar-usage-batch.d.ts +8 -3
- package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -1
- package/dist/schemas/record-scar-usage.d.ts +3 -0
- package/dist/schemas/record-scar-usage.d.ts.map +1 -1
- package/dist/schemas/record-scar-usage.js +1 -0
- package/dist/schemas/record-scar-usage.js.map +1 -1
- package/dist/schemas/registry.d.ts +18 -0
- package/dist/schemas/registry.d.ts.map +1 -0
- package/dist/schemas/registry.js +158 -0
- package/dist/schemas/registry.js.map +1 -0
- package/dist/schemas/save-transcript.d.ts +3 -3
- package/dist/schemas/search-transcripts.d.ts +33 -0
- package/dist/schemas/search-transcripts.d.ts.map +1 -0
- package/dist/schemas/search-transcripts.js +26 -0
- package/dist/schemas/search-transcripts.js.map +1 -0
- package/dist/schemas/search.d.ts +3 -3
- package/dist/schemas/session-close.d.ts +43 -15
- package/dist/schemas/session-close.d.ts.map +1 -1
- package/dist/schemas/session-close.js +7 -2
- package/dist/schemas/session-close.js.map +1 -1
- package/dist/schemas/session-start.d.ts +3 -3
- package/dist/schemas/thread.d.ts +3 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +82 -28
- package/dist/server.js.map +1 -1
- package/dist/services/active-sessions.d.ts +2 -1
- package/dist/services/active-sessions.d.ts.map +1 -1
- package/dist/services/active-sessions.js +130 -84
- package/dist/services/active-sessions.js.map +1 -1
- package/dist/services/analytics.d.ts.map +1 -1
- package/dist/services/analytics.js +1 -0
- package/dist/services/analytics.js.map +1 -1
- package/dist/services/behavioral-decay.d.ts +40 -0
- package/dist/services/behavioral-decay.d.ts.map +1 -0
- package/dist/services/behavioral-decay.js +110 -0
- package/dist/services/behavioral-decay.js.map +1 -0
- package/dist/services/bm25.d.ts +39 -0
- package/dist/services/bm25.d.ts.map +1 -0
- package/dist/services/bm25.js +132 -0
- package/dist/services/bm25.js.map +1 -0
- package/dist/services/cache.d.ts.map +1 -1
- package/dist/services/cache.js +9 -8
- package/dist/services/cache.js.map +1 -1
- package/dist/services/cache.test.js +17 -17
- package/dist/services/cache.test.js.map +1 -1
- package/dist/services/compliance-validator.d.ts.map +1 -1
- package/dist/services/compliance-validator.js +12 -1
- package/dist/services/compliance-validator.js.map +1 -1
- package/dist/services/display-protocol.d.ts +31 -0
- package/dist/services/display-protocol.d.ts.map +1 -0
- package/dist/services/display-protocol.js +73 -0
- package/dist/services/display-protocol.js.map +1 -0
- package/dist/services/effect-tracker.d.ts +81 -0
- package/dist/services/effect-tracker.d.ts.map +1 -0
- package/dist/services/effect-tracker.js +181 -0
- package/dist/services/effect-tracker.js.map +1 -0
- package/dist/services/file-lock.d.ts +31 -0
- package/dist/services/file-lock.d.ts.map +1 -0
- package/dist/services/file-lock.js +124 -0
- package/dist/services/file-lock.js.map +1 -0
- package/dist/services/gitmem-dir.d.ts +7 -0
- package/dist/services/gitmem-dir.d.ts.map +1 -1
- package/dist/services/gitmem-dir.js +21 -0
- package/dist/services/gitmem-dir.js.map +1 -1
- package/dist/services/local-file-storage.d.ts +3 -2
- package/dist/services/local-file-storage.d.ts.map +1 -1
- package/dist/services/local-file-storage.js +30 -43
- package/dist/services/local-file-storage.js.map +1 -1
- package/dist/services/local-vector-search.d.ts +10 -9
- package/dist/services/local-vector-search.d.ts.map +1 -1
- package/dist/services/local-vector-search.js +28 -23
- package/dist/services/local-vector-search.js.map +1 -1
- package/dist/services/metrics.d.ts +7 -2
- package/dist/services/metrics.d.ts.map +1 -1
- package/dist/services/metrics.js +41 -33
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/session-state.d.ts +8 -0
- package/dist/services/session-state.d.ts.map +1 -1
- package/dist/services/session-state.js +9 -2
- package/dist/services/session-state.js.map +1 -1
- package/dist/services/startup.d.ts +12 -13
- package/dist/services/startup.d.ts.map +1 -1
- package/dist/services/startup.js +104 -57
- package/dist/services/startup.js.map +1 -1
- package/dist/services/supabase-client.d.ts +2 -1
- package/dist/services/supabase-client.d.ts.map +1 -1
- package/dist/services/supabase-client.js +22 -16
- package/dist/services/supabase-client.js.map +1 -1
- package/dist/services/thread-dedup.d.ts +9 -0
- package/dist/services/thread-dedup.d.ts.map +1 -1
- package/dist/services/thread-dedup.js +27 -0
- package/dist/services/thread-dedup.js.map +1 -1
- package/dist/services/thread-manager.d.ts.map +1 -1
- package/dist/services/thread-manager.js +38 -16
- package/dist/services/thread-manager.js.map +1 -1
- package/dist/services/thread-suggestions.d.ts.map +1 -1
- package/dist/services/thread-suggestions.js +1 -1
- package/dist/services/thread-suggestions.js.map +1 -1
- package/dist/services/thread-supabase.d.ts +0 -1
- package/dist/services/thread-supabase.d.ts.map +1 -1
- package/dist/services/thread-supabase.js +83 -54
- package/dist/services/thread-supabase.js.map +1 -1
- package/dist/services/timezone.d.ts.map +1 -1
- package/dist/services/timezone.js +1 -0
- package/dist/services/timezone.js.map +1 -1
- package/dist/services/transcript-chunker.d.ts.map +1 -1
- package/dist/services/transcript-chunker.js +18 -4
- package/dist/services/transcript-chunker.js.map +1 -1
- package/dist/services/variant-generation.d.ts +41 -0
- package/dist/services/variant-generation.d.ts.map +1 -0
- package/dist/services/variant-generation.js +263 -0
- package/dist/services/variant-generation.js.map +1 -0
- package/dist/tools/absorb-observations.d.ts.map +1 -1
- package/dist/tools/absorb-observations.js +9 -0
- package/dist/tools/absorb-observations.js.map +1 -1
- package/dist/tools/analyze.d.ts.map +1 -1
- package/dist/tools/analyze.js +13 -2
- package/dist/tools/analyze.js.map +1 -1
- package/dist/tools/archive-learning.d.ts +28 -0
- package/dist/tools/archive-learning.d.ts.map +1 -0
- package/dist/tools/archive-learning.js +81 -0
- package/dist/tools/archive-learning.js.map +1 -0
- package/dist/tools/cleanup-threads.d.ts +1 -0
- package/dist/tools/cleanup-threads.d.ts.map +1 -1
- package/dist/tools/cleanup-threads.js +111 -18
- package/dist/tools/cleanup-threads.js.map +1 -1
- package/dist/tools/confirm-scars.d.ts.map +1 -1
- package/dist/tools/confirm-scars.js +8 -2
- package/dist/tools/confirm-scars.js.map +1 -1
- package/dist/tools/create-decision.d.ts.map +1 -1
- package/dist/tools/create-decision.js +11 -8
- package/dist/tools/create-decision.js.map +1 -1
- package/dist/tools/create-learning.d.ts.map +1 -1
- package/dist/tools/create-learning.js +35 -11
- package/dist/tools/create-learning.js.map +1 -1
- package/dist/tools/create-linear-issue.d.ts +18 -0
- package/dist/tools/create-linear-issue.d.ts.map +1 -0
- package/dist/tools/create-linear-issue.js +197 -0
- package/dist/tools/create-linear-issue.js.map +1 -0
- package/dist/tools/create-thread.d.ts +2 -1
- package/dist/tools/create-thread.d.ts.map +1 -1
- package/dist/tools/create-thread.js +9 -4
- package/dist/tools/create-thread.js.map +1 -1
- package/dist/tools/definitions.d.ts +785 -34
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +239 -95
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/dismiss-suggestion.d.ts +1 -0
- package/dist/tools/dismiss-suggestion.d.ts.map +1 -1
- package/dist/tools/dismiss-suggestion.js +4 -0
- package/dist/tools/dismiss-suggestion.js.map +1 -1
- package/dist/tools/graph-traverse.d.ts +1 -0
- package/dist/tools/graph-traverse.d.ts.map +1 -1
- package/dist/tools/graph-traverse.js +24 -9
- package/dist/tools/graph-traverse.js.map +1 -1
- package/dist/tools/list-threads.d.ts.map +1 -1
- package/dist/tools/list-threads.js +49 -5
- package/dist/tools/list-threads.js.map +1 -1
- package/dist/tools/log.d.ts +1 -0
- package/dist/tools/log.d.ts.map +1 -1
- package/dist/tools/log.js +84 -17
- package/dist/tools/log.js.map +1 -1
- package/dist/tools/prepare-context.d.ts +1 -0
- package/dist/tools/prepare-context.d.ts.map +1 -1
- package/dist/tools/prepare-context.js +15 -85
- package/dist/tools/prepare-context.js.map +1 -1
- package/dist/tools/promote-suggestion.d.ts +1 -0
- package/dist/tools/promote-suggestion.d.ts.map +1 -1
- package/dist/tools/promote-suggestion.js +5 -0
- package/dist/tools/promote-suggestion.js.map +1 -1
- package/dist/tools/recall.d.ts +2 -0
- package/dist/tools/recall.d.ts.map +1 -1
- package/dist/tools/recall.js +43 -10
- package/dist/tools/recall.js.map +1 -1
- package/dist/tools/recall.test.js +6 -6
- package/dist/tools/recall.test.js.map +1 -1
- package/dist/tools/record-scar-usage-batch.d.ts.map +1 -1
- package/dist/tools/record-scar-usage-batch.js +13 -0
- package/dist/tools/record-scar-usage-batch.js.map +1 -1
- package/dist/tools/record-scar-usage.d.ts.map +1 -1
- package/dist/tools/record-scar-usage.js +6 -0
- package/dist/tools/record-scar-usage.js.map +1 -1
- package/dist/tools/resolve-thread.d.ts.map +1 -1
- package/dist/tools/resolve-thread.js +57 -6
- package/dist/tools/resolve-thread.js.map +1 -1
- package/dist/tools/save-transcript.d.ts +1 -0
- package/dist/tools/save-transcript.d.ts.map +1 -1
- package/dist/tools/save-transcript.js +3 -1
- package/dist/tools/save-transcript.js.map +1 -1
- package/dist/tools/search-transcripts.d.ts +44 -0
- package/dist/tools/search-transcripts.d.ts.map +1 -0
- package/dist/tools/search-transcripts.js +158 -0
- package/dist/tools/search-transcripts.js.map +1 -0
- package/dist/tools/search.d.ts +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +74 -3
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/session-close.d.ts.map +1 -1
- package/dist/tools/session-close.js +563 -326
- package/dist/tools/session-close.js.map +1 -1
- package/dist/tools/session-start.d.ts +10 -6
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +317 -426
- package/dist/tools/session-start.js.map +1 -1
- package/dist/types/index.d.ts +37 -4
- package/dist/types/index.d.ts.map +1 -1
- package/hooks/hooks/hooks.json +8 -37
- package/hooks/scripts/auto-retrieve-hook.sh +163 -0
- package/hooks/scripts/post-tool-use.sh +0 -16
- package/hooks/scripts/recall-check.sh +0 -11
- package/hooks/scripts/session-close-check.sh +1 -1
- package/hooks/scripts/session-start.sh +89 -13
- package/hooks/tests/test-hooks.sh +3 -49
- package/package.json +3 -2
- package/schema/setup.sql +1 -1
|
@@ -2,31 +2,33 @@
|
|
|
2
2
|
* session_start Tool
|
|
3
3
|
*
|
|
4
4
|
* Initialize session, detect agent, load institutional context.
|
|
5
|
-
* Returns
|
|
5
|
+
* Returns threads and recent decisions. Scars surface via recall on demand.
|
|
6
6
|
*
|
|
7
|
-
* Performance target: <
|
|
7
|
+
* Performance target: <750ms (OD-645: Lean Start)
|
|
8
8
|
*
|
|
9
|
-
* OD-
|
|
10
|
-
*
|
|
9
|
+
* OD-645: Removed scar/wins queries from start pipeline.
|
|
10
|
+
* Scars load on-demand via recall(). Wins available via search/log.
|
|
11
|
+
* loadLastSession and loadRecentDecisions run in parallel.
|
|
12
|
+
* createSessionRecord is fire-and-forget.
|
|
11
13
|
*/
|
|
12
14
|
import * as fs from "fs";
|
|
13
15
|
import * as path from "path";
|
|
14
16
|
import { v4 as uuidv4 } from "uuid";
|
|
15
17
|
import { detectAgent } from "../services/agent-detection.js";
|
|
16
18
|
import * as supabase from "../services/supabase-client.js";
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
+
// OD-645: Scar search removed from start pipeline (loads on-demand via recall)
|
|
20
|
+
import { ensureInitialized } from "../services/startup.js";
|
|
19
21
|
import { hasSupabase } from "../services/tier.js";
|
|
20
22
|
import { getStorage } from "../services/storage.js";
|
|
21
23
|
import { Timer, recordMetrics, calculateContextBytes, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
|
|
22
24
|
import { setCurrentSession, getCurrentSession, addSurfacedScars, getSurfacedScars } from "../services/session-state.js"; // OD-547, OD-552
|
|
23
25
|
import { aggregateThreads, saveThreadsFile, loadThreadsFile, mergeThreadStates } from "../services/thread-manager.js"; // OD-thread-lifecycle
|
|
26
|
+
import { deduplicateThreadList } from "../services/thread-dedup.js"; // OD-641
|
|
24
27
|
import { loadActiveThreadsFromSupabase, archiveDormantThreads } from "../services/thread-supabase.js"; // OD-623, Phase 6
|
|
25
|
-
import { setGitmemDir, getSessionPath } from "../services/gitmem-dir.js";
|
|
28
|
+
import { setGitmemDir, getGitmemDir, getSessionPath, getConfigProject } from "../services/gitmem-dir.js";
|
|
26
29
|
import { registerSession, findSessionByHostPid, pruneStale, migrateFromLegacy } from "../services/active-sessions.js";
|
|
27
30
|
import * as os from "os";
|
|
28
31
|
import { formatDate } from "../services/timezone.js";
|
|
29
|
-
import { loadSuggestions, getPendingSuggestions } from "../services/thread-suggestions.js";
|
|
30
32
|
/**
|
|
31
33
|
* Normalize decisions from mixed formats (strings or objects) to string[].
|
|
32
34
|
* Historical sessions (pre-2026) stored {title, decision} objects.
|
|
@@ -35,6 +37,7 @@ import { loadSuggestions, getPendingSuggestions } from "../services/thread-sugge
|
|
|
35
37
|
function normalizeDecisions(decisions) {
|
|
36
38
|
return decisions.map((d) => typeof d === "string" ? d : d.title);
|
|
37
39
|
}
|
|
40
|
+
// OD-645: WinRecord removed (wins available via search/log)
|
|
38
41
|
/**
|
|
39
42
|
* Aggregate open threads across multiple recent sessions.
|
|
40
43
|
* Deduplicates by exact lowercase match. Excludes PROJECT STATE: threads
|
|
@@ -50,6 +53,18 @@ function normalizeDecisions(decisions) {
|
|
|
50
53
|
*/
|
|
51
54
|
async function loadLastSession(agent, project) {
|
|
52
55
|
const timer = new Timer();
|
|
56
|
+
if (!hasSupabase()) {
|
|
57
|
+
// Free tier: no session history from Supabase, use local threads only
|
|
58
|
+
const fileThreads = loadThreadsFile();
|
|
59
|
+
return {
|
|
60
|
+
session: null,
|
|
61
|
+
aggregated_open_threads: fileThreads.filter(t => t.status === "open"),
|
|
62
|
+
displayInfo: [],
|
|
63
|
+
latency_ms: timer.stop(),
|
|
64
|
+
network_call: false,
|
|
65
|
+
threadsFromSupabase: false,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
53
68
|
try {
|
|
54
69
|
// Use _lite view for performance (excludes embedding)
|
|
55
70
|
// OD-460: View now includes decisions/open_threads arrays
|
|
@@ -61,15 +76,15 @@ async function loadLastSession(agent, project) {
|
|
|
61
76
|
});
|
|
62
77
|
// OD-623: Try loading threads from Supabase (source of truth) first
|
|
63
78
|
let aggregated_open_threads;
|
|
64
|
-
let recently_resolved_threads;
|
|
65
79
|
let displayInfo = [];
|
|
80
|
+
let threadsFromSupabase = false;
|
|
66
81
|
const supabaseThreads = await loadActiveThreadsFromSupabase(project);
|
|
67
82
|
if (supabaseThreads !== null) {
|
|
68
83
|
// Supabase is source of truth for threads
|
|
69
84
|
aggregated_open_threads = supabaseThreads.open;
|
|
70
|
-
recently_resolved_threads = supabaseThreads.recentlyResolved;
|
|
71
85
|
displayInfo = supabaseThreads.displayInfo;
|
|
72
|
-
|
|
86
|
+
threadsFromSupabase = true;
|
|
87
|
+
console.error(`[session_start] Loaded ${aggregated_open_threads.length} open threads from Supabase`);
|
|
73
88
|
// Phase 6: Auto-archive dormant threads (fire-and-forget)
|
|
74
89
|
archiveDormantThreads(project).catch(() => { });
|
|
75
90
|
}
|
|
@@ -77,12 +92,11 @@ async function loadLastSession(agent, project) {
|
|
|
77
92
|
// Fallback: aggregate from session records (original behavior)
|
|
78
93
|
const threadResult = aggregateThreads(sessions);
|
|
79
94
|
aggregated_open_threads = threadResult.open;
|
|
80
|
-
|
|
81
|
-
console.error(`[session_start] Aggregated threads from sessions: ${aggregated_open_threads.length} open, ${recently_resolved_threads.length} recently resolved (Supabase thread query failed)`);
|
|
95
|
+
console.error(`[session_start] Aggregated ${aggregated_open_threads.length} open threads from sessions (Supabase thread query failed)`);
|
|
82
96
|
}
|
|
83
97
|
const latency_ms = timer.stop();
|
|
84
98
|
if (sessions.length === 0) {
|
|
85
|
-
return { session: null, aggregated_open_threads,
|
|
99
|
+
return { session: null, aggregated_open_threads, displayInfo, latency_ms, network_call: true, threadsFromSupabase };
|
|
86
100
|
}
|
|
87
101
|
// Find the most recent session that was properly closed
|
|
88
102
|
const closedSession = sessions.find((s) => s.close_compliance != null);
|
|
@@ -99,10 +113,10 @@ async function loadLastSession(agent, project) {
|
|
|
99
113
|
open_threads: session.open_threads || [],
|
|
100
114
|
},
|
|
101
115
|
aggregated_open_threads,
|
|
102
|
-
recently_resolved_threads,
|
|
103
116
|
displayInfo,
|
|
104
117
|
latency_ms,
|
|
105
118
|
network_call: true,
|
|
119
|
+
threadsFromSupabase,
|
|
106
120
|
};
|
|
107
121
|
}
|
|
108
122
|
return {
|
|
@@ -114,99 +128,46 @@ async function loadLastSession(agent, project) {
|
|
|
114
128
|
open_threads: closedSession.open_threads || [],
|
|
115
129
|
},
|
|
116
130
|
aggregated_open_threads,
|
|
117
|
-
recently_resolved_threads,
|
|
118
131
|
displayInfo,
|
|
119
132
|
latency_ms,
|
|
120
|
-
network_call: true,
|
|
133
|
+
network_call: true,
|
|
134
|
+
threadsFromSupabase,
|
|
121
135
|
};
|
|
122
136
|
}
|
|
123
137
|
catch (error) {
|
|
124
138
|
console.error("[session_start] Failed to load last session:", error);
|
|
125
|
-
return { session: null, aggregated_open_threads: [],
|
|
139
|
+
return { session: null, aggregated_open_threads: [], displayInfo: [], latency_ms: timer.stop(), network_call: true, threadsFromSupabase: false };
|
|
126
140
|
}
|
|
127
141
|
}
|
|
142
|
+
// OD-645: queryRelevantScars removed — scars load on-demand via recall()
|
|
128
143
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* - No file-based cache = no race conditions
|
|
133
|
-
* - Same query = same results every time
|
|
134
|
-
* - No Supabase hit = fast & scalable
|
|
135
|
-
*
|
|
136
|
-
* OD-489: Returns timing and network call info for instrumentation.
|
|
144
|
+
* OD-666: Load recent rapport summaries across all agents for this project.
|
|
145
|
+
* Returns up to 3 most recent sessions that have a non-null rapport_summary.
|
|
146
|
+
* Cross-agent by design: CLI session rapport visible to DAC's next session.
|
|
137
147
|
*/
|
|
138
|
-
async function
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
async function loadRecentRapport(project) {
|
|
149
|
+
if (!hasSupabase())
|
|
150
|
+
return [];
|
|
141
151
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (lastSession.key_decisions?.length) {
|
|
157
|
-
// Include up to 3 decisions to avoid query bloat
|
|
158
|
-
queryParts.push(lastSession.key_decisions.slice(0, 3).join(" "));
|
|
159
|
-
}
|
|
160
|
-
if (lastSession.open_threads?.length) {
|
|
161
|
-
// Include up to 3 open threads
|
|
162
|
-
queryParts.push(lastSession.open_threads.slice(0, 3).map(t => typeof t === "string" ? t : t.text).join(" "));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// Default query only if nothing else available
|
|
166
|
-
const query = queryParts.length > 0
|
|
167
|
-
? queryParts.join(" ")
|
|
168
|
-
: "deployment verification testing integration";
|
|
169
|
-
// Ensure local search is initialized
|
|
170
|
-
await ensureInitialized(proj);
|
|
171
|
-
// Use local vector search if available (OD-473)
|
|
172
|
-
if (isLocalSearchAvailable(proj)) {
|
|
173
|
-
console.error("[session_start] Using local vector search");
|
|
174
|
-
const scars = await localScarSearch(query, 5, proj);
|
|
175
|
-
const latency_ms = timer.stop();
|
|
176
|
-
return {
|
|
177
|
-
scars,
|
|
178
|
-
local_search: true,
|
|
179
|
-
latency_ms,
|
|
180
|
-
network_call: false, // LOCAL - no network call!
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// Fallback to Supabase if local search not available
|
|
184
|
-
console.error("[session_start] Falling back to Supabase scar search");
|
|
185
|
-
const { results } = await supabase.cachedScarSearch(query, 5, proj);
|
|
186
|
-
const scars = results.map((scar) => ({
|
|
187
|
-
id: scar.id,
|
|
188
|
-
title: scar.title,
|
|
189
|
-
severity: scar.severity || "medium",
|
|
190
|
-
description: scar.description || "",
|
|
191
|
-
counter_arguments: scar.counter_arguments || [],
|
|
192
|
-
similarity: scar.similarity || 0,
|
|
152
|
+
const sessions = await supabase.listRecords({
|
|
153
|
+
table: "orchestra_sessions_lite",
|
|
154
|
+
columns: "agent,rapport_summary,created_at",
|
|
155
|
+
filters: { project },
|
|
156
|
+
limit: 20, // Fetch more to find ones with rapport
|
|
157
|
+
orderBy: { column: "created_at", ascending: false },
|
|
158
|
+
});
|
|
159
|
+
return sessions
|
|
160
|
+
.filter((s) => s.rapport_summary)
|
|
161
|
+
.slice(0, 3)
|
|
162
|
+
.map((s) => ({
|
|
163
|
+
agent: s.agent,
|
|
164
|
+
summary: s.rapport_summary,
|
|
165
|
+
date: formatDate(s.created_at),
|
|
193
166
|
}));
|
|
194
|
-
const latency_ms = timer.stop();
|
|
195
|
-
return {
|
|
196
|
-
scars,
|
|
197
|
-
local_search: false,
|
|
198
|
-
latency_ms,
|
|
199
|
-
network_call: true, // REMOTE - hit Supabase
|
|
200
|
-
};
|
|
201
167
|
}
|
|
202
168
|
catch (error) {
|
|
203
|
-
console.error("[session_start] Failed to
|
|
204
|
-
return
|
|
205
|
-
scars: [],
|
|
206
|
-
local_search: false,
|
|
207
|
-
latency_ms: timer.stop(),
|
|
208
|
-
network_call: true, // Assume network was attempted
|
|
209
|
-
};
|
|
169
|
+
console.error("[session_start] Failed to load rapport summaries:", error);
|
|
170
|
+
return [];
|
|
210
171
|
}
|
|
211
172
|
}
|
|
212
173
|
/**
|
|
@@ -255,60 +216,21 @@ async function loadRecentDecisions(project, limit = 5) {
|
|
|
255
216
|
};
|
|
256
217
|
}
|
|
257
218
|
}
|
|
258
|
-
|
|
259
|
-
* Load recent wins from institutional memory.
|
|
260
|
-
* Queries orchestra_learnings_lite for learning_type="win".
|
|
261
|
-
* Runs in parallel with scars/decisions — hidden by scar search bottleneck.
|
|
262
|
-
*/
|
|
263
|
-
async function loadRecentWins(project, limit = 3, maxAgeDays = 7) {
|
|
264
|
-
const timer = new Timer();
|
|
265
|
-
try {
|
|
266
|
-
// Use cached wins query (same pattern as cachedListDecisions)
|
|
267
|
-
const { data: records, cache_hit, cache_age_ms } = await supabase.cachedListWins(project, limit + 5, // Fetch extra for date filtering
|
|
268
|
-
"id,title,description,created_at,source_linear_issue");
|
|
269
|
-
const latency_ms = timer.stop();
|
|
270
|
-
// Filter to last N days in-memory
|
|
271
|
-
const cutoff = new Date();
|
|
272
|
-
cutoff.setDate(cutoff.getDate() - maxAgeDays);
|
|
273
|
-
const cutoffStr = cutoff.toISOString();
|
|
274
|
-
const filtered = records
|
|
275
|
-
.filter((r) => r.created_at >= cutoffStr)
|
|
276
|
-
.slice(0, limit);
|
|
277
|
-
const wins = filtered.map((r) => ({
|
|
278
|
-
id: r.id,
|
|
279
|
-
title: r.title,
|
|
280
|
-
description: (r.description || "").slice(0, 200),
|
|
281
|
-
date: formatDate(r.created_at.split("T")[0]),
|
|
282
|
-
source_issue: r.source_linear_issue,
|
|
283
|
-
}));
|
|
284
|
-
console.error(`[session_start] Loaded ${records.length} wins, ${wins.length} after date filter, cache_hit=${cache_hit}`);
|
|
285
|
-
return {
|
|
286
|
-
wins,
|
|
287
|
-
cache_hit,
|
|
288
|
-
cache_age_ms,
|
|
289
|
-
latency_ms,
|
|
290
|
-
network_call: !cache_hit,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
console.error("[session_start] Failed to load wins:", error);
|
|
295
|
-
return {
|
|
296
|
-
wins: [],
|
|
297
|
-
cache_hit: false,
|
|
298
|
-
latency_ms: timer.stop(),
|
|
299
|
-
network_call: true,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
}
|
|
219
|
+
// OD-645: loadRecentWins removed — wins available via search/log on-demand
|
|
303
220
|
/**
|
|
304
221
|
* Create a new session record
|
|
305
222
|
*
|
|
306
223
|
* OD-489: Returns timing and network call info for instrumentation.
|
|
307
224
|
*/
|
|
308
|
-
async function createSessionRecord(agent, project, linearIssue
|
|
309
|
-
|
|
225
|
+
async function createSessionRecord(agent, project, linearIssue, preGeneratedId // OD-645: Accept pre-generated UUID for fire-and-forget pattern
|
|
226
|
+
) {
|
|
227
|
+
const sessionId = preGeneratedId || uuidv4();
|
|
310
228
|
const today = new Date().toISOString().split("T")[0];
|
|
311
229
|
const timer = new Timer();
|
|
230
|
+
if (!hasSupabase()) {
|
|
231
|
+
// Free tier: session tracked locally only
|
|
232
|
+
return { session_id: sessionId, latency_ms: timer.stop(), network_call: false };
|
|
233
|
+
}
|
|
312
234
|
try {
|
|
313
235
|
// OD-cast: Capture asciinema recording path from Docker entrypoint
|
|
314
236
|
const recordingPath = process.env.GITMEM_RECORDING_PATH || null;
|
|
@@ -342,10 +264,36 @@ async function createSessionRecord(agent, project, linearIssue) {
|
|
|
342
264
|
};
|
|
343
265
|
}
|
|
344
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Mark a displaced session as superseded in Supabase.
|
|
269
|
+
* Fire-and-forget — failures logged but don't block session_start.
|
|
270
|
+
* Only sets close_compliance if it's currently null (truly abandoned).
|
|
271
|
+
*/
|
|
272
|
+
async function markSessionSuperseded(oldSessionId, newSessionId) {
|
|
273
|
+
try {
|
|
274
|
+
// Check if session already has close_compliance (was properly closed)
|
|
275
|
+
const existing = await supabase.directQuery("orchestra_sessions", { filters: { id: oldSessionId }, select: "close_compliance" });
|
|
276
|
+
if (existing.length > 0 && existing[0].close_compliance != null) {
|
|
277
|
+
// Already closed — don't overwrite
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await supabase.directPatch("orchestra_sessions", { id: oldSessionId }, {
|
|
281
|
+
close_compliance: {
|
|
282
|
+
close_type: "superseded",
|
|
283
|
+
superseded_by: newSessionId,
|
|
284
|
+
superseded_at: new Date().toISOString(),
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
console.error(`[session_start] Marked session ${oldSessionId.slice(0, 8)} as superseded by ${newSessionId.slice(0, 8)}`);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
console.error(`[session_start] Failed to mark session ${oldSessionId.slice(0, 8)} as superseded:`, error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
345
293
|
/**
|
|
346
294
|
* Free tier session_start — all-local, no Supabase
|
|
347
295
|
*/
|
|
348
|
-
async function sessionStartFree(params, env, agent, project, timer, metricsId, existingSessionId, existingStartedAt) {
|
|
296
|
+
async function sessionStartFree(params, env, agent, project, timer, metricsId, existingSessionId, existingStartedAt, forceCarryActivity) {
|
|
349
297
|
const storage = getStorage();
|
|
350
298
|
const isResuming = !!existingSessionId;
|
|
351
299
|
const sessionId = existingSessionId || uuidv4();
|
|
@@ -353,7 +301,6 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
353
301
|
// Load last session from local storage
|
|
354
302
|
let lastSession = null;
|
|
355
303
|
let freeAggregatedThreads = [];
|
|
356
|
-
let freeRecentlyResolved = [];
|
|
357
304
|
try {
|
|
358
305
|
const sessions = await storage.query("sessions", {
|
|
359
306
|
order: "session_date.desc",
|
|
@@ -362,7 +309,6 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
362
309
|
// Aggregate threads across recent sessions (OD-thread-lifecycle)
|
|
363
310
|
const freeThreadResult = aggregateThreads(sessions);
|
|
364
311
|
freeAggregatedThreads = freeThreadResult.open;
|
|
365
|
-
freeRecentlyResolved = freeThreadResult.recently_resolved;
|
|
366
312
|
const closedSession = sessions.find((s) => s.close_compliance != null) || sessions[0];
|
|
367
313
|
if (closedSession) {
|
|
368
314
|
lastSession = {
|
|
@@ -377,27 +323,7 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
377
323
|
catch (error) {
|
|
378
324
|
console.error("[session_start] Failed to load last session:", error);
|
|
379
325
|
}
|
|
380
|
-
//
|
|
381
|
-
let scars = [];
|
|
382
|
-
try {
|
|
383
|
-
const queryParts = [];
|
|
384
|
-
if (params.issue_title)
|
|
385
|
-
queryParts.push(params.issue_title);
|
|
386
|
-
if (params.issue_description)
|
|
387
|
-
queryParts.push(params.issue_description.slice(0, 200));
|
|
388
|
-
if (params.issue_labels?.length)
|
|
389
|
-
queryParts.push(params.issue_labels.join(" "));
|
|
390
|
-
if (queryParts.length === 0 && lastSession) {
|
|
391
|
-
if (lastSession.title && lastSession.title !== "Untitled Session") {
|
|
392
|
-
queryParts.push(lastSession.title);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
const query = queryParts.length > 0 ? queryParts.join(" ") : "deployment verification testing";
|
|
396
|
-
scars = await storage.search(query, 5);
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
console.error("[session_start] Failed to query scars:", error);
|
|
400
|
-
}
|
|
326
|
+
// OD-645: Scars removed from start pipeline — load on-demand via recall
|
|
401
327
|
// Load recent decisions from local storage (time-scoped to 5 days)
|
|
402
328
|
let decisions = [];
|
|
403
329
|
try {
|
|
@@ -421,30 +347,7 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
421
347
|
catch (error) {
|
|
422
348
|
console.error("[session_start] Failed to load decisions:", error);
|
|
423
349
|
}
|
|
424
|
-
//
|
|
425
|
-
let freeWins = [];
|
|
426
|
-
try {
|
|
427
|
-
const winRecords = await storage.query("learnings", {
|
|
428
|
-
order: "created_at.desc",
|
|
429
|
-
limit: 8,
|
|
430
|
-
});
|
|
431
|
-
const winCutoff = new Date();
|
|
432
|
-
winCutoff.setDate(winCutoff.getDate() - 7);
|
|
433
|
-
const winCutoffStr = winCutoff.toISOString();
|
|
434
|
-
freeWins = winRecords
|
|
435
|
-
.filter((w) => w.learning_type === "win" && w.created_at >= winCutoffStr)
|
|
436
|
-
.slice(0, 3)
|
|
437
|
-
.map((w) => ({
|
|
438
|
-
id: w.id,
|
|
439
|
-
title: w.title,
|
|
440
|
-
description: (w.description || "").slice(0, 200),
|
|
441
|
-
date: formatDate(w.created_at.split("T")[0]),
|
|
442
|
-
source_issue: w.source_linear_issue,
|
|
443
|
-
}));
|
|
444
|
-
}
|
|
445
|
-
catch (error) {
|
|
446
|
-
console.error("[session_start] Failed to load wins:", error);
|
|
447
|
-
}
|
|
350
|
+
// OD-645: Wins removed from start pipeline — available via search/log
|
|
448
351
|
// Create session record locally (skip if resuming existing session)
|
|
449
352
|
if (!isResuming) {
|
|
450
353
|
try {
|
|
@@ -473,37 +376,32 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
473
376
|
?.map((t) => typeof t === "string" ? t : t.text)
|
|
474
377
|
.find((t) => t.startsWith("PROJECT STATE:"))
|
|
475
378
|
?.replace(/^PROJECT STATE:\s*/, "");
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
});
|
|
481
|
-
const surfacedAt = new Date().toISOString();
|
|
482
|
-
const surfacedScars = scars.map((scar) => ({
|
|
483
|
-
scar_id: scar.id,
|
|
484
|
-
scar_title: scar.title,
|
|
485
|
-
scar_severity: scar.severity || "medium",
|
|
486
|
-
surfaced_at: surfacedAt,
|
|
487
|
-
source: "session_start",
|
|
488
|
-
}));
|
|
379
|
+
// OD-645: Simplified performance data (no scars/wins)
|
|
380
|
+
const performance = buildPerformanceData("session_start", latencyMs, decisions.length + (lastSession ? 1 : 0));
|
|
381
|
+
// OD-645: surfacedScars initialized empty — populated by recall during session
|
|
382
|
+
const surfacedScars = [];
|
|
489
383
|
// GIT-20: Persist to per-session dir, legacy file, and registry
|
|
490
384
|
// writeSessionFiles merges with existing file threads to preserve mid-session creations
|
|
491
385
|
let freeMergedThreads = freeAggregatedThreads;
|
|
492
386
|
try {
|
|
493
|
-
freeMergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, freeAggregatedThreads);
|
|
387
|
+
freeMergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, freeAggregatedThreads, undefined, false, false, isResuming ? existingStartedAt : undefined);
|
|
494
388
|
}
|
|
495
389
|
catch (error) {
|
|
496
390
|
console.warn("[session_start] Failed to persist session files:", error);
|
|
497
391
|
}
|
|
498
|
-
// t-f7c2fa01: On resume, preserve original startedAt so session_close duration is accurate
|
|
392
|
+
// t-f7c2fa01: On resume OR force, preserve original startedAt so session_close duration is accurate
|
|
393
|
+
const freeMergedScars = forceCarryActivity ? [...forceCarryActivity.surfacedScars, ...surfacedScars] : surfacedScars;
|
|
499
394
|
setCurrentSession({
|
|
500
395
|
sessionId,
|
|
501
396
|
linearIssue: params.linear_issue,
|
|
502
397
|
agent,
|
|
503
398
|
startedAt: (isResuming && existingStartedAt) || new Date(),
|
|
504
|
-
surfacedScars,
|
|
399
|
+
surfacedScars: freeMergedScars,
|
|
400
|
+
observations: forceCarryActivity?.observations,
|
|
401
|
+
children: forceCarryActivity?.children,
|
|
505
402
|
threads: freeMergedThreads,
|
|
506
403
|
});
|
|
404
|
+
// OD-645: No scars/wins in start result
|
|
507
405
|
const freeResult = {
|
|
508
406
|
session_id: sessionId,
|
|
509
407
|
agent,
|
|
@@ -512,10 +410,9 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
512
410
|
last_session: lastSession,
|
|
513
411
|
...(projectState && { project_state: projectState }),
|
|
514
412
|
...(freeMergedThreads.length > 0 && { open_threads: freeMergedThreads }),
|
|
515
|
-
...(freeRecentlyResolved.length > 0 && { recently_resolved: freeRecentlyResolved }),
|
|
516
|
-
relevant_scars: scars,
|
|
517
413
|
recent_decisions: decisions,
|
|
518
|
-
|
|
414
|
+
gitmem_dir: getGitmemDir(),
|
|
415
|
+
project,
|
|
519
416
|
performance,
|
|
520
417
|
};
|
|
521
418
|
freeResult.display = formatStartDisplay(freeResult);
|
|
@@ -606,19 +503,35 @@ function checkExistingSession(agent, force) {
|
|
|
606
503
|
* GIT-20: Write session state to per-session directory, legacy file, and registry.
|
|
607
504
|
* Consolidates write logic used by session_start (main + free) and session_refresh.
|
|
608
505
|
*/
|
|
609
|
-
function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, recordingPath, isRefresh) {
|
|
506
|
+
function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, recordingPath, isRefresh, supabaseAuthoritative, startedAt) {
|
|
610
507
|
const gitmemDir = path.join(process.cwd(), ".gitmem");
|
|
611
508
|
if (!fs.existsSync(gitmemDir)) {
|
|
612
509
|
fs.mkdirSync(gitmemDir, { recursive: true });
|
|
613
510
|
}
|
|
614
511
|
setGitmemDir(gitmemDir);
|
|
512
|
+
// Preserve original started_at on resume/refresh to keep duration accurate
|
|
513
|
+
let effectiveStartedAt = startedAt?.toISOString() || new Date().toISOString();
|
|
514
|
+
if (isRefresh || startedAt) {
|
|
515
|
+
// On refresh or resume, try to read the existing started_at from the session file
|
|
516
|
+
try {
|
|
517
|
+
const existingPath = getSessionPath(sessionId, "session.json");
|
|
518
|
+
if (fs.existsSync(existingPath)) {
|
|
519
|
+
const existing = JSON.parse(fs.readFileSync(existingPath, "utf-8"));
|
|
520
|
+
if (existing.started_at) {
|
|
521
|
+
effectiveStartedAt = existing.started_at;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch { /* use calculated value */ }
|
|
526
|
+
}
|
|
615
527
|
const data = {
|
|
616
528
|
session_id: sessionId,
|
|
617
529
|
agent,
|
|
618
|
-
started_at:
|
|
530
|
+
started_at: effectiveStartedAt,
|
|
619
531
|
project,
|
|
620
532
|
hostname: os.hostname(),
|
|
621
533
|
pid: process.pid,
|
|
534
|
+
gitmem_dir: gitmemDir,
|
|
622
535
|
surfaced_scars: surfacedScars,
|
|
623
536
|
threads,
|
|
624
537
|
...(recordingPath && { recording_path: recordingPath }),
|
|
@@ -636,7 +549,7 @@ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, re
|
|
|
636
549
|
// Legacy active-session.json write removed — per-session dir is the source of truth
|
|
637
550
|
// 3. Register in active-sessions registry (skip on refresh — already registered)
|
|
638
551
|
if (!isRefresh) {
|
|
639
|
-
registerSession({
|
|
552
|
+
const displaced = registerSession({
|
|
640
553
|
session_id: sessionId,
|
|
641
554
|
agent,
|
|
642
555
|
started_at: data.started_at,
|
|
@@ -644,106 +557,114 @@ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, re
|
|
|
644
557
|
pid: process.pid,
|
|
645
558
|
project,
|
|
646
559
|
});
|
|
560
|
+
// Mark displaced sessions as superseded in Supabase (fire-and-forget)
|
|
561
|
+
for (const oldId of displaced) {
|
|
562
|
+
markSessionSuperseded(oldId, sessionId).catch(() => { });
|
|
563
|
+
}
|
|
647
564
|
}
|
|
648
|
-
// 4. Threads file —
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
565
|
+
// 4. Threads file — when Supabase is authoritative, REPLACE file contents with Supabase
|
|
566
|
+
// data, preserving only local-only threads (created mid-session but not yet synced).
|
|
567
|
+
// This prevents the feedback loop where resolved threads accumulate in threads.json
|
|
568
|
+
// and inflate the count on each session_start.
|
|
652
569
|
const existingFileThreads = loadThreadsFile();
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
570
|
+
let merged;
|
|
571
|
+
if (supabaseAuthoritative) {
|
|
572
|
+
// Supabase is source of truth — use its threads, but preserve any local-only threads
|
|
573
|
+
// (threads in the file that don't exist in the Supabase set, e.g. created via create_thread
|
|
574
|
+
// mid-session but not yet synced to Supabase by session_close).
|
|
575
|
+
const supabaseIds = new Set(threads.map(t => t.id));
|
|
576
|
+
const localOnlyThreads = existingFileThreads.filter(t => !supabaseIds.has(t.id));
|
|
577
|
+
if (localOnlyThreads.length > 0) {
|
|
578
|
+
console.error(`[session_start] Preserving ${localOnlyThreads.length} local-only threads not yet in Supabase`);
|
|
579
|
+
}
|
|
580
|
+
merged = deduplicateThreadList([...threads, ...localOnlyThreads]);
|
|
658
581
|
}
|
|
582
|
+
else {
|
|
583
|
+
// Fallback (free tier / Supabase offline): merge with existing file
|
|
584
|
+
merged = existingFileThreads.length > 0
|
|
585
|
+
? deduplicateThreadList(mergeThreadStates(threads, existingFileThreads))
|
|
586
|
+
: deduplicateThreadList(threads);
|
|
587
|
+
}
|
|
588
|
+
saveThreadsFile(merged);
|
|
659
589
|
return merged;
|
|
660
590
|
}
|
|
661
591
|
/**
|
|
662
592
|
* Format pre-formatted display string for session_start/session_refresh results.
|
|
663
|
-
*
|
|
593
|
+
*
|
|
594
|
+
* This produces TWO parts:
|
|
595
|
+
* 1. A clean visual block (Option A style) for terminal display
|
|
596
|
+
* 2. An aggressive prompt injection wrapper that forces the LLM to echo
|
|
597
|
+
* the visual block verbatim instead of adding its own commentary
|
|
598
|
+
*
|
|
599
|
+
* The "Karpathy injection" works by embedding strong display instructions
|
|
600
|
+
* directly in the MCP response data that the LLM processes. This overrides
|
|
601
|
+
* any system-prompt ceremony (like CLAUDE.md "I've read..." boilerplate).
|
|
602
|
+
*
|
|
603
|
+
* Design: 80-char terminal safe, monospace-friendly, no markdown headers.
|
|
604
|
+
*/
|
|
605
|
+
/**
|
|
606
|
+
* Strip thread ID prefixes (e.g., "t-d573c47f: ") from display text.
|
|
664
607
|
*/
|
|
608
|
+
function stripThreadPrefix(text) {
|
|
609
|
+
return text.replace(/^t-[a-f0-9]+:\s*/i, "");
|
|
610
|
+
}
|
|
665
611
|
function formatStartDisplay(result, displayInfoMap) {
|
|
666
|
-
const
|
|
667
|
-
//
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
if (result.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
if (info) {
|
|
693
|
-
const status = info.lifecycle_status.toUpperCase();
|
|
694
|
-
const score = info.vitality_score.toFixed(2);
|
|
695
|
-
const age = info.days_since_touch === 0 ? "today" : `${info.days_since_touch}d ago`;
|
|
696
|
-
lines.push(`- \`${t.id}\`: ${text} **[${status} ${score}]** (${info.thread_class}, ${age})`);
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
lines.push(`- \`${t.id}\`: ${text}`);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (result.open_threads.length > 7) {
|
|
703
|
-
lines.push(`- *... and ${result.open_threads.length - 7} more*`);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
// Suggested threads (Phase 5: Implicit Thread Detection)
|
|
707
|
-
if (result.suggested_threads?.length) {
|
|
708
|
-
lines.push("");
|
|
709
|
-
lines.push(`### Suggested Threads (${result.suggested_threads.length})`);
|
|
710
|
-
lines.push(`*Recurring topics not yet tracked:*`);
|
|
711
|
-
for (const s of result.suggested_threads.slice(0, 3)) {
|
|
712
|
-
const text = s.text.length > 60 ? s.text.slice(0, 57) + "..." : s.text;
|
|
713
|
-
lines.push(`- \`${s.id}\`: ${text} (${s.evidence_sessions.length} sessions)`);
|
|
714
|
-
}
|
|
715
|
-
if (result.suggested_threads.length > 3) {
|
|
716
|
-
lines.push(`- *... and ${result.suggested_threads.length - 3} more*`);
|
|
612
|
+
const visual = [];
|
|
613
|
+
// Line 1: product name + session state
|
|
614
|
+
const stateLabel = result.refreshed ? "refreshed" : (result.resumed ? "resumed" : "active");
|
|
615
|
+
visual.push(`gitmem ── ${stateLabel}`);
|
|
616
|
+
// Line 2: session ID + agent + project
|
|
617
|
+
const parts = [result.session_id, result.agent];
|
|
618
|
+
if (result.project)
|
|
619
|
+
parts.push(result.project);
|
|
620
|
+
visual.push(parts.join(" · "));
|
|
621
|
+
// Threads section — top 5 by vitality, truncated to 60 chars
|
|
622
|
+
const hasThreads = result.open_threads && result.open_threads.length > 0;
|
|
623
|
+
const hasDecisions = result.recent_decisions && result.recent_decisions.length > 0;
|
|
624
|
+
if (hasThreads) {
|
|
625
|
+
visual.push("");
|
|
626
|
+
visual.push(`Threads (${result.open_threads.length})`);
|
|
627
|
+
const enriched = result.open_threads.map(t => ({
|
|
628
|
+
thread: t,
|
|
629
|
+
info: displayInfoMap?.get(t.id),
|
|
630
|
+
}));
|
|
631
|
+
enriched.sort((a, b) => (b.info?.vitality_score ?? 0) - (a.info?.vitality_score ?? 0));
|
|
632
|
+
const maxShow = 5;
|
|
633
|
+
for (let i = 0; i < Math.min(enriched.length, maxShow); i++) {
|
|
634
|
+
const raw = enriched[i].thread.text;
|
|
635
|
+
const text = stripThreadPrefix(raw);
|
|
636
|
+
const truncated = text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
637
|
+
visual.push(` ${truncated}`);
|
|
717
638
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
// Relevant scars
|
|
721
|
-
if (result.relevant_scars?.length) {
|
|
722
|
-
lines.push("");
|
|
723
|
-
lines.push(`### Relevant Scars (${result.relevant_scars.length})`);
|
|
724
|
-
for (const s of result.relevant_scars.slice(0, 5)) {
|
|
725
|
-
const severity = (s.severity || "medium").toUpperCase();
|
|
726
|
-
const title = s.title.length > 60 ? s.title.slice(0, 57) + "..." : s.title;
|
|
727
|
-
lines.push(`- **[${severity}]** ${title}`);
|
|
639
|
+
if (result.open_threads.length > maxShow) {
|
|
640
|
+
visual.push(` +${result.open_threads.length - maxShow} more`);
|
|
728
641
|
}
|
|
729
642
|
}
|
|
730
|
-
//
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
643
|
+
// Decisions section — top 3 with compact date
|
|
644
|
+
if (hasDecisions) {
|
|
645
|
+
visual.push("");
|
|
646
|
+
visual.push(`Decisions (${result.recent_decisions.length})`);
|
|
734
647
|
for (const d of result.recent_decisions.slice(0, 3)) {
|
|
735
|
-
|
|
648
|
+
const title = d.title.length > 50 ? d.title.slice(0, 47) + "..." : d.title;
|
|
649
|
+
visual.push(` ${title} · ${d.date}`);
|
|
736
650
|
}
|
|
737
651
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
652
|
+
if (!hasThreads && !hasDecisions) {
|
|
653
|
+
visual.push("");
|
|
654
|
+
visual.push("No threads or decisions.");
|
|
655
|
+
}
|
|
656
|
+
const visualBlock = visual.join("\n");
|
|
657
|
+
// ── Display-first layout ──
|
|
658
|
+
// Visual block comes FIRST so Claude Code's collapsed tool output shows
|
|
659
|
+
// "gitmem ── active" as the first visible line, not LLM instructions.
|
|
660
|
+
// LLM instructions follow AFTER — the model reads the full result regardless.
|
|
661
|
+
return [
|
|
662
|
+
visualBlock,
|
|
663
|
+
"",
|
|
664
|
+
"───────────────────────────────────────────────────",
|
|
665
|
+
"DISPLAY PROTOCOL: Output the content above verbatim as your response text.",
|
|
666
|
+
"Tool results are collapsed in the CLI — the user cannot see them unless you echo them.",
|
|
667
|
+
].join("\n");
|
|
747
668
|
}
|
|
748
669
|
/**
|
|
749
670
|
* Execute session_start tool
|
|
@@ -761,89 +682,88 @@ export async function sessionStart(params) {
|
|
|
761
682
|
// 1. Detect agent (or use provided)
|
|
762
683
|
const env = detectAgent();
|
|
763
684
|
const agent = params.agent_identity || env.agent;
|
|
764
|
-
const project = params.project || "
|
|
685
|
+
const project = params.project || getConfigProject() || "default";
|
|
765
686
|
// OD-558: Check for existing active session — reuse session_id but still load full context
|
|
766
687
|
const existingSession = checkExistingSession(agent, params.force);
|
|
767
688
|
const isResuming = existingSession !== null;
|
|
689
|
+
// t-f7c2fa01: When force:true kills an existing session, carry forward its startedAt
|
|
690
|
+
// so session_close duration reflects the full conversation, not just the new session.
|
|
691
|
+
// Also carry forward activity counts (recalls, observations) so standard close isn't rejected.
|
|
692
|
+
const priorSession = params.force ? getCurrentSession() : null;
|
|
693
|
+
const forceCarryStartedAt = priorSession?.startedAt;
|
|
694
|
+
const forceCarrySurfacedScars = priorSession?.surfacedScars || [];
|
|
695
|
+
const forceCarryObservations = priorSession?.observations || [];
|
|
696
|
+
const forceCarryChildren = priorSession?.children || [];
|
|
768
697
|
// Free tier: all-local path
|
|
769
698
|
if (!hasSupabase()) {
|
|
770
|
-
return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt);
|
|
699
|
+
return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt || forceCarryStartedAt, priorSession ? { surfacedScars: forceCarrySurfacedScars, observations: forceCarryObservations, children: forceCarryChildren } : undefined);
|
|
771
700
|
}
|
|
772
|
-
// 2. Load last session
|
|
773
|
-
//
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
// OD-473: Scars use local vector search (deterministic, no race conditions)
|
|
778
|
-
// Pass full lastSession for richer context (title + decisions + open_threads)
|
|
779
|
-
// Wins query runs parallel — hidden by scar search bottleneck (~611ms > ~300ms)
|
|
780
|
-
const [scarsResult, decisionsResult, winsResult] = await Promise.all([
|
|
781
|
-
queryRelevantScars(params.issue_title, params.issue_description, params.issue_labels, project, lastSession),
|
|
701
|
+
// 2. OD-645: Load last session + decisions in parallel (was sequential)
|
|
702
|
+
// Scars and wins removed from pipeline — load on-demand via recall/search
|
|
703
|
+
// OD-666: Rapport loading disabled — recording kept in session_close but not injected
|
|
704
|
+
const [lastSessionResult, decisionsResult] = await Promise.all([
|
|
705
|
+
loadLastSession(agent, project),
|
|
782
706
|
loadRecentDecisions(project, 3),
|
|
783
|
-
loadRecentWins(project, 3, 7),
|
|
784
707
|
]);
|
|
785
|
-
const
|
|
708
|
+
const lastSession = lastSessionResult.session;
|
|
786
709
|
const decisions = decisionsResult.decisions;
|
|
787
|
-
|
|
788
|
-
const
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
const surfacedScars = scars.map((scar) => ({
|
|
792
|
-
scar_id: scar.id,
|
|
793
|
-
scar_title: scar.title,
|
|
794
|
-
scar_severity: scar.severity || "medium",
|
|
795
|
-
surfaced_at: surfacedAt,
|
|
796
|
-
source: "session_start",
|
|
797
|
-
}));
|
|
798
|
-
// 4. Create session record (skip if resuming existing session — OD-558)
|
|
710
|
+
// OD-645: surfacedScars initialized empty — populated by recall/confirm_scars during session
|
|
711
|
+
const surfacedScars = [];
|
|
712
|
+
// 3. Create session record — fire-and-forget (OD-645)
|
|
713
|
+
// UUID generated locally, Supabase write runs in background
|
|
799
714
|
let sessionId;
|
|
800
|
-
let sessionCreateResult;
|
|
801
715
|
if (isResuming) {
|
|
802
716
|
sessionId = existingSession.sessionId;
|
|
803
|
-
sessionCreateResult = { session_id: sessionId, latency_ms: 0, network_call: false };
|
|
804
717
|
console.error(`[session_start] Resuming session ${sessionId} — skipping record creation`);
|
|
805
718
|
}
|
|
806
719
|
else {
|
|
807
|
-
|
|
808
|
-
|
|
720
|
+
sessionId = uuidv4();
|
|
721
|
+
// Fire-and-forget: don't await the Supabase write
|
|
722
|
+
createSessionRecord(agent, project, params.linear_issue, sessionId).catch(() => { });
|
|
723
|
+
// Mark prior in-memory session as superseded (force=true path)
|
|
724
|
+
// Registry displacement in writeSessionFiles handles the registry case,
|
|
725
|
+
// but priorSession may not be in the registry (e.g., after MCP restart)
|
|
726
|
+
if (priorSession && priorSession.sessionId !== sessionId) {
|
|
727
|
+
markSessionSuperseded(priorSession.sessionId, sessionId).catch(() => { });
|
|
728
|
+
}
|
|
809
729
|
}
|
|
730
|
+
// Warm local scar cache for this project (fire-and-forget, non-blocking)
|
|
731
|
+
// By the time user calls recall(), cache should be hot (~1s background load)
|
|
732
|
+
ensureInitialized(project).catch((err) => {
|
|
733
|
+
console.error(`[session_start] Cache warmup failed for ${project}: ${err}`);
|
|
734
|
+
});
|
|
735
|
+
// Refresh behavioral decay scores (fire-and-forget, zero latency impact)
|
|
736
|
+
// Aggregates scar_usage patterns to update decay_multiplier on scars
|
|
737
|
+
import("../services/behavioral-decay.js").then(({ refreshBehavioralScores }) => {
|
|
738
|
+
refreshBehavioralScores().catch((err) => {
|
|
739
|
+
console.error(`[session_start] Behavioral decay refresh failed: ${err}`);
|
|
740
|
+
});
|
|
741
|
+
}).catch(() => { });
|
|
810
742
|
const latencyMs = timer.stop();
|
|
811
|
-
const memoriesSurfaced = scars.map((s) => s.id);
|
|
812
|
-
const similarityScores = scars.map((s) => s.similarity);
|
|
813
743
|
// OD-534: Extract PROJECT STATE from last session if present
|
|
814
744
|
const projectState = lastSession?.open_threads
|
|
815
745
|
?.map((t) => typeof t === "string" ? t : t.text)
|
|
816
746
|
.find(t => t.startsWith("PROJECT STATE:"))
|
|
817
747
|
?.replace(/^PROJECT STATE:\s*/, "");
|
|
818
|
-
// OD-
|
|
748
|
+
// OD-645: Simplified performance breakdown (no scar_search, wins, session_create)
|
|
819
749
|
const breakdown = {
|
|
820
|
-
last_session: buildComponentPerformance(lastSessionResult.latency_ms, "supabase",
|
|
821
|
-
lastSessionResult.network_call, lastSessionResult.network_call ? "miss" : "hit"),
|
|
822
|
-
scar_search: buildComponentPerformance(scarsResult.latency_ms, usedLocalSearch ? "local_cache" : "supabase", scarsResult.network_call, usedLocalSearch ? "hit" : "miss"),
|
|
750
|
+
last_session: buildComponentPerformance(lastSessionResult.latency_ms, "supabase", lastSessionResult.network_call, lastSessionResult.network_call ? "miss" : "hit"),
|
|
823
751
|
decisions: buildComponentPerformance(decisionsResult.latency_ms, decisionsResult.cache_hit ? "local_cache" : "supabase", decisionsResult.network_call, decisionsResult.cache_hit ? "hit" : "miss"),
|
|
824
|
-
wins: buildComponentPerformance(winsResult.latency_ms, winsResult.cache_hit ? "local_cache" : "supabase", winsResult.network_call, winsResult.cache_hit ? "hit" : "miss"),
|
|
825
|
-
session_create: buildComponentPerformance(sessionCreateResult.latency_ms, "supabase", // Session create always writes to Supabase
|
|
826
|
-
sessionCreateResult.network_call, "not_applicable" // Write operation, not a cache lookup
|
|
827
|
-
),
|
|
828
752
|
};
|
|
829
753
|
// Build performance data with detailed breakdown
|
|
830
|
-
const performance = buildPerformanceData("session_start", latencyMs,
|
|
831
|
-
memoriesSurfaced,
|
|
832
|
-
similarityScores,
|
|
833
|
-
search_mode: usedLocalSearch ? "local" : "remote",
|
|
754
|
+
const performance = buildPerformanceData("session_start", latencyMs, decisions.length + (lastSession ? 1 : 0), {
|
|
834
755
|
breakdown,
|
|
835
756
|
});
|
|
836
757
|
// Capture recording path from Docker entrypoint env var
|
|
837
758
|
const recordingPath = process.env.GITMEM_RECORDING_PATH || undefined;
|
|
838
759
|
const aggregatedThreads = lastSessionResult.aggregated_open_threads;
|
|
839
|
-
const recentlyResolvedThreads = lastSessionResult.recently_resolved_threads;
|
|
840
760
|
const threadDisplayInfo = lastSessionResult.displayInfo;
|
|
841
761
|
// GIT-20: Persist to per-session dir, legacy file, and active-sessions registry
|
|
842
|
-
//
|
|
843
|
-
//
|
|
762
|
+
// When Supabase was the thread source, replace file contents (not merge) to prevent
|
|
763
|
+
// feedback loop accumulation of resolved threads.
|
|
844
764
|
let mergedThreads = aggregatedThreads;
|
|
845
765
|
try {
|
|
846
|
-
mergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, aggregatedThreads, recordingPath);
|
|
766
|
+
mergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, aggregatedThreads, recordingPath, false, lastSessionResult.threadsFromSupabase, isResuming ? (existingSession?.startedAt || undefined) : undefined);
|
|
847
767
|
}
|
|
848
768
|
catch (error) {
|
|
849
769
|
console.warn("[session_start] Failed to persist session files:", error);
|
|
@@ -851,65 +771,62 @@ export async function sessionStart(params) {
|
|
|
851
771
|
// OD-547: Set active session for variant assignment in recall
|
|
852
772
|
// OD-552: Initialize with surfaced scars for auto-bridge at close time
|
|
853
773
|
// OD-thread-lifecycle: Initialize with merged threads (aggregated + mid-session preserved)
|
|
854
|
-
// t-f7c2fa01: On resume, preserve original startedAt so session_close duration is accurate
|
|
774
|
+
// t-f7c2fa01: On resume OR force, preserve original startedAt so session_close duration is accurate
|
|
775
|
+
const mergedScars = [...forceCarrySurfacedScars, ...surfacedScars];
|
|
855
776
|
setCurrentSession({
|
|
856
777
|
sessionId,
|
|
857
778
|
linearIssue: params.linear_issue,
|
|
858
779
|
agent,
|
|
859
|
-
|
|
860
|
-
|
|
780
|
+
project,
|
|
781
|
+
startedAt: (isResuming && existingSession?.startedAt) || forceCarryStartedAt || new Date(),
|
|
782
|
+
surfacedScars: mergedScars,
|
|
783
|
+
observations: forceCarryObservations,
|
|
784
|
+
children: forceCarryChildren,
|
|
861
785
|
threads: mergedThreads,
|
|
862
786
|
});
|
|
787
|
+
// OD-645: Build result — no scars/wins (load on-demand via recall/search)
|
|
788
|
+
const openOnly = mergedThreads.filter(t => t.status === "open" || !t.status);
|
|
789
|
+
// Strip bulky fields from last_session — open_threads used only for PROJECT STATE extraction above
|
|
790
|
+
const slimLastSession = lastSession ? {
|
|
791
|
+
id: lastSession.id,
|
|
792
|
+
title: lastSession.title,
|
|
793
|
+
date: lastSession.date,
|
|
794
|
+
key_decisions: lastSession.key_decisions,
|
|
795
|
+
open_threads: [], // stripped — stale prior-session threads add noise
|
|
796
|
+
} : null;
|
|
863
797
|
const result = {
|
|
864
798
|
session_id: sessionId,
|
|
865
799
|
agent,
|
|
866
800
|
...(isResuming && { resumed: true }),
|
|
867
801
|
detected_environment: env,
|
|
868
|
-
last_session:
|
|
802
|
+
last_session: slimLastSession,
|
|
869
803
|
...(projectState && { project_state: projectState }), // OD-534
|
|
870
|
-
...(
|
|
871
|
-
open_threads: mergedThreads,
|
|
872
|
-
}),
|
|
873
|
-
...(recentlyResolvedThreads.length > 0 && {
|
|
874
|
-
recently_resolved: recentlyResolvedThreads,
|
|
875
|
-
}),
|
|
876
|
-
...(() => {
|
|
877
|
-
const pending = getPendingSuggestions(loadSuggestions());
|
|
878
|
-
return pending.length > 0 ? { suggested_threads: pending } : {};
|
|
879
|
-
})(),
|
|
880
|
-
relevant_scars: scars,
|
|
804
|
+
...(openOnly.length > 0 && { open_threads: openOnly }),
|
|
881
805
|
recent_decisions: decisions,
|
|
882
|
-
...(wins.length > 0 && { recent_wins: wins }),
|
|
883
806
|
...(recordingPath && { recording_path: recordingPath }),
|
|
807
|
+
gitmem_dir: getGitmemDir(),
|
|
808
|
+
project,
|
|
884
809
|
performance,
|
|
885
810
|
};
|
|
886
|
-
// Record metrics
|
|
811
|
+
// Record metrics (OD-645: simplified — no scar-related fields)
|
|
887
812
|
recordMetrics({
|
|
888
813
|
id: metricsId,
|
|
889
814
|
session_id: sessionId,
|
|
890
815
|
agent: agent,
|
|
891
816
|
tool_name: "session_start",
|
|
892
817
|
query_text: [params.issue_title, params.issue_description].filter(Boolean).join(" ").slice(0, 500),
|
|
893
|
-
tables_searched:
|
|
894
|
-
? ["orchestra_sessions_lite", "orchestra_decisions_lite", "orchestra_learnings_lite"]
|
|
895
|
-
: ["orchestra_sessions_lite", "orchestra_learnings", "orchestra_decisions_lite", "orchestra_learnings_lite"],
|
|
818
|
+
tables_searched: ["orchestra_sessions_lite", "orchestra_decisions_lite"],
|
|
896
819
|
latency_ms: latencyMs,
|
|
897
|
-
result_count:
|
|
898
|
-
similarity_scores: similarityScores,
|
|
820
|
+
result_count: decisions.length + (lastSession ? 1 : 0),
|
|
899
821
|
context_bytes: calculateContextBytes(result),
|
|
900
822
|
phase_tag: "session_start",
|
|
901
823
|
linear_issue: params.linear_issue,
|
|
902
|
-
memories_surfaced: memoriesSurfaced,
|
|
903
824
|
metadata: {
|
|
904
825
|
project,
|
|
905
826
|
has_last_session: !!lastSession,
|
|
906
|
-
scars_count: scars.length,
|
|
907
827
|
decisions_count: decisions.length,
|
|
908
|
-
|
|
909
|
-
open_threads_count: lastSessionResult.aggregated_open_threads.length,
|
|
910
|
-
used_local_search: usedLocalSearch, // OD-473: deterministic local search
|
|
828
|
+
open_threads_count: aggregatedThreads.length,
|
|
911
829
|
decisions_cache_hit: decisionsResult.cache_hit,
|
|
912
|
-
// OD-489: Detailed instrumentation
|
|
913
830
|
network_calls_made: performance.network_calls_made,
|
|
914
831
|
fully_local: performance.fully_local,
|
|
915
832
|
},
|
|
@@ -941,7 +858,7 @@ export async function sessionRefresh(params) {
|
|
|
941
858
|
if (currentSession) {
|
|
942
859
|
sessionId = currentSession.sessionId;
|
|
943
860
|
agent = currentSession.agent || "CLI";
|
|
944
|
-
project = params.project || "
|
|
861
|
+
project = params.project || currentSession.project || "default";
|
|
945
862
|
}
|
|
946
863
|
else {
|
|
947
864
|
// GIT-20: Fallback — check registry for this process, then legacy file
|
|
@@ -961,7 +878,7 @@ export async function sessionRefresh(params) {
|
|
|
961
878
|
}
|
|
962
879
|
sessionId = raw.session_id;
|
|
963
880
|
agent = raw.agent || "CLI";
|
|
964
|
-
project = params.project || raw.project || "
|
|
881
|
+
project = params.project || raw.project || "default";
|
|
965
882
|
}
|
|
966
883
|
// Free tier: all-local path (reuse session_start free path)
|
|
967
884
|
if (!hasSupabase()) {
|
|
@@ -973,49 +890,32 @@ export async function sessionRefresh(params) {
|
|
|
973
890
|
performance: buildPerformanceData("session_refresh", timer.stop(), 0),
|
|
974
891
|
};
|
|
975
892
|
}
|
|
976
|
-
// 2.
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const [
|
|
980
|
-
|
|
893
|
+
// 2. OD-645: Load last session + decisions in parallel (same as session_start)
|
|
894
|
+
// Scars and wins removed — load on-demand via recall/search
|
|
895
|
+
// OD-666: Rapport loading disabled — recording kept in session_close but not injected
|
|
896
|
+
const [lastSessionResult, decisionsResult] = await Promise.all([
|
|
897
|
+
loadLastSession(agent, project),
|
|
981
898
|
loadRecentDecisions(project, 3),
|
|
982
|
-
loadRecentWins(project, 3, 7),
|
|
983
899
|
]);
|
|
984
|
-
const
|
|
900
|
+
const lastSession = lastSessionResult.session;
|
|
985
901
|
const decisions = decisionsResult.decisions;
|
|
986
|
-
|
|
987
|
-
const usedLocalSearch = scarsResult.local_search;
|
|
988
|
-
// 3. Build surfaced scars and merge with existing
|
|
989
|
-
const surfacedAt = new Date().toISOString();
|
|
990
|
-
const newSurfacedScars = scars.map((scar) => ({
|
|
991
|
-
scar_id: scar.id,
|
|
992
|
-
scar_title: scar.title,
|
|
993
|
-
scar_severity: scar.severity || "medium",
|
|
994
|
-
surfaced_at: surfacedAt,
|
|
995
|
-
source: "session_start", // Same source — this is a refresh of start context
|
|
996
|
-
}));
|
|
997
|
-
// Merge: add new scars to existing (addSurfacedScars deduplicates by scar_id)
|
|
998
|
-
addSurfacedScars(newSurfacedScars);
|
|
902
|
+
// OD-645: surfacedScars not re-queried on refresh — existing ones preserved in session state
|
|
999
903
|
const refreshAggregatedThreads = lastSessionResult.aggregated_open_threads;
|
|
1000
|
-
const recentlyResolvedThreads = lastSessionResult.recently_resolved_threads;
|
|
1001
904
|
const refreshDisplayInfo = lastSessionResult.displayInfo;
|
|
1002
|
-
//
|
|
905
|
+
// 3. Extract PROJECT STATE (OD-534)
|
|
1003
906
|
const projectState = lastSession?.open_threads
|
|
1004
907
|
?.map((t) => typeof t === "string" ? t : t.text)
|
|
1005
908
|
.find(t => t.startsWith("PROJECT STATE:"))
|
|
1006
909
|
?.replace(/^PROJECT STATE:\s*/, "");
|
|
1007
|
-
//
|
|
910
|
+
// 4. OD-645: Simplified performance breakdown (no scar_search, wins)
|
|
1008
911
|
const latencyMs = timer.stop();
|
|
1009
912
|
const breakdown = {
|
|
1010
913
|
last_session: buildComponentPerformance(lastSessionResult.latency_ms, "supabase", lastSessionResult.network_call, lastSessionResult.network_call ? "miss" : "hit"),
|
|
1011
|
-
scar_search: buildComponentPerformance(scarsResult.latency_ms, usedLocalSearch ? "local_cache" : "supabase", scarsResult.network_call, usedLocalSearch ? "hit" : "miss"),
|
|
1012
914
|
decisions: buildComponentPerformance(decisionsResult.latency_ms, decisionsResult.cache_hit ? "local_cache" : "supabase", decisionsResult.network_call, decisionsResult.cache_hit ? "hit" : "miss"),
|
|
1013
|
-
wins: buildComponentPerformance(winsResult.latency_ms, winsResult.cache_hit ? "local_cache" : "supabase", winsResult.network_call, winsResult.cache_hit ? "hit" : "miss"),
|
|
1014
915
|
};
|
|
1015
|
-
const
|
|
1016
|
-
const similarityScores = scars.map((s) => s.similarity);
|
|
1017
|
-
const performance = buildPerformanceData("session_refresh", latencyMs, scars.length + decisions.length + wins.length + (lastSession ? 1 : 0), { memoriesSurfaced, similarityScores, search_mode: usedLocalSearch ? "local" : "remote", breakdown });
|
|
916
|
+
const performance = buildPerformanceData("session_refresh", latencyMs, decisions.length + (lastSession ? 1 : 0), { breakdown });
|
|
1018
917
|
const recordingPath = process.env.GITMEM_RECORDING_PATH || undefined;
|
|
918
|
+
// OD-645: Build result — no scars/wins
|
|
1019
919
|
const result = {
|
|
1020
920
|
session_id: sessionId,
|
|
1021
921
|
agent,
|
|
@@ -1023,64 +923,55 @@ export async function sessionRefresh(params) {
|
|
|
1023
923
|
detected_environment: detectAgent(),
|
|
1024
924
|
last_session: lastSession,
|
|
1025
925
|
...(projectState && { project_state: projectState }),
|
|
1026
|
-
// open_threads
|
|
1027
|
-
relevant_scars: scars,
|
|
926
|
+
// open_threads filled after merge below
|
|
1028
927
|
recent_decisions: decisions,
|
|
1029
|
-
|
|
928
|
+
// OD-666: Rapport disabled — not injected into session context
|
|
1030
929
|
...(recordingPath && { recording_path: recordingPath }),
|
|
930
|
+
project,
|
|
1031
931
|
performance,
|
|
1032
932
|
};
|
|
1033
933
|
// GIT-20: Update per-session dir and legacy file with refreshed context
|
|
1034
|
-
|
|
934
|
+
const existingSurfacedScars = Array.isArray(getSurfacedScars()) ? getSurfacedScars() : [];
|
|
1035
935
|
let refreshMergedThreads = refreshAggregatedThreads;
|
|
1036
936
|
try {
|
|
1037
|
-
|
|
1038
|
-
refreshMergedThreads = writeSessionFiles(sessionId, agent, project, allSurfacedScars, refreshAggregatedThreads, recordingPath, true);
|
|
937
|
+
refreshMergedThreads = writeSessionFiles(sessionId, agent, project, existingSurfacedScars, refreshAggregatedThreads, recordingPath, true, lastSessionResult.threadsFromSupabase);
|
|
1039
938
|
console.error(`[session_refresh] Context refreshed for session ${sessionId}`);
|
|
1040
939
|
}
|
|
1041
940
|
catch (error) {
|
|
1042
941
|
console.warn("[session_refresh] Failed to update session files:", error);
|
|
1043
942
|
}
|
|
1044
|
-
// Add merged threads to result
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (recentlyResolvedThreads.length > 0) {
|
|
1049
|
-
result.recently_resolved = recentlyResolvedThreads;
|
|
943
|
+
// Add merged threads to result (only open threads)
|
|
944
|
+
const refreshOpenOnly = refreshMergedThreads.filter(t => t.status === "open" || !t.status);
|
|
945
|
+
if (refreshOpenOnly.length > 0) {
|
|
946
|
+
result.open_threads = refreshOpenOnly;
|
|
1050
947
|
}
|
|
1051
|
-
//
|
|
948
|
+
// 5. Update in-memory session state with merged threads
|
|
1052
949
|
setCurrentSession({
|
|
1053
950
|
sessionId,
|
|
1054
951
|
agent,
|
|
952
|
+
project,
|
|
1055
953
|
startedAt: currentSession?.startedAt || new Date(),
|
|
1056
|
-
surfacedScars:
|
|
954
|
+
surfacedScars: currentSession?.surfacedScars || [],
|
|
1057
955
|
threads: refreshMergedThreads,
|
|
1058
956
|
linearIssue: currentSession?.linearIssue,
|
|
1059
957
|
});
|
|
1060
|
-
// Record metrics
|
|
958
|
+
// Record metrics (OD-645: simplified — no scar-related fields)
|
|
1061
959
|
recordMetrics({
|
|
1062
960
|
id: metricsId,
|
|
1063
961
|
session_id: sessionId,
|
|
1064
962
|
agent: agent,
|
|
1065
963
|
tool_name: "session_refresh",
|
|
1066
964
|
query_text: "mid-session context refresh",
|
|
1067
|
-
tables_searched:
|
|
1068
|
-
? ["orchestra_sessions_lite", "orchestra_decisions_lite", "orchestra_learnings_lite"]
|
|
1069
|
-
: ["orchestra_sessions_lite", "orchestra_learnings", "orchestra_decisions_lite", "orchestra_learnings_lite"],
|
|
965
|
+
tables_searched: ["orchestra_sessions_lite", "orchestra_decisions_lite"],
|
|
1070
966
|
latency_ms: latencyMs,
|
|
1071
|
-
result_count:
|
|
1072
|
-
similarity_scores: similarityScores,
|
|
967
|
+
result_count: decisions.length + (lastSession ? 1 : 0),
|
|
1073
968
|
context_bytes: calculateContextBytes(result),
|
|
1074
969
|
phase_tag: "session_refresh",
|
|
1075
|
-
memories_surfaced: memoriesSurfaced,
|
|
1076
970
|
metadata: {
|
|
1077
971
|
project,
|
|
1078
972
|
has_last_session: !!lastSession,
|
|
1079
|
-
scars_count: scars.length,
|
|
1080
973
|
decisions_count: decisions.length,
|
|
1081
|
-
wins_count: wins.length,
|
|
1082
974
|
open_threads_count: refreshMergedThreads.length,
|
|
1083
|
-
used_local_search: usedLocalSearch,
|
|
1084
975
|
network_calls_made: performance.network_calls_made,
|
|
1085
976
|
fully_local: performance.fully_local,
|
|
1086
977
|
},
|