gitmem-mcp 1.0.0 → 1.0.2
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 +7 -0
- package/CLAUDE.md.template +63 -55
- package/README.md +79 -163
- 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/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-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 +319 -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,38 @@ 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
|
+
if (!hasSupabase())
|
|
274
|
+
return; // Free tier: no remote session tracking
|
|
275
|
+
try {
|
|
276
|
+
// Check if session already has close_compliance (was properly closed)
|
|
277
|
+
const existing = await supabase.directQuery("orchestra_sessions", { filters: { id: oldSessionId }, select: "close_compliance" });
|
|
278
|
+
if (existing.length > 0 && existing[0].close_compliance != null) {
|
|
279
|
+
// Already closed — don't overwrite
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
await supabase.directPatch("orchestra_sessions", { id: oldSessionId }, {
|
|
283
|
+
close_compliance: {
|
|
284
|
+
close_type: "superseded",
|
|
285
|
+
superseded_by: newSessionId,
|
|
286
|
+
superseded_at: new Date().toISOString(),
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
console.error(`[session_start] Marked session ${oldSessionId.slice(0, 8)} as superseded by ${newSessionId.slice(0, 8)}`);
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.error(`[session_start] Failed to mark session ${oldSessionId.slice(0, 8)} as superseded:`, error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
345
295
|
/**
|
|
346
296
|
* Free tier session_start — all-local, no Supabase
|
|
347
297
|
*/
|
|
348
|
-
async function sessionStartFree(params, env, agent, project, timer, metricsId, existingSessionId, existingStartedAt) {
|
|
298
|
+
async function sessionStartFree(params, env, agent, project, timer, metricsId, existingSessionId, existingStartedAt, forceCarryActivity) {
|
|
349
299
|
const storage = getStorage();
|
|
350
300
|
const isResuming = !!existingSessionId;
|
|
351
301
|
const sessionId = existingSessionId || uuidv4();
|
|
@@ -353,7 +303,6 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
353
303
|
// Load last session from local storage
|
|
354
304
|
let lastSession = null;
|
|
355
305
|
let freeAggregatedThreads = [];
|
|
356
|
-
let freeRecentlyResolved = [];
|
|
357
306
|
try {
|
|
358
307
|
const sessions = await storage.query("sessions", {
|
|
359
308
|
order: "session_date.desc",
|
|
@@ -362,7 +311,6 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
362
311
|
// Aggregate threads across recent sessions (OD-thread-lifecycle)
|
|
363
312
|
const freeThreadResult = aggregateThreads(sessions);
|
|
364
313
|
freeAggregatedThreads = freeThreadResult.open;
|
|
365
|
-
freeRecentlyResolved = freeThreadResult.recently_resolved;
|
|
366
314
|
const closedSession = sessions.find((s) => s.close_compliance != null) || sessions[0];
|
|
367
315
|
if (closedSession) {
|
|
368
316
|
lastSession = {
|
|
@@ -377,27 +325,7 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
377
325
|
catch (error) {
|
|
378
326
|
console.error("[session_start] Failed to load last session:", error);
|
|
379
327
|
}
|
|
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
|
-
}
|
|
328
|
+
// OD-645: Scars removed from start pipeline — load on-demand via recall
|
|
401
329
|
// Load recent decisions from local storage (time-scoped to 5 days)
|
|
402
330
|
let decisions = [];
|
|
403
331
|
try {
|
|
@@ -421,30 +349,7 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
421
349
|
catch (error) {
|
|
422
350
|
console.error("[session_start] Failed to load decisions:", error);
|
|
423
351
|
}
|
|
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
|
-
}
|
|
352
|
+
// OD-645: Wins removed from start pipeline — available via search/log
|
|
448
353
|
// Create session record locally (skip if resuming existing session)
|
|
449
354
|
if (!isResuming) {
|
|
450
355
|
try {
|
|
@@ -473,37 +378,32 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
473
378
|
?.map((t) => typeof t === "string" ? t : t.text)
|
|
474
379
|
.find((t) => t.startsWith("PROJECT STATE:"))
|
|
475
380
|
?.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
|
-
}));
|
|
381
|
+
// OD-645: Simplified performance data (no scars/wins)
|
|
382
|
+
const performance = buildPerformanceData("session_start", latencyMs, decisions.length + (lastSession ? 1 : 0));
|
|
383
|
+
// OD-645: surfacedScars initialized empty — populated by recall during session
|
|
384
|
+
const surfacedScars = [];
|
|
489
385
|
// GIT-20: Persist to per-session dir, legacy file, and registry
|
|
490
386
|
// writeSessionFiles merges with existing file threads to preserve mid-session creations
|
|
491
387
|
let freeMergedThreads = freeAggregatedThreads;
|
|
492
388
|
try {
|
|
493
|
-
freeMergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, freeAggregatedThreads);
|
|
389
|
+
freeMergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, freeAggregatedThreads, undefined, false, false, isResuming ? existingStartedAt : undefined);
|
|
494
390
|
}
|
|
495
391
|
catch (error) {
|
|
496
392
|
console.warn("[session_start] Failed to persist session files:", error);
|
|
497
393
|
}
|
|
498
|
-
// t-f7c2fa01: On resume, preserve original startedAt so session_close duration is accurate
|
|
394
|
+
// t-f7c2fa01: On resume OR force, preserve original startedAt so session_close duration is accurate
|
|
395
|
+
const freeMergedScars = forceCarryActivity ? [...forceCarryActivity.surfacedScars, ...surfacedScars] : surfacedScars;
|
|
499
396
|
setCurrentSession({
|
|
500
397
|
sessionId,
|
|
501
398
|
linearIssue: params.linear_issue,
|
|
502
399
|
agent,
|
|
503
400
|
startedAt: (isResuming && existingStartedAt) || new Date(),
|
|
504
|
-
surfacedScars,
|
|
401
|
+
surfacedScars: freeMergedScars,
|
|
402
|
+
observations: forceCarryActivity?.observations,
|
|
403
|
+
children: forceCarryActivity?.children,
|
|
505
404
|
threads: freeMergedThreads,
|
|
506
405
|
});
|
|
406
|
+
// OD-645: No scars/wins in start result
|
|
507
407
|
const freeResult = {
|
|
508
408
|
session_id: sessionId,
|
|
509
409
|
agent,
|
|
@@ -512,10 +412,9 @@ async function sessionStartFree(params, env, agent, project, timer, metricsId, e
|
|
|
512
412
|
last_session: lastSession,
|
|
513
413
|
...(projectState && { project_state: projectState }),
|
|
514
414
|
...(freeMergedThreads.length > 0 && { open_threads: freeMergedThreads }),
|
|
515
|
-
...(freeRecentlyResolved.length > 0 && { recently_resolved: freeRecentlyResolved }),
|
|
516
|
-
relevant_scars: scars,
|
|
517
415
|
recent_decisions: decisions,
|
|
518
|
-
|
|
416
|
+
gitmem_dir: getGitmemDir(),
|
|
417
|
+
project,
|
|
519
418
|
performance,
|
|
520
419
|
};
|
|
521
420
|
freeResult.display = formatStartDisplay(freeResult);
|
|
@@ -606,19 +505,35 @@ function checkExistingSession(agent, force) {
|
|
|
606
505
|
* GIT-20: Write session state to per-session directory, legacy file, and registry.
|
|
607
506
|
* Consolidates write logic used by session_start (main + free) and session_refresh.
|
|
608
507
|
*/
|
|
609
|
-
function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, recordingPath, isRefresh) {
|
|
508
|
+
function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, recordingPath, isRefresh, supabaseAuthoritative, startedAt) {
|
|
610
509
|
const gitmemDir = path.join(process.cwd(), ".gitmem");
|
|
611
510
|
if (!fs.existsSync(gitmemDir)) {
|
|
612
511
|
fs.mkdirSync(gitmemDir, { recursive: true });
|
|
613
512
|
}
|
|
614
513
|
setGitmemDir(gitmemDir);
|
|
514
|
+
// Preserve original started_at on resume/refresh to keep duration accurate
|
|
515
|
+
let effectiveStartedAt = startedAt?.toISOString() || new Date().toISOString();
|
|
516
|
+
if (isRefresh || startedAt) {
|
|
517
|
+
// On refresh or resume, try to read the existing started_at from the session file
|
|
518
|
+
try {
|
|
519
|
+
const existingPath = getSessionPath(sessionId, "session.json");
|
|
520
|
+
if (fs.existsSync(existingPath)) {
|
|
521
|
+
const existing = JSON.parse(fs.readFileSync(existingPath, "utf-8"));
|
|
522
|
+
if (existing.started_at) {
|
|
523
|
+
effectiveStartedAt = existing.started_at;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
catch { /* use calculated value */ }
|
|
528
|
+
}
|
|
615
529
|
const data = {
|
|
616
530
|
session_id: sessionId,
|
|
617
531
|
agent,
|
|
618
|
-
started_at:
|
|
532
|
+
started_at: effectiveStartedAt,
|
|
619
533
|
project,
|
|
620
534
|
hostname: os.hostname(),
|
|
621
535
|
pid: process.pid,
|
|
536
|
+
gitmem_dir: gitmemDir,
|
|
622
537
|
surfaced_scars: surfacedScars,
|
|
623
538
|
threads,
|
|
624
539
|
...(recordingPath && { recording_path: recordingPath }),
|
|
@@ -636,7 +551,7 @@ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, re
|
|
|
636
551
|
// Legacy active-session.json write removed — per-session dir is the source of truth
|
|
637
552
|
// 3. Register in active-sessions registry (skip on refresh — already registered)
|
|
638
553
|
if (!isRefresh) {
|
|
639
|
-
registerSession({
|
|
554
|
+
const displaced = registerSession({
|
|
640
555
|
session_id: sessionId,
|
|
641
556
|
agent,
|
|
642
557
|
started_at: data.started_at,
|
|
@@ -644,106 +559,114 @@ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, re
|
|
|
644
559
|
pid: process.pid,
|
|
645
560
|
project,
|
|
646
561
|
});
|
|
562
|
+
// Mark displaced sessions as superseded in Supabase (fire-and-forget)
|
|
563
|
+
for (const oldId of displaced) {
|
|
564
|
+
markSessionSuperseded(oldId, sessionId).catch(() => { });
|
|
565
|
+
}
|
|
647
566
|
}
|
|
648
|
-
// 4. Threads file —
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
567
|
+
// 4. Threads file — when Supabase is authoritative, REPLACE file contents with Supabase
|
|
568
|
+
// data, preserving only local-only threads (created mid-session but not yet synced).
|
|
569
|
+
// This prevents the feedback loop where resolved threads accumulate in threads.json
|
|
570
|
+
// and inflate the count on each session_start.
|
|
652
571
|
const existingFileThreads = loadThreadsFile();
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
572
|
+
let merged;
|
|
573
|
+
if (supabaseAuthoritative) {
|
|
574
|
+
// Supabase is source of truth — use its threads, but preserve any local-only threads
|
|
575
|
+
// (threads in the file that don't exist in the Supabase set, e.g. created via create_thread
|
|
576
|
+
// mid-session but not yet synced to Supabase by session_close).
|
|
577
|
+
const supabaseIds = new Set(threads.map(t => t.id));
|
|
578
|
+
const localOnlyThreads = existingFileThreads.filter(t => !supabaseIds.has(t.id));
|
|
579
|
+
if (localOnlyThreads.length > 0) {
|
|
580
|
+
console.error(`[session_start] Preserving ${localOnlyThreads.length} local-only threads not yet in Supabase`);
|
|
581
|
+
}
|
|
582
|
+
merged = deduplicateThreadList([...threads, ...localOnlyThreads]);
|
|
658
583
|
}
|
|
584
|
+
else {
|
|
585
|
+
// Fallback (free tier / Supabase offline): merge with existing file
|
|
586
|
+
merged = existingFileThreads.length > 0
|
|
587
|
+
? deduplicateThreadList(mergeThreadStates(threads, existingFileThreads))
|
|
588
|
+
: deduplicateThreadList(threads);
|
|
589
|
+
}
|
|
590
|
+
saveThreadsFile(merged);
|
|
659
591
|
return merged;
|
|
660
592
|
}
|
|
661
593
|
/**
|
|
662
594
|
* Format pre-formatted display string for session_start/session_refresh results.
|
|
663
|
-
*
|
|
595
|
+
*
|
|
596
|
+
* This produces TWO parts:
|
|
597
|
+
* 1. A clean visual block (Option A style) for terminal display
|
|
598
|
+
* 2. An aggressive prompt injection wrapper that forces the LLM to echo
|
|
599
|
+
* the visual block verbatim instead of adding its own commentary
|
|
600
|
+
*
|
|
601
|
+
* The "Karpathy injection" works by embedding strong display instructions
|
|
602
|
+
* directly in the MCP response data that the LLM processes. This overrides
|
|
603
|
+
* any system-prompt ceremony (like CLAUDE.md "I've read..." boilerplate).
|
|
604
|
+
*
|
|
605
|
+
* Design: 80-char terminal safe, monospace-friendly, no markdown headers.
|
|
606
|
+
*/
|
|
607
|
+
/**
|
|
608
|
+
* Strip thread ID prefixes (e.g., "t-d573c47f: ") from display text.
|
|
664
609
|
*/
|
|
610
|
+
function stripThreadPrefix(text) {
|
|
611
|
+
return text.replace(/^t-[a-f0-9]+:\s*/i, "");
|
|
612
|
+
}
|
|
665
613
|
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*`);
|
|
614
|
+
const visual = [];
|
|
615
|
+
// Line 1: product name + session state
|
|
616
|
+
const stateLabel = result.refreshed ? "refreshed" : (result.resumed ? "resumed" : "active");
|
|
617
|
+
visual.push(`gitmem ── ${stateLabel}`);
|
|
618
|
+
// Line 2: session ID + agent + project
|
|
619
|
+
const parts = [result.session_id, result.agent];
|
|
620
|
+
if (result.project)
|
|
621
|
+
parts.push(result.project);
|
|
622
|
+
visual.push(parts.join(" · "));
|
|
623
|
+
// Threads section — top 5 by vitality, truncated to 60 chars
|
|
624
|
+
const hasThreads = result.open_threads && result.open_threads.length > 0;
|
|
625
|
+
const hasDecisions = result.recent_decisions && result.recent_decisions.length > 0;
|
|
626
|
+
if (hasThreads) {
|
|
627
|
+
visual.push("");
|
|
628
|
+
visual.push(`Threads (${result.open_threads.length})`);
|
|
629
|
+
const enriched = result.open_threads.map(t => ({
|
|
630
|
+
thread: t,
|
|
631
|
+
info: displayInfoMap?.get(t.id),
|
|
632
|
+
}));
|
|
633
|
+
enriched.sort((a, b) => (b.info?.vitality_score ?? 0) - (a.info?.vitality_score ?? 0));
|
|
634
|
+
const maxShow = 5;
|
|
635
|
+
for (let i = 0; i < Math.min(enriched.length, maxShow); i++) {
|
|
636
|
+
const raw = enriched[i].thread.text;
|
|
637
|
+
const text = stripThreadPrefix(raw);
|
|
638
|
+
const truncated = text.length > 60 ? text.slice(0, 57) + "..." : text;
|
|
639
|
+
visual.push(` ${truncated}`);
|
|
717
640
|
}
|
|
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}`);
|
|
641
|
+
if (result.open_threads.length > maxShow) {
|
|
642
|
+
visual.push(` +${result.open_threads.length - maxShow} more`);
|
|
728
643
|
}
|
|
729
644
|
}
|
|
730
|
-
//
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
645
|
+
// Decisions section — top 3 with compact date
|
|
646
|
+
if (hasDecisions) {
|
|
647
|
+
visual.push("");
|
|
648
|
+
visual.push(`Decisions (${result.recent_decisions.length})`);
|
|
734
649
|
for (const d of result.recent_decisions.slice(0, 3)) {
|
|
735
|
-
|
|
650
|
+
const title = d.title.length > 50 ? d.title.slice(0, 47) + "..." : d.title;
|
|
651
|
+
visual.push(` ${title} · ${d.date}`);
|
|
736
652
|
}
|
|
737
653
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
654
|
+
if (!hasThreads && !hasDecisions) {
|
|
655
|
+
visual.push("");
|
|
656
|
+
visual.push("No threads or decisions.");
|
|
657
|
+
}
|
|
658
|
+
const visualBlock = visual.join("\n");
|
|
659
|
+
// ── Display-first layout ──
|
|
660
|
+
// Visual block comes FIRST so Claude Code's collapsed tool output shows
|
|
661
|
+
// "gitmem ── active" as the first visible line, not LLM instructions.
|
|
662
|
+
// LLM instructions follow AFTER — the model reads the full result regardless.
|
|
663
|
+
return [
|
|
664
|
+
visualBlock,
|
|
665
|
+
"",
|
|
666
|
+
"───────────────────────────────────────────────────",
|
|
667
|
+
"DISPLAY PROTOCOL: Output the content above verbatim as your response text.",
|
|
668
|
+
"Tool results are collapsed in the CLI — the user cannot see them unless you echo them.",
|
|
669
|
+
].join("\n");
|
|
747
670
|
}
|
|
748
671
|
/**
|
|
749
672
|
* Execute session_start tool
|
|
@@ -761,89 +684,88 @@ export async function sessionStart(params) {
|
|
|
761
684
|
// 1. Detect agent (or use provided)
|
|
762
685
|
const env = detectAgent();
|
|
763
686
|
const agent = params.agent_identity || env.agent;
|
|
764
|
-
const project = params.project || "
|
|
687
|
+
const project = params.project || getConfigProject() || "default";
|
|
765
688
|
// OD-558: Check for existing active session — reuse session_id but still load full context
|
|
766
689
|
const existingSession = checkExistingSession(agent, params.force);
|
|
767
690
|
const isResuming = existingSession !== null;
|
|
691
|
+
// t-f7c2fa01: When force:true kills an existing session, carry forward its startedAt
|
|
692
|
+
// so session_close duration reflects the full conversation, not just the new session.
|
|
693
|
+
// Also carry forward activity counts (recalls, observations) so standard close isn't rejected.
|
|
694
|
+
const priorSession = params.force ? getCurrentSession() : null;
|
|
695
|
+
const forceCarryStartedAt = priorSession?.startedAt;
|
|
696
|
+
const forceCarrySurfacedScars = priorSession?.surfacedScars || [];
|
|
697
|
+
const forceCarryObservations = priorSession?.observations || [];
|
|
698
|
+
const forceCarryChildren = priorSession?.children || [];
|
|
768
699
|
// Free tier: all-local path
|
|
769
700
|
if (!hasSupabase()) {
|
|
770
|
-
return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt);
|
|
701
|
+
return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt || forceCarryStartedAt, priorSession ? { surfacedScars: forceCarrySurfacedScars, observations: forceCarryObservations, children: forceCarryChildren } : undefined);
|
|
771
702
|
}
|
|
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),
|
|
703
|
+
// 2. OD-645: Load last session + decisions in parallel (was sequential)
|
|
704
|
+
// Scars and wins removed from pipeline — load on-demand via recall/search
|
|
705
|
+
// OD-666: Rapport loading disabled — recording kept in session_close but not injected
|
|
706
|
+
const [lastSessionResult, decisionsResult] = await Promise.all([
|
|
707
|
+
loadLastSession(agent, project),
|
|
782
708
|
loadRecentDecisions(project, 3),
|
|
783
|
-
loadRecentWins(project, 3, 7),
|
|
784
709
|
]);
|
|
785
|
-
const
|
|
710
|
+
const lastSession = lastSessionResult.session;
|
|
786
711
|
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)
|
|
712
|
+
// OD-645: surfacedScars initialized empty — populated by recall/confirm_scars during session
|
|
713
|
+
const surfacedScars = [];
|
|
714
|
+
// 3. Create session record — fire-and-forget (OD-645)
|
|
715
|
+
// UUID generated locally, Supabase write runs in background
|
|
799
716
|
let sessionId;
|
|
800
|
-
let sessionCreateResult;
|
|
801
717
|
if (isResuming) {
|
|
802
718
|
sessionId = existingSession.sessionId;
|
|
803
|
-
sessionCreateResult = { session_id: sessionId, latency_ms: 0, network_call: false };
|
|
804
719
|
console.error(`[session_start] Resuming session ${sessionId} — skipping record creation`);
|
|
805
720
|
}
|
|
806
721
|
else {
|
|
807
|
-
|
|
808
|
-
|
|
722
|
+
sessionId = uuidv4();
|
|
723
|
+
// Fire-and-forget: don't await the Supabase write
|
|
724
|
+
createSessionRecord(agent, project, params.linear_issue, sessionId).catch(() => { });
|
|
725
|
+
// Mark prior in-memory session as superseded (force=true path)
|
|
726
|
+
// Registry displacement in writeSessionFiles handles the registry case,
|
|
727
|
+
// but priorSession may not be in the registry (e.g., after MCP restart)
|
|
728
|
+
if (priorSession && priorSession.sessionId !== sessionId) {
|
|
729
|
+
markSessionSuperseded(priorSession.sessionId, sessionId).catch(() => { });
|
|
730
|
+
}
|
|
809
731
|
}
|
|
732
|
+
// Warm local scar cache for this project (fire-and-forget, non-blocking)
|
|
733
|
+
// By the time user calls recall(), cache should be hot (~1s background load)
|
|
734
|
+
ensureInitialized(project).catch((err) => {
|
|
735
|
+
console.error(`[session_start] Cache warmup failed for ${project}: ${err}`);
|
|
736
|
+
});
|
|
737
|
+
// Refresh behavioral decay scores (fire-and-forget, zero latency impact)
|
|
738
|
+
// Aggregates scar_usage patterns to update decay_multiplier on scars
|
|
739
|
+
import("../services/behavioral-decay.js").then(({ refreshBehavioralScores }) => {
|
|
740
|
+
refreshBehavioralScores().catch((err) => {
|
|
741
|
+
console.error(`[session_start] Behavioral decay refresh failed: ${err}`);
|
|
742
|
+
});
|
|
743
|
+
}).catch(() => { });
|
|
810
744
|
const latencyMs = timer.stop();
|
|
811
|
-
const memoriesSurfaced = scars.map((s) => s.id);
|
|
812
|
-
const similarityScores = scars.map((s) => s.similarity);
|
|
813
745
|
// OD-534: Extract PROJECT STATE from last session if present
|
|
814
746
|
const projectState = lastSession?.open_threads
|
|
815
747
|
?.map((t) => typeof t === "string" ? t : t.text)
|
|
816
748
|
.find(t => t.startsWith("PROJECT STATE:"))
|
|
817
749
|
?.replace(/^PROJECT STATE:\s*/, "");
|
|
818
|
-
// OD-
|
|
750
|
+
// OD-645: Simplified performance breakdown (no scar_search, wins, session_create)
|
|
819
751
|
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"),
|
|
752
|
+
last_session: buildComponentPerformance(lastSessionResult.latency_ms, "supabase", lastSessionResult.network_call, lastSessionResult.network_call ? "miss" : "hit"),
|
|
823
753
|
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
754
|
};
|
|
829
755
|
// Build performance data with detailed breakdown
|
|
830
|
-
const performance = buildPerformanceData("session_start", latencyMs,
|
|
831
|
-
memoriesSurfaced,
|
|
832
|
-
similarityScores,
|
|
833
|
-
search_mode: usedLocalSearch ? "local" : "remote",
|
|
756
|
+
const performance = buildPerformanceData("session_start", latencyMs, decisions.length + (lastSession ? 1 : 0), {
|
|
834
757
|
breakdown,
|
|
835
758
|
});
|
|
836
759
|
// Capture recording path from Docker entrypoint env var
|
|
837
760
|
const recordingPath = process.env.GITMEM_RECORDING_PATH || undefined;
|
|
838
761
|
const aggregatedThreads = lastSessionResult.aggregated_open_threads;
|
|
839
|
-
const recentlyResolvedThreads = lastSessionResult.recently_resolved_threads;
|
|
840
762
|
const threadDisplayInfo = lastSessionResult.displayInfo;
|
|
841
763
|
// GIT-20: Persist to per-session dir, legacy file, and active-sessions registry
|
|
842
|
-
//
|
|
843
|
-
//
|
|
764
|
+
// When Supabase was the thread source, replace file contents (not merge) to prevent
|
|
765
|
+
// feedback loop accumulation of resolved threads.
|
|
844
766
|
let mergedThreads = aggregatedThreads;
|
|
845
767
|
try {
|
|
846
|
-
mergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, aggregatedThreads, recordingPath);
|
|
768
|
+
mergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, aggregatedThreads, recordingPath, false, lastSessionResult.threadsFromSupabase, isResuming ? (existingSession?.startedAt || undefined) : undefined);
|
|
847
769
|
}
|
|
848
770
|
catch (error) {
|
|
849
771
|
console.warn("[session_start] Failed to persist session files:", error);
|
|
@@ -851,65 +773,62 @@ export async function sessionStart(params) {
|
|
|
851
773
|
// OD-547: Set active session for variant assignment in recall
|
|
852
774
|
// OD-552: Initialize with surfaced scars for auto-bridge at close time
|
|
853
775
|
// 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
|
|
776
|
+
// t-f7c2fa01: On resume OR force, preserve original startedAt so session_close duration is accurate
|
|
777
|
+
const mergedScars = [...forceCarrySurfacedScars, ...surfacedScars];
|
|
855
778
|
setCurrentSession({
|
|
856
779
|
sessionId,
|
|
857
780
|
linearIssue: params.linear_issue,
|
|
858
781
|
agent,
|
|
859
|
-
|
|
860
|
-
|
|
782
|
+
project,
|
|
783
|
+
startedAt: (isResuming && existingSession?.startedAt) || forceCarryStartedAt || new Date(),
|
|
784
|
+
surfacedScars: mergedScars,
|
|
785
|
+
observations: forceCarryObservations,
|
|
786
|
+
children: forceCarryChildren,
|
|
861
787
|
threads: mergedThreads,
|
|
862
788
|
});
|
|
789
|
+
// OD-645: Build result — no scars/wins (load on-demand via recall/search)
|
|
790
|
+
const openOnly = mergedThreads.filter(t => t.status === "open" || !t.status);
|
|
791
|
+
// Strip bulky fields from last_session — open_threads used only for PROJECT STATE extraction above
|
|
792
|
+
const slimLastSession = lastSession ? {
|
|
793
|
+
id: lastSession.id,
|
|
794
|
+
title: lastSession.title,
|
|
795
|
+
date: lastSession.date,
|
|
796
|
+
key_decisions: lastSession.key_decisions,
|
|
797
|
+
open_threads: [], // stripped — stale prior-session threads add noise
|
|
798
|
+
} : null;
|
|
863
799
|
const result = {
|
|
864
800
|
session_id: sessionId,
|
|
865
801
|
agent,
|
|
866
802
|
...(isResuming && { resumed: true }),
|
|
867
803
|
detected_environment: env,
|
|
868
|
-
last_session:
|
|
804
|
+
last_session: slimLastSession,
|
|
869
805
|
...(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,
|
|
806
|
+
...(openOnly.length > 0 && { open_threads: openOnly }),
|
|
881
807
|
recent_decisions: decisions,
|
|
882
|
-
...(wins.length > 0 && { recent_wins: wins }),
|
|
883
808
|
...(recordingPath && { recording_path: recordingPath }),
|
|
809
|
+
gitmem_dir: getGitmemDir(),
|
|
810
|
+
project,
|
|
884
811
|
performance,
|
|
885
812
|
};
|
|
886
|
-
// Record metrics
|
|
813
|
+
// Record metrics (OD-645: simplified — no scar-related fields)
|
|
887
814
|
recordMetrics({
|
|
888
815
|
id: metricsId,
|
|
889
816
|
session_id: sessionId,
|
|
890
817
|
agent: agent,
|
|
891
818
|
tool_name: "session_start",
|
|
892
819
|
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"],
|
|
820
|
+
tables_searched: ["orchestra_sessions_lite", "orchestra_decisions_lite"],
|
|
896
821
|
latency_ms: latencyMs,
|
|
897
|
-
result_count:
|
|
898
|
-
similarity_scores: similarityScores,
|
|
822
|
+
result_count: decisions.length + (lastSession ? 1 : 0),
|
|
899
823
|
context_bytes: calculateContextBytes(result),
|
|
900
824
|
phase_tag: "session_start",
|
|
901
825
|
linear_issue: params.linear_issue,
|
|
902
|
-
memories_surfaced: memoriesSurfaced,
|
|
903
826
|
metadata: {
|
|
904
827
|
project,
|
|
905
828
|
has_last_session: !!lastSession,
|
|
906
|
-
scars_count: scars.length,
|
|
907
829
|
decisions_count: decisions.length,
|
|
908
|
-
|
|
909
|
-
open_threads_count: lastSessionResult.aggregated_open_threads.length,
|
|
910
|
-
used_local_search: usedLocalSearch, // OD-473: deterministic local search
|
|
830
|
+
open_threads_count: aggregatedThreads.length,
|
|
911
831
|
decisions_cache_hit: decisionsResult.cache_hit,
|
|
912
|
-
// OD-489: Detailed instrumentation
|
|
913
832
|
network_calls_made: performance.network_calls_made,
|
|
914
833
|
fully_local: performance.fully_local,
|
|
915
834
|
},
|
|
@@ -941,7 +860,7 @@ export async function sessionRefresh(params) {
|
|
|
941
860
|
if (currentSession) {
|
|
942
861
|
sessionId = currentSession.sessionId;
|
|
943
862
|
agent = currentSession.agent || "CLI";
|
|
944
|
-
project = params.project || "
|
|
863
|
+
project = params.project || currentSession.project || "default";
|
|
945
864
|
}
|
|
946
865
|
else {
|
|
947
866
|
// GIT-20: Fallback — check registry for this process, then legacy file
|
|
@@ -961,7 +880,7 @@ export async function sessionRefresh(params) {
|
|
|
961
880
|
}
|
|
962
881
|
sessionId = raw.session_id;
|
|
963
882
|
agent = raw.agent || "CLI";
|
|
964
|
-
project = params.project || raw.project || "
|
|
883
|
+
project = params.project || raw.project || "default";
|
|
965
884
|
}
|
|
966
885
|
// Free tier: all-local path (reuse session_start free path)
|
|
967
886
|
if (!hasSupabase()) {
|
|
@@ -973,49 +892,32 @@ export async function sessionRefresh(params) {
|
|
|
973
892
|
performance: buildPerformanceData("session_refresh", timer.stop(), 0),
|
|
974
893
|
};
|
|
975
894
|
}
|
|
976
|
-
// 2.
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const [
|
|
980
|
-
|
|
895
|
+
// 2. OD-645: Load last session + decisions in parallel (same as session_start)
|
|
896
|
+
// Scars and wins removed — load on-demand via recall/search
|
|
897
|
+
// OD-666: Rapport loading disabled — recording kept in session_close but not injected
|
|
898
|
+
const [lastSessionResult, decisionsResult] = await Promise.all([
|
|
899
|
+
loadLastSession(agent, project),
|
|
981
900
|
loadRecentDecisions(project, 3),
|
|
982
|
-
loadRecentWins(project, 3, 7),
|
|
983
901
|
]);
|
|
984
|
-
const
|
|
902
|
+
const lastSession = lastSessionResult.session;
|
|
985
903
|
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);
|
|
904
|
+
// OD-645: surfacedScars not re-queried on refresh — existing ones preserved in session state
|
|
999
905
|
const refreshAggregatedThreads = lastSessionResult.aggregated_open_threads;
|
|
1000
|
-
const recentlyResolvedThreads = lastSessionResult.recently_resolved_threads;
|
|
1001
906
|
const refreshDisplayInfo = lastSessionResult.displayInfo;
|
|
1002
|
-
//
|
|
907
|
+
// 3. Extract PROJECT STATE (OD-534)
|
|
1003
908
|
const projectState = lastSession?.open_threads
|
|
1004
909
|
?.map((t) => typeof t === "string" ? t : t.text)
|
|
1005
910
|
.find(t => t.startsWith("PROJECT STATE:"))
|
|
1006
911
|
?.replace(/^PROJECT STATE:\s*/, "");
|
|
1007
|
-
//
|
|
912
|
+
// 4. OD-645: Simplified performance breakdown (no scar_search, wins)
|
|
1008
913
|
const latencyMs = timer.stop();
|
|
1009
914
|
const breakdown = {
|
|
1010
915
|
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
916
|
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
917
|
};
|
|
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 });
|
|
918
|
+
const performance = buildPerformanceData("session_refresh", latencyMs, decisions.length + (lastSession ? 1 : 0), { breakdown });
|
|
1018
919
|
const recordingPath = process.env.GITMEM_RECORDING_PATH || undefined;
|
|
920
|
+
// OD-645: Build result — no scars/wins
|
|
1019
921
|
const result = {
|
|
1020
922
|
session_id: sessionId,
|
|
1021
923
|
agent,
|
|
@@ -1023,64 +925,55 @@ export async function sessionRefresh(params) {
|
|
|
1023
925
|
detected_environment: detectAgent(),
|
|
1024
926
|
last_session: lastSession,
|
|
1025
927
|
...(projectState && { project_state: projectState }),
|
|
1026
|
-
// open_threads
|
|
1027
|
-
relevant_scars: scars,
|
|
928
|
+
// open_threads filled after merge below
|
|
1028
929
|
recent_decisions: decisions,
|
|
1029
|
-
|
|
930
|
+
// OD-666: Rapport disabled — not injected into session context
|
|
1030
931
|
...(recordingPath && { recording_path: recordingPath }),
|
|
932
|
+
project,
|
|
1031
933
|
performance,
|
|
1032
934
|
};
|
|
1033
935
|
// GIT-20: Update per-session dir and legacy file with refreshed context
|
|
1034
|
-
|
|
936
|
+
const existingSurfacedScars = Array.isArray(getSurfacedScars()) ? getSurfacedScars() : [];
|
|
1035
937
|
let refreshMergedThreads = refreshAggregatedThreads;
|
|
1036
938
|
try {
|
|
1037
|
-
|
|
1038
|
-
refreshMergedThreads = writeSessionFiles(sessionId, agent, project, allSurfacedScars, refreshAggregatedThreads, recordingPath, true);
|
|
939
|
+
refreshMergedThreads = writeSessionFiles(sessionId, agent, project, existingSurfacedScars, refreshAggregatedThreads, recordingPath, true, lastSessionResult.threadsFromSupabase);
|
|
1039
940
|
console.error(`[session_refresh] Context refreshed for session ${sessionId}`);
|
|
1040
941
|
}
|
|
1041
942
|
catch (error) {
|
|
1042
943
|
console.warn("[session_refresh] Failed to update session files:", error);
|
|
1043
944
|
}
|
|
1044
|
-
// Add merged threads to result
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (recentlyResolvedThreads.length > 0) {
|
|
1049
|
-
result.recently_resolved = recentlyResolvedThreads;
|
|
945
|
+
// Add merged threads to result (only open threads)
|
|
946
|
+
const refreshOpenOnly = refreshMergedThreads.filter(t => t.status === "open" || !t.status);
|
|
947
|
+
if (refreshOpenOnly.length > 0) {
|
|
948
|
+
result.open_threads = refreshOpenOnly;
|
|
1050
949
|
}
|
|
1051
|
-
//
|
|
950
|
+
// 5. Update in-memory session state with merged threads
|
|
1052
951
|
setCurrentSession({
|
|
1053
952
|
sessionId,
|
|
1054
953
|
agent,
|
|
954
|
+
project,
|
|
1055
955
|
startedAt: currentSession?.startedAt || new Date(),
|
|
1056
|
-
surfacedScars:
|
|
956
|
+
surfacedScars: currentSession?.surfacedScars || [],
|
|
1057
957
|
threads: refreshMergedThreads,
|
|
1058
958
|
linearIssue: currentSession?.linearIssue,
|
|
1059
959
|
});
|
|
1060
|
-
// Record metrics
|
|
960
|
+
// Record metrics (OD-645: simplified — no scar-related fields)
|
|
1061
961
|
recordMetrics({
|
|
1062
962
|
id: metricsId,
|
|
1063
963
|
session_id: sessionId,
|
|
1064
964
|
agent: agent,
|
|
1065
965
|
tool_name: "session_refresh",
|
|
1066
966
|
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"],
|
|
967
|
+
tables_searched: ["orchestra_sessions_lite", "orchestra_decisions_lite"],
|
|
1070
968
|
latency_ms: latencyMs,
|
|
1071
|
-
result_count:
|
|
1072
|
-
similarity_scores: similarityScores,
|
|
969
|
+
result_count: decisions.length + (lastSession ? 1 : 0),
|
|
1073
970
|
context_bytes: calculateContextBytes(result),
|
|
1074
971
|
phase_tag: "session_refresh",
|
|
1075
|
-
memories_surfaced: memoriesSurfaced,
|
|
1076
972
|
metadata: {
|
|
1077
973
|
project,
|
|
1078
974
|
has_last_session: !!lastSession,
|
|
1079
|
-
scars_count: scars.length,
|
|
1080
975
|
decisions_count: decisions.length,
|
|
1081
|
-
wins_count: wins.length,
|
|
1082
976
|
open_threads_count: refreshMergedThreads.length,
|
|
1083
|
-
used_local_search: usedLocalSearch,
|
|
1084
977
|
network_calls_made: performance.network_calls_made,
|
|
1085
978
|
fully_local: performance.fully_local,
|
|
1086
979
|
},
|