gitmem-mcp 0.2.0

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.
Files changed (316) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/CLAUDE.md.template +65 -0
  3. package/LICENSE +21 -0
  4. package/README.md +221 -0
  5. package/bin/gitmem.js +383 -0
  6. package/dist/commands/check.d.ts +33 -0
  7. package/dist/commands/check.d.ts.map +1 -0
  8. package/dist/commands/check.js +492 -0
  9. package/dist/commands/check.js.map +1 -0
  10. package/dist/constants/closing-questions.d.ts +40 -0
  11. package/dist/constants/closing-questions.d.ts.map +1 -0
  12. package/dist/constants/closing-questions.js +107 -0
  13. package/dist/constants/closing-questions.js.map +1 -0
  14. package/dist/diagnostics/anonymizer.d.ts +55 -0
  15. package/dist/diagnostics/anonymizer.d.ts.map +1 -0
  16. package/dist/diagnostics/anonymizer.js +191 -0
  17. package/dist/diagnostics/anonymizer.js.map +1 -0
  18. package/dist/diagnostics/channels.d.ts +132 -0
  19. package/dist/diagnostics/channels.d.ts.map +1 -0
  20. package/dist/diagnostics/channels.js +150 -0
  21. package/dist/diagnostics/channels.js.map +1 -0
  22. package/dist/diagnostics/collector.d.ts +183 -0
  23. package/dist/diagnostics/collector.d.ts.map +1 -0
  24. package/dist/diagnostics/collector.js +227 -0
  25. package/dist/diagnostics/collector.js.map +1 -0
  26. package/dist/diagnostics/index.d.ts +28 -0
  27. package/dist/diagnostics/index.d.ts.map +1 -0
  28. package/dist/diagnostics/index.js +31 -0
  29. package/dist/diagnostics/index.js.map +1 -0
  30. package/dist/index.d.ts +13 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/schemas/absorb-observations.d.ts +63 -0
  35. package/dist/schemas/absorb-observations.d.ts.map +1 -0
  36. package/dist/schemas/absorb-observations.js +25 -0
  37. package/dist/schemas/absorb-observations.js.map +1 -0
  38. package/dist/schemas/active-sessions.d.ts +71 -0
  39. package/dist/schemas/active-sessions.d.ts.map +1 -0
  40. package/dist/schemas/active-sessions.js +19 -0
  41. package/dist/schemas/active-sessions.js.map +1 -0
  42. package/dist/schemas/analyze.d.ts +38 -0
  43. package/dist/schemas/analyze.d.ts.map +1 -0
  44. package/dist/schemas/analyze.js +30 -0
  45. package/dist/schemas/analyze.js.map +1 -0
  46. package/dist/schemas/common.d.ts +55 -0
  47. package/dist/schemas/common.d.ts.map +1 -0
  48. package/dist/schemas/common.js +65 -0
  49. package/dist/schemas/common.js.map +1 -0
  50. package/dist/schemas/create-decision.d.ts +48 -0
  51. package/dist/schemas/create-decision.d.ts.map +1 -0
  52. package/dist/schemas/create-decision.js +31 -0
  53. package/dist/schemas/create-decision.js.map +1 -0
  54. package/dist/schemas/create-learning.d.ts +107 -0
  55. package/dist/schemas/create-learning.d.ts.map +1 -0
  56. package/dist/schemas/create-learning.js +64 -0
  57. package/dist/schemas/create-learning.js.map +1 -0
  58. package/dist/schemas/get-transcript.d.ts +24 -0
  59. package/dist/schemas/get-transcript.d.ts.map +1 -0
  60. package/dist/schemas/get-transcript.js +22 -0
  61. package/dist/schemas/get-transcript.js.map +1 -0
  62. package/dist/schemas/index.d.ts +23 -0
  63. package/dist/schemas/index.d.ts.map +1 -0
  64. package/dist/schemas/index.js +23 -0
  65. package/dist/schemas/index.js.map +1 -0
  66. package/dist/schemas/log.d.ts +36 -0
  67. package/dist/schemas/log.d.ts.map +1 -0
  68. package/dist/schemas/log.js +27 -0
  69. package/dist/schemas/log.js.map +1 -0
  70. package/dist/schemas/prepare-context.d.ts +41 -0
  71. package/dist/schemas/prepare-context.d.ts.map +1 -0
  72. package/dist/schemas/prepare-context.js +31 -0
  73. package/dist/schemas/prepare-context.js.map +1 -0
  74. package/dist/schemas/recall.d.ts +41 -0
  75. package/dist/schemas/recall.d.ts.map +1 -0
  76. package/dist/schemas/recall.js +47 -0
  77. package/dist/schemas/recall.js.map +1 -0
  78. package/dist/schemas/record-scar-usage-batch.d.ts +82 -0
  79. package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -0
  80. package/dist/schemas/record-scar-usage-batch.js +25 -0
  81. package/dist/schemas/record-scar-usage-batch.js.map +1 -0
  82. package/dist/schemas/record-scar-usage.d.ts +51 -0
  83. package/dist/schemas/record-scar-usage.d.ts.map +1 -0
  84. package/dist/schemas/record-scar-usage.js +32 -0
  85. package/dist/schemas/record-scar-usage.js.map +1 -0
  86. package/dist/schemas/save-transcript.d.ts +38 -0
  87. package/dist/schemas/save-transcript.d.ts.map +1 -0
  88. package/dist/schemas/save-transcript.js +30 -0
  89. package/dist/schemas/save-transcript.js.map +1 -0
  90. package/dist/schemas/search.d.ts +36 -0
  91. package/dist/schemas/search.d.ts.map +1 -0
  92. package/dist/schemas/search.js +27 -0
  93. package/dist/schemas/search.js.map +1 -0
  94. package/dist/schemas/session-close.d.ts +371 -0
  95. package/dist/schemas/session-close.d.ts.map +1 -0
  96. package/dist/schemas/session-close.js +95 -0
  97. package/dist/schemas/session-close.js.map +1 -0
  98. package/dist/schemas/session-start.d.ts +46 -0
  99. package/dist/schemas/session-start.d.ts.map +1 -0
  100. package/dist/schemas/session-start.js +33 -0
  101. package/dist/schemas/session-start.js.map +1 -0
  102. package/dist/schemas/thread.d.ts +72 -0
  103. package/dist/schemas/thread.d.ts.map +1 -0
  104. package/dist/schemas/thread.js +39 -0
  105. package/dist/schemas/thread.js.map +1 -0
  106. package/dist/server.d.ts +22 -0
  107. package/dist/server.d.ts.map +1 -0
  108. package/dist/server.js +313 -0
  109. package/dist/server.js.map +1 -0
  110. package/dist/services/active-sessions.d.ts +66 -0
  111. package/dist/services/active-sessions.d.ts.map +1 -0
  112. package/dist/services/active-sessions.js +311 -0
  113. package/dist/services/active-sessions.js.map +1 -0
  114. package/dist/services/agent-detection.d.ts +25 -0
  115. package/dist/services/agent-detection.d.ts.map +1 -0
  116. package/dist/services/agent-detection.js +93 -0
  117. package/dist/services/agent-detection.js.map +1 -0
  118. package/dist/services/analytics.d.ts +201 -0
  119. package/dist/services/analytics.d.ts.map +1 -0
  120. package/dist/services/analytics.js +483 -0
  121. package/dist/services/analytics.js.map +1 -0
  122. package/dist/services/cache.d.ts +148 -0
  123. package/dist/services/cache.d.ts.map +1 -0
  124. package/dist/services/cache.js +384 -0
  125. package/dist/services/cache.js.map +1 -0
  126. package/dist/services/cache.test.d.ts +8 -0
  127. package/dist/services/cache.test.d.ts.map +1 -0
  128. package/dist/services/cache.test.js +267 -0
  129. package/dist/services/cache.test.js.map +1 -0
  130. package/dist/services/compliance-validator.d.ts +30 -0
  131. package/dist/services/compliance-validator.d.ts.map +1 -0
  132. package/dist/services/compliance-validator.js +257 -0
  133. package/dist/services/compliance-validator.js.map +1 -0
  134. package/dist/services/config.d.ts +48 -0
  135. package/dist/services/config.d.ts.map +1 -0
  136. package/dist/services/config.js +128 -0
  137. package/dist/services/config.js.map +1 -0
  138. package/dist/services/embedding.d.ts +58 -0
  139. package/dist/services/embedding.d.ts.map +1 -0
  140. package/dist/services/embedding.js +243 -0
  141. package/dist/services/embedding.js.map +1 -0
  142. package/dist/services/gitmem-dir.d.ts +38 -0
  143. package/dist/services/gitmem-dir.d.ts.map +1 -0
  144. package/dist/services/gitmem-dir.js +84 -0
  145. package/dist/services/gitmem-dir.js.map +1 -0
  146. package/dist/services/local-file-storage.d.ts +56 -0
  147. package/dist/services/local-file-storage.d.ts.map +1 -0
  148. package/dist/services/local-file-storage.js +213 -0
  149. package/dist/services/local-file-storage.js.map +1 -0
  150. package/dist/services/local-vector-search.d.ts +137 -0
  151. package/dist/services/local-vector-search.d.ts.map +1 -0
  152. package/dist/services/local-vector-search.js +311 -0
  153. package/dist/services/local-vector-search.js.map +1 -0
  154. package/dist/services/metrics.d.ts +104 -0
  155. package/dist/services/metrics.d.ts.map +1 -0
  156. package/dist/services/metrics.js +264 -0
  157. package/dist/services/metrics.js.map +1 -0
  158. package/dist/services/session-state.d.ts +113 -0
  159. package/dist/services/session-state.d.ts.map +1 -0
  160. package/dist/services/session-state.js +203 -0
  161. package/dist/services/session-state.js.map +1 -0
  162. package/dist/services/startup.d.ts +112 -0
  163. package/dist/services/startup.d.ts.map +1 -0
  164. package/dist/services/startup.js +436 -0
  165. package/dist/services/startup.js.map +1 -0
  166. package/dist/services/storage.d.ts +43 -0
  167. package/dist/services/storage.d.ts.map +1 -0
  168. package/dist/services/storage.js +92 -0
  169. package/dist/services/storage.js.map +1 -0
  170. package/dist/services/supabase-client.d.ts +163 -0
  171. package/dist/services/supabase-client.d.ts.map +1 -0
  172. package/dist/services/supabase-client.js +510 -0
  173. package/dist/services/supabase-client.js.map +1 -0
  174. package/dist/services/thread-dedup.d.ts +44 -0
  175. package/dist/services/thread-dedup.d.ts.map +1 -0
  176. package/dist/services/thread-dedup.js +113 -0
  177. package/dist/services/thread-dedup.js.map +1 -0
  178. package/dist/services/thread-manager.d.ts +77 -0
  179. package/dist/services/thread-manager.d.ts.map +1 -0
  180. package/dist/services/thread-manager.js +250 -0
  181. package/dist/services/thread-manager.js.map +1 -0
  182. package/dist/services/thread-suggestions.d.ts +66 -0
  183. package/dist/services/thread-suggestions.d.ts.map +1 -0
  184. package/dist/services/thread-suggestions.js +243 -0
  185. package/dist/services/thread-suggestions.js.map +1 -0
  186. package/dist/services/thread-supabase.d.ts +111 -0
  187. package/dist/services/thread-supabase.d.ts.map +1 -0
  188. package/dist/services/thread-supabase.js +459 -0
  189. package/dist/services/thread-supabase.js.map +1 -0
  190. package/dist/services/thread-vitality.d.ts +65 -0
  191. package/dist/services/thread-vitality.d.ts.map +1 -0
  192. package/dist/services/thread-vitality.js +143 -0
  193. package/dist/services/thread-vitality.js.map +1 -0
  194. package/dist/services/tier.d.ts +52 -0
  195. package/dist/services/tier.d.ts.map +1 -0
  196. package/dist/services/tier.js +109 -0
  197. package/dist/services/tier.js.map +1 -0
  198. package/dist/services/timezone.d.ts +37 -0
  199. package/dist/services/timezone.d.ts.map +1 -0
  200. package/dist/services/timezone.js +147 -0
  201. package/dist/services/timezone.js.map +1 -0
  202. package/dist/services/transcript-chunker.d.ts +18 -0
  203. package/dist/services/transcript-chunker.d.ts.map +1 -0
  204. package/dist/services/transcript-chunker.js +237 -0
  205. package/dist/services/transcript-chunker.js.map +1 -0
  206. package/dist/services/triple-writer.d.ts +128 -0
  207. package/dist/services/triple-writer.d.ts.map +1 -0
  208. package/dist/services/triple-writer.js +338 -0
  209. package/dist/services/triple-writer.js.map +1 -0
  210. package/dist/services/variant-assignment.d.ts +92 -0
  211. package/dist/services/variant-assignment.d.ts.map +1 -0
  212. package/dist/services/variant-assignment.js +196 -0
  213. package/dist/services/variant-assignment.js.map +1 -0
  214. package/dist/tools/absorb-observations.d.ts +16 -0
  215. package/dist/tools/absorb-observations.d.ts.map +1 -0
  216. package/dist/tools/absorb-observations.js +82 -0
  217. package/dist/tools/absorb-observations.js.map +1 -0
  218. package/dist/tools/analyze.d.ts +55 -0
  219. package/dist/tools/analyze.d.ts.map +1 -0
  220. package/dist/tools/analyze.js +139 -0
  221. package/dist/tools/analyze.js.map +1 -0
  222. package/dist/tools/cleanup-threads.d.ts +47 -0
  223. package/dist/tools/cleanup-threads.d.ts.map +1 -0
  224. package/dist/tools/cleanup-threads.js +127 -0
  225. package/dist/tools/cleanup-threads.js.map +1 -0
  226. package/dist/tools/confirm-scars.d.ts +23 -0
  227. package/dist/tools/confirm-scars.d.ts.map +1 -0
  228. package/dist/tools/confirm-scars.js +209 -0
  229. package/dist/tools/confirm-scars.js.map +1 -0
  230. package/dist/tools/create-decision.d.ts +15 -0
  231. package/dist/tools/create-decision.d.ts.map +1 -0
  232. package/dist/tools/create-decision.js +138 -0
  233. package/dist/tools/create-decision.js.map +1 -0
  234. package/dist/tools/create-learning.d.ts +15 -0
  235. package/dist/tools/create-learning.d.ts.map +1 -0
  236. package/dist/tools/create-learning.js +226 -0
  237. package/dist/tools/create-learning.js.map +1 -0
  238. package/dist/tools/create-thread.d.ts +42 -0
  239. package/dist/tools/create-thread.d.ts.map +1 -0
  240. package/dist/tools/create-thread.js +180 -0
  241. package/dist/tools/create-thread.js.map +1 -0
  242. package/dist/tools/definitions.d.ts +5013 -0
  243. package/dist/tools/definitions.d.ts.map +1 -0
  244. package/dist/tools/definitions.js +2017 -0
  245. package/dist/tools/definitions.js.map +1 -0
  246. package/dist/tools/dismiss-suggestion.d.ts +20 -0
  247. package/dist/tools/dismiss-suggestion.d.ts.map +1 -0
  248. package/dist/tools/dismiss-suggestion.js +40 -0
  249. package/dist/tools/dismiss-suggestion.js.map +1 -0
  250. package/dist/tools/get-transcript.d.ts +24 -0
  251. package/dist/tools/get-transcript.d.ts.map +1 -0
  252. package/dist/tools/get-transcript.js +52 -0
  253. package/dist/tools/get-transcript.js.map +1 -0
  254. package/dist/tools/graph-traverse.d.ts +83 -0
  255. package/dist/tools/graph-traverse.d.ts.map +1 -0
  256. package/dist/tools/graph-traverse.js +394 -0
  257. package/dist/tools/graph-traverse.js.map +1 -0
  258. package/dist/tools/list-threads.d.ts +15 -0
  259. package/dist/tools/list-threads.d.ts.map +1 -0
  260. package/dist/tools/list-threads.js +114 -0
  261. package/dist/tools/list-threads.js.map +1 -0
  262. package/dist/tools/log.d.ts +43 -0
  263. package/dist/tools/log.d.ts.map +1 -0
  264. package/dist/tools/log.js +157 -0
  265. package/dist/tools/log.js.map +1 -0
  266. package/dist/tools/prepare-context.d.ts +36 -0
  267. package/dist/tools/prepare-context.d.ts.map +1 -0
  268. package/dist/tools/prepare-context.js +353 -0
  269. package/dist/tools/prepare-context.js.map +1 -0
  270. package/dist/tools/promote-suggestion.d.ts +25 -0
  271. package/dist/tools/promote-suggestion.d.ts.map +1 -0
  272. package/dist/tools/promote-suggestion.js +60 -0
  273. package/dist/tools/promote-suggestion.js.map +1 -0
  274. package/dist/tools/recall.d.ts +77 -0
  275. package/dist/tools/recall.d.ts.map +1 -0
  276. package/dist/tools/recall.js +423 -0
  277. package/dist/tools/recall.js.map +1 -0
  278. package/dist/tools/recall.test.d.ts +5 -0
  279. package/dist/tools/recall.test.d.ts.map +1 -0
  280. package/dist/tools/recall.test.js +155 -0
  281. package/dist/tools/recall.test.js.map +1 -0
  282. package/dist/tools/record-scar-usage-batch.d.ts +10 -0
  283. package/dist/tools/record-scar-usage-batch.d.ts.map +1 -0
  284. package/dist/tools/record-scar-usage-batch.js +153 -0
  285. package/dist/tools/record-scar-usage-batch.js.map +1 -0
  286. package/dist/tools/record-scar-usage.d.ts +14 -0
  287. package/dist/tools/record-scar-usage.d.ts.map +1 -0
  288. package/dist/tools/record-scar-usage.js +94 -0
  289. package/dist/tools/record-scar-usage.js.map +1 -0
  290. package/dist/tools/resolve-thread.d.ts +16 -0
  291. package/dist/tools/resolve-thread.d.ts.map +1 -0
  292. package/dist/tools/resolve-thread.js +102 -0
  293. package/dist/tools/resolve-thread.js.map +1 -0
  294. package/dist/tools/save-transcript.d.ts +29 -0
  295. package/dist/tools/save-transcript.d.ts.map +1 -0
  296. package/dist/tools/save-transcript.js +97 -0
  297. package/dist/tools/save-transcript.js.map +1 -0
  298. package/dist/tools/search.d.ts +46 -0
  299. package/dist/tools/search.d.ts.map +1 -0
  300. package/dist/tools/search.js +186 -0
  301. package/dist/tools/search.js.map +1 -0
  302. package/dist/tools/session-close.d.ts +14 -0
  303. package/dist/tools/session-close.d.ts.map +1 -0
  304. package/dist/tools/session-close.js +881 -0
  305. package/dist/tools/session-close.js.map +1 -0
  306. package/dist/tools/session-start.d.ts +38 -0
  307. package/dist/tools/session-start.d.ts.map +1 -0
  308. package/dist/tools/session-start.js +1104 -0
  309. package/dist/tools/session-start.js.map +1 -0
  310. package/dist/types/index.d.ts +456 -0
  311. package/dist/types/index.d.ts.map +1 -0
  312. package/dist/types/index.js +5 -0
  313. package/dist/types/index.js.map +1 -0
  314. package/package.json +76 -0
  315. package/schema/setup.sql +193 -0
  316. package/schema/starter-scars.json +206 -0
@@ -0,0 +1,881 @@
1
+ /**
2
+ * session_close Tool
3
+ *
4
+ * Persist session with compliance validation.
5
+ * Validates that required fields are present based on close type.
6
+ *
7
+ * Performance target: <3000ms (OD-429)
8
+ */
9
+ import { v4 as uuidv4 } from "uuid";
10
+ import { detectAgent } from "../services/agent-detection.js";
11
+ import * as supabase from "../services/supabase-client.js";
12
+ import { embed, isEmbeddingAvailable } from "../services/embedding.js";
13
+ import { hasSupabase } from "../services/tier.js";
14
+ import { getStorage } from "../services/storage.js";
15
+ import { clearCurrentSession, getSurfacedScars, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js"; // OD-547, OD-552, v2 Phase 2
16
+ import { normalizeThreads, mergeThreadStates, migrateStringThread } from "../services/thread-manager.js"; // OD-thread-lifecycle
17
+ import { syncThreadsToSupabase, loadOpenThreadEmbeddings } from "../services/thread-supabase.js"; // OD-624
18
+ import { validateSessionClose, buildCloseCompliance, } from "../services/compliance-validator.js";
19
+ import { Timer, recordMetrics, buildPerformanceData, updateRelevanceData, } from "../services/metrics.js";
20
+ import { recordScarUsageBatch } from "./record-scar-usage-batch.js";
21
+ import { saveTranscript } from "./save-transcript.js";
22
+ import { processTranscript } from "../services/transcript-chunker.js";
23
+ import * as fs from "fs";
24
+ import * as path from "path";
25
+ import * as os from "os";
26
+ import { getGitmemPath, getGitmemDir, getSessionPath } from "../services/gitmem-dir.js";
27
+ import { unregisterSession, findSessionByHostPid } from "../services/active-sessions.js";
28
+ import { loadSuggestions, saveSuggestions, detectSuggestedThreads, loadRecentSessionEmbeddings } from "../services/thread-suggestions.js";
29
+ /**
30
+ * Find the most recently modified transcript file in Claude Code projects directory
31
+ * OD-538: Search by recency, not by filename matching (supports post-compaction)
32
+ */
33
+ function findMostRecentTranscript(projectsDir, cwdBasename, cwdFull) {
34
+ // Claude Code names project dirs by replacing / with - in the full CWD path
35
+ // e.g., /Users/chriscrawford/nTEG-Labs -> -Users-chriscrawford-nTEG-Labs
36
+ const claudeCodeDirName = cwdFull.replace(/\//g, "-");
37
+ const possibleDirs = [
38
+ path.join(projectsDir, claudeCodeDirName), // Primary: full path with dashes (e.g., -Users-chriscrawford-nTEG-Labs)
39
+ path.join(projectsDir, "-workspace"),
40
+ path.join(projectsDir, "workspace"),
41
+ path.join(projectsDir, cwdBasename), // Legacy fallback
42
+ ];
43
+ let allTranscripts = [];
44
+ for (const dir of possibleDirs) {
45
+ if (!fs.existsSync(dir))
46
+ continue;
47
+ try {
48
+ const files = fs.readdirSync(dir)
49
+ .filter(f => f.endsWith(".jsonl"))
50
+ .map(f => {
51
+ const fullPath = path.join(dir, f);
52
+ const stats = fs.statSync(fullPath);
53
+ return { path: fullPath, mtime: stats.mtime };
54
+ });
55
+ allTranscripts.push(...files);
56
+ }
57
+ catch (error) {
58
+ // Ignore read errors for individual directories
59
+ continue;
60
+ }
61
+ }
62
+ if (allTranscripts.length === 0)
63
+ return null;
64
+ // Sort by modification time, most recent first
65
+ allTranscripts.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
66
+ // Only consider files modified in the last 5 minutes (active session)
67
+ const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
68
+ const recentTranscripts = allTranscripts.filter(t => t.mtime.getTime() > fiveMinutesAgo);
69
+ if (recentTranscripts.length === 0) {
70
+ console.warn("[session_close] No recently modified transcripts found (last 5 min)");
71
+ return allTranscripts[0].path; // Fallback to most recent overall
72
+ }
73
+ return recentTranscripts[0].path;
74
+ }
75
+ /**
76
+ * Extract Claude Code session ID from transcript JSONL content
77
+ * OD-538: Provides traceability between GitMem sessions and IDE sessions
78
+ */
79
+ function extractClaudeSessionId(transcriptContent, filePath) {
80
+ try {
81
+ // Try to parse first line of JSONL
82
+ const firstLine = transcriptContent.split('\n')[0];
83
+ if (!firstLine)
84
+ return null;
85
+ const firstMessage = JSON.parse(firstLine);
86
+ // Check for session_id in message metadata
87
+ if (firstMessage.session_id) {
88
+ return firstMessage.session_id;
89
+ }
90
+ // Fallback: extract from filename (format: {session-id}.jsonl)
91
+ const filename = path.basename(filePath, '.jsonl');
92
+ // Validate it looks like a UUID
93
+ if (filename.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/)) {
94
+ return filename;
95
+ }
96
+ return null;
97
+ }
98
+ catch (error) {
99
+ // If parsing fails, try filename extraction
100
+ const filename = path.basename(filePath, '.jsonl');
101
+ if (filename.match(/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/)) {
102
+ return filename;
103
+ }
104
+ return null;
105
+ }
106
+ }
107
+ /**
108
+ * Free tier session_close — persist locally, skip compliance/transcripts/embedding
109
+ */
110
+ async function sessionCloseFree(params, timer) {
111
+ const storage = getStorage();
112
+ const env = detectAgent();
113
+ const agentIdentity = env.agent;
114
+ const sessionId = params.session_id || uuidv4();
115
+ // Build minimal close compliance
116
+ const learningsCount = params.learnings_created?.length || 0;
117
+ const closeCompliance = {
118
+ close_type: params.close_type,
119
+ agent: agentIdentity,
120
+ checklist_displayed: true,
121
+ questions_answered_by_agent: !!params.closing_reflection,
122
+ human_asked_for_corrections: !!params.human_corrections || params.human_corrections === "",
123
+ learnings_stored: learningsCount,
124
+ scars_applied: params.closing_reflection?.scars_applied?.length || 0,
125
+ };
126
+ try {
127
+ // Load existing session if available
128
+ const existingSession = await storage.get("sessions", sessionId);
129
+ const sessionData = {
130
+ ...(existingSession || {}),
131
+ id: sessionId,
132
+ close_compliance: closeCompliance,
133
+ };
134
+ if (params.closing_reflection) {
135
+ const reflection = { ...params.closing_reflection };
136
+ if (params.human_corrections) {
137
+ reflection.human_additions = params.human_corrections;
138
+ }
139
+ sessionData.closing_reflection = reflection;
140
+ }
141
+ if (params.decisions && params.decisions.length > 0) {
142
+ sessionData.decisions = params.decisions.map((d) => d.title);
143
+ }
144
+ // OD-thread-lifecycle: Normalize threads for free tier too
145
+ const freeSessionThreads = getThreads();
146
+ if (params.open_threads && params.open_threads.length > 0) {
147
+ const normalized = normalizeThreads(params.open_threads, params.session_id);
148
+ const merged = freeSessionThreads.length > 0
149
+ ? mergeThreadStates(normalized, freeSessionThreads)
150
+ : normalized;
151
+ sessionData.open_threads = merged;
152
+ }
153
+ else if (freeSessionThreads.length > 0) {
154
+ sessionData.open_threads = freeSessionThreads;
155
+ }
156
+ if (params.project_state) {
157
+ const projectStateText = `PROJECT STATE: ${params.project_state}`;
158
+ const existing = (sessionData.open_threads || []);
159
+ const filtered = existing.filter((t) => {
160
+ const text = typeof t === "string" ? t : t.text;
161
+ return !text.startsWith("PROJECT STATE:");
162
+ });
163
+ sessionData.open_threads = [migrateStringThread(projectStateText, params.session_id), ...filtered];
164
+ }
165
+ // Persist session locally
166
+ await storage.upsert("sessions", sessionData);
167
+ // Record scar usage locally if provided
168
+ if (params.scars_to_record && params.scars_to_record.length > 0) {
169
+ for (const scar of params.scars_to_record) {
170
+ await storage.upsert("scar_usage", {
171
+ id: uuidv4(),
172
+ scar_id: scar.scar_identifier,
173
+ session_id: sessionId,
174
+ agent: agentIdentity,
175
+ surfaced_at: scar.surfaced_at,
176
+ reference_type: scar.reference_type,
177
+ reference_context: scar.reference_context,
178
+ created_at: new Date().toISOString(),
179
+ });
180
+ }
181
+ }
182
+ // Clear session state
183
+ clearCurrentSession();
184
+ // GIT-21: Clean up session files (registry, per-session dir, legacy file)
185
+ cleanupSessionFiles(sessionId);
186
+ const latencyMs = timer.stop();
187
+ const perfData = buildPerformanceData("session_close", latencyMs, 1);
188
+ const display = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, true);
189
+ return {
190
+ success: true,
191
+ session_id: sessionId,
192
+ close_compliance: closeCompliance,
193
+ performance: perfData,
194
+ display,
195
+ };
196
+ }
197
+ catch (error) {
198
+ const errorMessage = error instanceof Error ? error.message : String(error);
199
+ clearCurrentSession();
200
+ const latencyMs = timer.stop();
201
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
202
+ const errorDisplay = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, false, [`Failed to persist session: ${errorMessage}`]);
203
+ return {
204
+ success: false,
205
+ session_id: sessionId,
206
+ close_compliance: closeCompliance,
207
+ validation_errors: [`Failed to persist session: ${errorMessage}`],
208
+ performance: perfData,
209
+ display: errorDisplay,
210
+ };
211
+ }
212
+ }
213
+ /**
214
+ * Build a pre-formatted display string for consistent CLI output.
215
+ * Agents echo this string directly instead of formatting ad-hoc.
216
+ */
217
+ function formatCloseDisplay(sessionId, compliance, params, learningsCount, success, errors) {
218
+ const lines = [];
219
+ if (!success) {
220
+ lines.push("**Session close FAILED.**");
221
+ if (errors?.length) {
222
+ for (const e of errors)
223
+ lines.push(`- Error: ${e}`);
224
+ }
225
+ lines.push("");
226
+ }
227
+ // Header
228
+ const closeLabel = compliance.close_type.toUpperCase();
229
+ lines.push(`## ${closeLabel} CLOSE — ${success ? "COMPLETE" : "FAILED"}`);
230
+ lines.push(`**Session:** \`${sessionId.slice(0, 8)}\` | **Agent:** ${compliance.agent}`);
231
+ // Checklist
232
+ const check = (ok) => ok ? "done" : "missing";
233
+ lines.push("");
234
+ lines.push(`### Checklist`);
235
+ if (compliance.close_type === "standard") {
236
+ lines.push(`- [${check(compliance.checklist_displayed)}] Read active-session.json`);
237
+ lines.push(`- [${check(compliance.questions_answered_by_agent)}] Agent answered 7 questions`);
238
+ lines.push(`- [${check(compliance.human_asked_for_corrections)}] Human asked for corrections`);
239
+ lines.push(`- [${check(learningsCount > 0)}] Created learning entries (${learningsCount})`);
240
+ lines.push(`- [${check(compliance.scars_applied > 0)}] Recorded scar usage (${compliance.scars_applied})`);
241
+ lines.push(`- [${check(success)}] Session persisted`);
242
+ }
243
+ else {
244
+ lines.push(`- [${check(success)}] Session persisted`);
245
+ lines.push(`- Agent: ${compliance.agent} | Close type: ${compliance.close_type}`);
246
+ }
247
+ // Threads summary
248
+ const threads = params.open_threads || [];
249
+ if (threads.length > 0) {
250
+ const openCount = threads.filter(t => {
251
+ if (typeof t === "string")
252
+ return true;
253
+ return t.status === "open";
254
+ }).length;
255
+ const resolvedCount = threads.length - openCount;
256
+ lines.push("");
257
+ lines.push(`### Threads`);
258
+ lines.push(`${openCount} open, ${resolvedCount} resolved, ${threads.length} total`);
259
+ }
260
+ // Decisions
261
+ if (params.decisions?.length) {
262
+ lines.push("");
263
+ lines.push(`### Decisions`);
264
+ lines.push(`${params.decisions.length} captured`);
265
+ }
266
+ // Learnings
267
+ if (learningsCount > 0) {
268
+ lines.push("");
269
+ lines.push(`### Learnings`);
270
+ lines.push(`${learningsCount} created`);
271
+ }
272
+ return lines.join("\n");
273
+ }
274
+ /**
275
+ * GIT-21: Clean up all session files for a closed session.
276
+ * Unregisters from registry, deletes per-session directory, and removes legacy file.
277
+ */
278
+ function cleanupSessionFiles(sessionId) {
279
+ // 1. Unregister from active-sessions registry
280
+ try {
281
+ unregisterSession(sessionId);
282
+ }
283
+ catch (error) {
284
+ console.warn("[session_close] Failed to unregister session:", error);
285
+ }
286
+ // 2. Delete per-session directory
287
+ try {
288
+ const sessionDir = path.join(getGitmemDir(), "sessions", sessionId);
289
+ if (fs.existsSync(sessionDir)) {
290
+ fs.rmSync(sessionDir, { recursive: true, force: true });
291
+ console.error(`[session_close] Cleaned up session directory: ${sessionDir}`);
292
+ }
293
+ }
294
+ catch (error) {
295
+ console.warn("[session_close] Failed to clean up session directory:", error);
296
+ }
297
+ // Legacy active-session.json cleanup removed — file is no longer written
298
+ }
299
+ /**
300
+ * Execute session_close tool
301
+ */
302
+ export async function sessionClose(params) {
303
+ const timer = new Timer();
304
+ const metricsId = uuidv4();
305
+ // GIT-21: Recover session_id from active-sessions registry (hostname+PID) or legacy file
306
+ if (!params.session_id && params.close_type !== "retroactive") {
307
+ // Try registry first (GIT-20 writes here)
308
+ try {
309
+ const mySession = findSessionByHostPid(os.hostname(), process.pid);
310
+ if (mySession) {
311
+ console.error(`[session_close] Recovered session_id from registry: ${mySession.session_id}`);
312
+ params = { ...params, session_id: mySession.session_id };
313
+ }
314
+ }
315
+ catch (error) {
316
+ console.warn("[session_close] Failed to check session registry:", error);
317
+ }
318
+ // Legacy active-session.json fallback removed — registry is the source of truth
319
+ }
320
+ // 0a. File-based payload handoff: if .gitmem/closing-payload.json exists,
321
+ // merge it with inline params (inline params take precedence).
322
+ // This keeps the visible MCP tool call small: just session_id + close_type.
323
+ const payloadPath = getGitmemPath("closing-payload.json");
324
+ try {
325
+ if (fs.existsSync(payloadPath)) {
326
+ const filePayload = JSON.parse(fs.readFileSync(payloadPath, "utf-8"));
327
+ // File provides defaults; inline params override
328
+ params = { ...filePayload, ...params };
329
+ console.error(`[session_close] Loaded closing payload from ${payloadPath}`);
330
+ // Clean up payload file
331
+ try {
332
+ fs.unlinkSync(payloadPath);
333
+ }
334
+ catch { /* ignore */ }
335
+ }
336
+ }
337
+ catch (error) {
338
+ console.warn("[session_close] Failed to read closing-payload.json:", error);
339
+ }
340
+ // Close type auto-detection: reject mismatched close types based on session activity.
341
+ // Standard close on a short/trivial session is wasteful; quick close on a long session loses data.
342
+ // t-f7c2fa01: If closing_reflection is already present (agent answered 7 questions),
343
+ // skip the mismatch gate — the ceremony is done, rejecting it wastes work.
344
+ const hasReflection = params.closing_reflection &&
345
+ Object.keys(params.closing_reflection).length > 0;
346
+ const activity = getSessionActivity();
347
+ if (activity && params.close_type && !hasReflection) {
348
+ const isMinimal = activity.recall_count === 0 &&
349
+ activity.observation_count === 0 &&
350
+ activity.children_count === 0;
351
+ if (params.close_type === "standard" && activity.duration_min < 30 && isMinimal) {
352
+ const latencyMs = timer.stop();
353
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
354
+ return {
355
+ success: false,
356
+ session_id: params.session_id || "",
357
+ close_compliance: {
358
+ close_type: params.close_type,
359
+ agent: detectAgent().agent,
360
+ checklist_displayed: false,
361
+ questions_answered_by_agent: false,
362
+ human_asked_for_corrections: false,
363
+ learnings_stored: 0,
364
+ scars_applied: 0,
365
+ },
366
+ validation_errors: [
367
+ `Close type mismatch: "standard" requested but session qualifies for "quick".`,
368
+ `Session duration: ${Math.round(activity.duration_min)} min (< 30 min threshold).`,
369
+ `Activity: ${activity.recall_count} recalls, ${activity.observation_count} observations, ${activity.children_count} children.`,
370
+ `Re-call with close_type: "quick" for short exploratory sessions.`,
371
+ ],
372
+ performance: perfData,
373
+ display: `## CLOSE TYPE MISMATCH\n\nSession is ${Math.round(activity.duration_min)} min with no substantive activity.\nUse \`close_type: "quick"\` instead of \`"standard"\`.`,
374
+ };
375
+ }
376
+ if (params.close_type === "quick" && (activity.duration_min >= 30 || !isMinimal)) {
377
+ // Warn but don't reject — agent chose quick on a substantive session
378
+ console.error(`[session_close] Warning: "quick" close on substantive session ` +
379
+ `(${Math.round(activity.duration_min)} min, ${activity.recall_count} recalls, ` +
380
+ `${activity.observation_count} observations). Consider "standard" close.`);
381
+ }
382
+ }
383
+ // Free tier: simple local persistence, skip Supabase recovery and compliance
384
+ if (!hasSupabase()) {
385
+ return sessionCloseFree(params, timer);
386
+ }
387
+ // 0b. If still no session_id, fall back to Supabase query for unclosed session from today
388
+ if (!params.session_id && params.close_type !== "retroactive") {
389
+ const env = detectAgent();
390
+ const agent = env.agent;
391
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
392
+ try {
393
+ const sessions = await supabase.listRecords({
394
+ table: "orchestra_sessions_lite",
395
+ filters: { agent },
396
+ limit: 10,
397
+ orderBy: { column: "created_at", ascending: false },
398
+ });
399
+ // Find most recent unclosed session from today
400
+ const unclosedToday = sessions.find(s => !s.close_compliance &&
401
+ s.session_date?.startsWith(today));
402
+ if (unclosedToday) {
403
+ console.error(`[session_close] Found unclosed session from today: ${unclosedToday.id}`);
404
+ params = { ...params, session_id: unclosedToday.id };
405
+ }
406
+ else {
407
+ // No unclosed session found - STOP and require session_id
408
+ const latencyMs = timer.stop();
409
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
410
+ return {
411
+ success: false,
412
+ session_id: "", // Empty string when no session found
413
+ close_compliance: {
414
+ close_type: params.close_type,
415
+ agent,
416
+ checklist_displayed: false,
417
+ questions_answered_by_agent: false,
418
+ human_asked_for_corrections: false,
419
+ learnings_stored: 0,
420
+ scars_applied: 0,
421
+ },
422
+ validation_errors: [
423
+ "No session_id provided and no unclosed session found from today.",
424
+ "Please call session_start first or provide the session_id from an earlier session_start.",
425
+ `Sessions checked: ${sessions.length}, none unclosed from today (${today})`
426
+ ],
427
+ performance: perfData,
428
+ };
429
+ }
430
+ }
431
+ catch (error) {
432
+ const errorMessage = error instanceof Error ? error.message : String(error);
433
+ const latencyMs = timer.stop();
434
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
435
+ return {
436
+ success: false,
437
+ session_id: "", // Empty string when search fails
438
+ close_compliance: {
439
+ close_type: params.close_type,
440
+ agent,
441
+ checklist_displayed: false,
442
+ questions_answered_by_agent: false,
443
+ human_asked_for_corrections: false,
444
+ learnings_stored: 0,
445
+ scars_applied: 0,
446
+ },
447
+ validation_errors: [
448
+ `Failed to search for unclosed sessions: ${errorMessage}`,
449
+ "Please provide session_id explicitly."
450
+ ],
451
+ performance: perfData,
452
+ };
453
+ }
454
+ }
455
+ // 1. Validate parameters
456
+ const validation = validateSessionClose(params);
457
+ if (!validation.valid) {
458
+ const latencyMs = timer.stop();
459
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
460
+ return {
461
+ success: false,
462
+ session_id: params.session_id,
463
+ close_compliance: {
464
+ close_type: params.close_type,
465
+ agent: "Unknown",
466
+ checklist_displayed: false,
467
+ questions_answered_by_agent: false,
468
+ human_asked_for_corrections: false,
469
+ learnings_stored: 0,
470
+ scars_applied: 0,
471
+ },
472
+ validation_errors: validation.errors,
473
+ performance: perfData,
474
+ };
475
+ }
476
+ // 2. Get agent identity
477
+ const env = detectAgent();
478
+ const agentIdentity = env.agent;
479
+ // 3. Build close compliance
480
+ const learningsCount = params.learnings_created?.length || 0;
481
+ const closeCompliance = buildCloseCompliance(params, agentIdentity, learningsCount);
482
+ // Add ceremony duration if provided
483
+ if (params.ceremony_duration_ms !== undefined) {
484
+ closeCompliance.ceremony_duration_ms = params.ceremony_duration_ms;
485
+ }
486
+ // 4. Handle retroactive vs normal close modes
487
+ const isRetroactive = params.close_type === "retroactive";
488
+ let sessionId;
489
+ let existingSession = null;
490
+ if (isRetroactive) {
491
+ // Retroactive mode: generate new session_id, create from scratch
492
+ // Only used when explicitly requested (not auto-triggered)
493
+ sessionId = uuidv4();
494
+ }
495
+ else {
496
+ // Normal mode: require existing session (guaranteed to exist by step 0 above)
497
+ sessionId = params.session_id;
498
+ try {
499
+ existingSession = await supabase.getRecord("orchestra_sessions", sessionId);
500
+ }
501
+ catch {
502
+ // Session might not exist yet, which is fine
503
+ }
504
+ if (!existingSession) {
505
+ const latencyMs = timer.stop();
506
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
507
+ return {
508
+ success: false,
509
+ session_id: sessionId,
510
+ close_compliance: closeCompliance,
511
+ validation_errors: [`Session ${sessionId} not found. Was session_start called?`],
512
+ performance: perfData,
513
+ };
514
+ }
515
+ }
516
+ // 5. Build session data (merge with existing or create from scratch)
517
+ let sessionData;
518
+ if (isRetroactive) {
519
+ // Retroactive mode: create minimal session from scratch
520
+ const now = new Date().toISOString();
521
+ sessionData = {
522
+ id: sessionId,
523
+ agent: agentIdentity,
524
+ project: "orchestra_dev", // Default for retroactive
525
+ session_title: "Retroactive Session", // Will be updated below if we have content
526
+ session_date: now,
527
+ created_at: now,
528
+ close_compliance: closeCompliance,
529
+ };
530
+ }
531
+ else {
532
+ // Normal mode: merge with existing session
533
+ // Remove embedding from existing to avoid re-embedding unchanged text
534
+ const { embedding: _embedding, ...existingWithoutEmbedding } = existingSession;
535
+ sessionData = {
536
+ ...existingWithoutEmbedding,
537
+ close_compliance: closeCompliance,
538
+ };
539
+ }
540
+ // Add closing reflection if provided
541
+ if (params.closing_reflection) {
542
+ const reflection = { ...params.closing_reflection };
543
+ // Add human corrections to reflection if provided
544
+ if (params.human_corrections) {
545
+ reflection.human_additions = params.human_corrections;
546
+ }
547
+ sessionData.closing_reflection = reflection;
548
+ }
549
+ // Add decisions if provided
550
+ if (params.decisions && params.decisions.length > 0) {
551
+ sessionData.decisions = params.decisions.map((d) => d.title);
552
+ }
553
+ // OD-thread-lifecycle: Normalize and merge open threads
554
+ const sessionThreads = getThreads(); // Mid-session thread state (may have resolutions)
555
+ if (params.open_threads && params.open_threads.length > 0) {
556
+ const normalized = normalizeThreads(params.open_threads, params.session_id);
557
+ // Merge incoming with mid-session state (preserves resolutions from resolve_thread calls)
558
+ const merged = sessionThreads.length > 0
559
+ ? mergeThreadStates(normalized, sessionThreads)
560
+ : normalized;
561
+ sessionData.open_threads = merged;
562
+ }
563
+ else if (sessionThreads.length > 0) {
564
+ // No new threads from close payload, but we have mid-session state (e.g., resolutions)
565
+ sessionData.open_threads = sessionThreads;
566
+ }
567
+ // OD-534: If project_state provided, prepend it to open_threads as a ThreadObject
568
+ if (params.project_state) {
569
+ const projectStateText = `PROJECT STATE: ${params.project_state}`;
570
+ const existing = (sessionData.open_threads || []);
571
+ // Replace existing PROJECT STATE if present, otherwise prepend
572
+ const filtered = existing.filter(t => {
573
+ const text = typeof t === "string" ? t : t.text;
574
+ return !text.startsWith("PROJECT STATE:");
575
+ });
576
+ sessionData.open_threads = [migrateStringThread(projectStateText, params.session_id), ...filtered];
577
+ }
578
+ // OD-624: Sync threads to Supabase (source of truth)
579
+ // New threads get created, resolved threads get updated, existing threads get touched.
580
+ // This runs async — does not block session close on failure.
581
+ const closeThreads = (sessionData.open_threads || []);
582
+ if (closeThreads.length > 0) {
583
+ const closeProject = isRetroactive ? "orchestra_dev" : existingSession?.project || "orchestra_dev";
584
+ syncThreadsToSupabase(closeThreads, closeProject, sessionId).catch((err) => {
585
+ console.error("[session_close] Thread Supabase sync failed (non-fatal):", err);
586
+ });
587
+ }
588
+ // v2 Phase 2: Persist observations and children from multi-agent work
589
+ const observations = getObservations();
590
+ if (observations.length > 0) {
591
+ sessionData.task_observations = observations;
592
+ }
593
+ const sessionChildren = getChildren();
594
+ if (sessionChildren.length > 0) {
595
+ sessionData.children = sessionChildren;
596
+ }
597
+ // Add linear issue if provided
598
+ if (params.linear_issue) {
599
+ sessionData.linear_issue = params.linear_issue;
600
+ }
601
+ // Update session title if we have meaningful content
602
+ if (params.closing_reflection?.what_worked || params.decisions?.length) {
603
+ const titleParts = [];
604
+ if (params.linear_issue) {
605
+ titleParts.push(params.linear_issue);
606
+ }
607
+ if (params.decisions?.length) {
608
+ titleParts.push(params.decisions[0].title);
609
+ }
610
+ else if (params.closing_reflection?.what_worked) {
611
+ // Use first 50 chars of what_worked as title hint
612
+ titleParts.push(params.closing_reflection.what_worked.slice(0, 50));
613
+ }
614
+ if (titleParts.length > 0 &&
615
+ (sessionData.session_title === "Interactive Session" ||
616
+ sessionData.session_title === "Retroactive Session")) {
617
+ sessionData.session_title = titleParts.join(" - ");
618
+ }
619
+ }
620
+ // OD-538: Capture transcript if enabled (default true for CLI/DAC)
621
+ const shouldCaptureTranscript = params.capture_transcript !== false &&
622
+ (agentIdentity === "CLI" || agentIdentity === "DAC");
623
+ if (shouldCaptureTranscript) {
624
+ try {
625
+ let transcriptFilePath = null;
626
+ // Option 1: Explicit transcript path provided (overrides auto-detection)
627
+ if (params.transcript_path) {
628
+ if (fs.existsSync(params.transcript_path)) {
629
+ transcriptFilePath = params.transcript_path;
630
+ console.error(`[session_close] Using explicit transcript path: ${transcriptFilePath}`);
631
+ }
632
+ else {
633
+ console.warn(`[session_close] Explicit transcript path does not exist: ${params.transcript_path}`);
634
+ }
635
+ }
636
+ // Option 2: Auto-detect by searching for most recent transcript
637
+ if (!transcriptFilePath) {
638
+ const homeDir = os.homedir();
639
+ const projectsDir = path.join(homeDir, ".claude", "projects");
640
+ const cwd = process.cwd();
641
+ const projectDirName = path.basename(cwd);
642
+ transcriptFilePath = findMostRecentTranscript(projectsDir, projectDirName, cwd);
643
+ if (transcriptFilePath) {
644
+ console.error(`[session_close] Auto-detected transcript: ${transcriptFilePath}`);
645
+ }
646
+ else {
647
+ console.error(`[session_close] No transcript file found in ${projectsDir}`);
648
+ }
649
+ }
650
+ // If we found a transcript, capture it
651
+ if (transcriptFilePath) {
652
+ const transcriptContent = fs.readFileSync(transcriptFilePath, "utf-8");
653
+ // Extract Claude Code session ID for traceability
654
+ const claudeSessionId = extractClaudeSessionId(transcriptContent, transcriptFilePath);
655
+ if (claudeSessionId) {
656
+ sessionData.claude_code_session_id = claudeSessionId;
657
+ console.error(`[session_close] Extracted Claude session ID: ${claudeSessionId}`);
658
+ }
659
+ // Call save_transcript tool
660
+ const saveResult = await saveTranscript({
661
+ session_id: sessionId,
662
+ transcript: transcriptContent,
663
+ format: "json",
664
+ project: isRetroactive ? "orchestra_dev" : existingSession?.project,
665
+ });
666
+ if (saveResult.success && saveResult.transcript_path) {
667
+ sessionData.transcript_path = saveResult.transcript_path;
668
+ console.error(`[session_close] Transcript saved: ${saveResult.transcript_path} (${saveResult.size_kb}KB)`);
669
+ // OD-540: Process transcript for semantic search (async, don't block session close)
670
+ processTranscript(sessionId, transcriptContent, isRetroactive ? "orchestra_dev" : existingSession?.project).then(result => {
671
+ if (result.success) {
672
+ console.error(`[session_close] Transcript chunking completed: ${result.chunksCreated} chunks created`);
673
+ }
674
+ else {
675
+ console.warn(`[session_close] Transcript chunking failed: ${result.error}`);
676
+ }
677
+ }).catch(err => {
678
+ console.error("[session_close] Transcript chunking error:", err);
679
+ });
680
+ }
681
+ else {
682
+ console.warn(`[session_close] Failed to save transcript: ${saveResult.error}`);
683
+ }
684
+ }
685
+ }
686
+ catch (error) {
687
+ // Don't fail session close if transcript capture fails
688
+ console.error("[session_close] Exception during transcript capture:", error);
689
+ }
690
+ }
691
+ // OD-552: Auto-bridge Q6 answers (closing_reflection.scars_applied) to scar_usage records
692
+ // This is the core fix: CLI/DAC sessions answer Q6 with scar names but these never
693
+ // became structured scar_usage records. Now we match Q6 answers against surfaced scars.
694
+ if ((!params.scars_to_record || params.scars_to_record.length === 0) &&
695
+ params.closing_reflection?.scars_applied?.length) {
696
+ try {
697
+ // Load surfaced scars: prefer in-memory, fall back to per-session dir, then legacy file
698
+ let surfacedScars = getSurfacedScars();
699
+ if (surfacedScars.length === 0 && params.session_id) {
700
+ // GIT-21: Try per-session directory first
701
+ try {
702
+ const sessionFilePath = getSessionPath(params.session_id, "session.json");
703
+ if (fs.existsSync(sessionFilePath)) {
704
+ const sessionData = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
705
+ if (sessionData.surfaced_scars && Array.isArray(sessionData.surfaced_scars)) {
706
+ surfacedScars = sessionData.surfaced_scars;
707
+ console.error(`[session_close] Loaded ${surfacedScars.length} surfaced scars from per-session file`);
708
+ }
709
+ }
710
+ }
711
+ catch { /* per-session file read failed */ }
712
+ }
713
+ if (surfacedScars.length > 0) {
714
+ const autoBridgedScars = [];
715
+ const matchedScarIds = new Set();
716
+ // For each Q6 answer, try to match against surfaced scars
717
+ for (const scarApplied of params.closing_reflection.scars_applied) {
718
+ const lowerApplied = scarApplied.toLowerCase();
719
+ // Match by UUID or title substring
720
+ const match = surfacedScars.find((s) => {
721
+ if (matchedScarIds.has(s.scar_id))
722
+ return false; // Don't double-match
723
+ return (s.scar_id === scarApplied || // Exact UUID match
724
+ s.scar_title.toLowerCase().includes(lowerApplied) || // Title contains answer
725
+ lowerApplied.includes(s.scar_title.toLowerCase()) // Answer contains title
726
+ );
727
+ });
728
+ if (match) {
729
+ matchedScarIds.add(match.scar_id);
730
+ autoBridgedScars.push({
731
+ scar_identifier: match.scar_id,
732
+ session_id: sessionId,
733
+ agent: agentIdentity,
734
+ surfaced_at: match.surfaced_at,
735
+ reference_type: "acknowledged",
736
+ reference_context: `Auto-bridged from Q6 answer: "${scarApplied}"`,
737
+ });
738
+ }
739
+ }
740
+ // For surfaced scars NOT mentioned in Q6, record as "none" (surfaced but ignored)
741
+ for (const scar of surfacedScars) {
742
+ if (!matchedScarIds.has(scar.scar_id)) {
743
+ autoBridgedScars.push({
744
+ scar_identifier: scar.scar_id,
745
+ session_id: sessionId,
746
+ agent: agentIdentity,
747
+ surfaced_at: scar.surfaced_at,
748
+ reference_type: "none",
749
+ reference_context: `Surfaced during ${scar.source} but not mentioned in closing reflection`,
750
+ });
751
+ }
752
+ }
753
+ if (autoBridgedScars.length > 0) {
754
+ params = { ...params, scars_to_record: autoBridgedScars };
755
+ console.error(`[session_close] Auto-bridged ${autoBridgedScars.length} scar usage records (${matchedScarIds.size} acknowledged, ${autoBridgedScars.length - matchedScarIds.size} unmentioned)`);
756
+ }
757
+ }
758
+ else {
759
+ console.error("[session_close] No surfaced scars available for auto-bridge");
760
+ }
761
+ }
762
+ catch (bridgeError) {
763
+ console.error("[session_close] Auto-bridge failed (non-fatal):", bridgeError);
764
+ }
765
+ }
766
+ // 6. Persist to Supabase (direct REST API, bypasses ww-mcp)
767
+ try {
768
+ // Generate embedding for session data
769
+ if (isEmbeddingAvailable()) {
770
+ try {
771
+ const embeddingParts = [
772
+ sessionData.session_title || "",
773
+ params.closing_reflection?.what_worked || "",
774
+ params.closing_reflection?.what_broke || "",
775
+ ...(params.open_threads || []).map(t => typeof t === "string" ? t : t.text),
776
+ ].filter(Boolean);
777
+ const embeddingText = embeddingParts.join(" | ");
778
+ if (embeddingText.length > 10) {
779
+ const embeddingVector = await embed(embeddingText);
780
+ if (embeddingVector) {
781
+ sessionData.embedding = JSON.stringify(embeddingVector);
782
+ }
783
+ }
784
+ }
785
+ catch (embError) {
786
+ console.warn("[session_close] Embedding generation failed (non-fatal):", embError);
787
+ }
788
+ }
789
+ await supabase.directUpsert("orchestra_sessions", sessionData);
790
+ // Phase 5: Implicit thread detection (fire-and-forget)
791
+ if (sessionData.embedding) {
792
+ (async () => {
793
+ try {
794
+ const sessionEmb = JSON.parse(sessionData.embedding);
795
+ const suggestProject = existingSession?.project || "orchestra_dev";
796
+ const recentSessions = await loadRecentSessionEmbeddings(suggestProject, 30, 20);
797
+ const threadEmbs = await loadOpenThreadEmbeddings(suggestProject);
798
+ if (recentSessions && threadEmbs) {
799
+ const existing = loadSuggestions();
800
+ const updated = detectSuggestedThreads({ session_id: sessionId, title: sessionData.session_title, embedding: sessionEmb }, recentSessions, threadEmbs, existing);
801
+ saveSuggestions(updated);
802
+ }
803
+ }
804
+ catch (err) {
805
+ console.error("[session_close] Thread suggestion detection failed (non-fatal):", err);
806
+ }
807
+ })();
808
+ }
809
+ // 7. Record scar usage if provided (parallel with metrics)
810
+ // OD-552: scars_to_record may now come from auto-bridge above
811
+ let scarRecordingResults;
812
+ if (params.scars_to_record && params.scars_to_record.length > 0) {
813
+ const project = isRetroactive
814
+ ? "orchestra_dev"
815
+ : existingSession.project;
816
+ scarRecordingResults = await recordScarUsageBatch({
817
+ scars: params.scars_to_record,
818
+ project,
819
+ });
820
+ }
821
+ const latencyMs = timer.stop();
822
+ const perfData = buildPerformanceData("session_close", latencyMs, 1);
823
+ // Update relevance data for memories applied during session
824
+ if (params.closing_reflection?.scars_applied?.length) {
825
+ updateRelevanceData(sessionId, params.closing_reflection.scars_applied).catch(() => { });
826
+ }
827
+ // Record metrics
828
+ recordMetrics({
829
+ id: metricsId,
830
+ session_id: sessionId,
831
+ agent: agentIdentity,
832
+ tool_name: "session_close",
833
+ tables_searched: ["orchestra_sessions"],
834
+ latency_ms: latencyMs,
835
+ result_count: 1,
836
+ phase_tag: "session_close",
837
+ linear_issue: params.linear_issue,
838
+ metadata: {
839
+ close_type: params.close_type,
840
+ learnings_created: learningsCount,
841
+ scars_applied: closeCompliance.scars_applied,
842
+ decisions_count: params.decisions?.length || 0,
843
+ open_threads_count: params.open_threads?.length || 0,
844
+ ceremony_duration_ms: params.ceremony_duration_ms,
845
+ scars_recorded_batch: scarRecordingResults?.resolved_count || 0,
846
+ scars_failed_batch: scarRecordingResults?.failed_count || 0,
847
+ retroactive: isRetroactive,
848
+ },
849
+ }).catch(() => { });
850
+ // OD-547: Clear session state after successful close
851
+ clearCurrentSession();
852
+ // GIT-21: Clean up session files (registry, per-session dir, legacy file)
853
+ cleanupSessionFiles(sessionId);
854
+ const display = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, true, validation.warnings.length > 0 ? validation.warnings : undefined);
855
+ return {
856
+ success: true,
857
+ session_id: sessionId,
858
+ close_compliance: closeCompliance,
859
+ validation_errors: validation.warnings.length > 0 ? validation.warnings : undefined,
860
+ performance: perfData,
861
+ display,
862
+ };
863
+ }
864
+ catch (error) {
865
+ const errorMessage = error instanceof Error ? error.message : String(error);
866
+ const latencyMs = timer.stop();
867
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
868
+ // OD-547: Clear session state even on error (session is done either way)
869
+ clearCurrentSession();
870
+ const errorDisplay = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, false, [`Failed to persist session: ${errorMessage}`]);
871
+ return {
872
+ success: false,
873
+ session_id: sessionId,
874
+ close_compliance: closeCompliance,
875
+ validation_errors: [`Failed to persist session: ${errorMessage}`],
876
+ performance: perfData,
877
+ display: errorDisplay,
878
+ };
879
+ }
880
+ }
881
+ //# sourceMappingURL=session-close.js.map