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,1104 @@
1
+ /**
2
+ * session_start Tool
3
+ *
4
+ * Initialize session, detect agent, load institutional context.
5
+ * Returns last session, relevant scars, and recent decisions.
6
+ *
7
+ * Performance target: <1500ms (OD-429, revised Feb 2026)
8
+ *
9
+ * OD-473: Uses local vector search for consistent scar results.
10
+ * No file-based caching = no race conditions = deterministic results.
11
+ */
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { v4 as uuidv4 } from "uuid";
15
+ import { detectAgent } from "../services/agent-detection.js";
16
+ import * as supabase from "../services/supabase-client.js";
17
+ import { ensureInitialized, isLocalSearchAvailable } from "../services/startup.js";
18
+ import { localScarSearch } from "../services/local-vector-search.js";
19
+ import { hasSupabase } from "../services/tier.js";
20
+ import { getStorage } from "../services/storage.js";
21
+ import { Timer, recordMetrics, calculateContextBytes, buildPerformanceData, buildComponentPerformance, } from "../services/metrics.js";
22
+ import { setCurrentSession, getCurrentSession, addSurfacedScars, getSurfacedScars } from "../services/session-state.js"; // OD-547, OD-552
23
+ import { aggregateThreads, saveThreadsFile, loadThreadsFile, mergeThreadStates } from "../services/thread-manager.js"; // OD-thread-lifecycle
24
+ import { loadActiveThreadsFromSupabase, archiveDormantThreads } from "../services/thread-supabase.js"; // OD-623, Phase 6
25
+ import { setGitmemDir, getSessionPath } from "../services/gitmem-dir.js";
26
+ import { registerSession, findSessionByHostPid, pruneStale, migrateFromLegacy } from "../services/active-sessions.js";
27
+ import * as os from "os";
28
+ import { formatDate } from "../services/timezone.js";
29
+ import { loadSuggestions, getPendingSuggestions } from "../services/thread-suggestions.js";
30
+ /**
31
+ * Normalize decisions from mixed formats (strings or objects) to string[].
32
+ * Historical sessions (pre-2026) stored {title, decision} objects.
33
+ * Current code stores title strings only.
34
+ */
35
+ function normalizeDecisions(decisions) {
36
+ return decisions.map((d) => typeof d === "string" ? d : d.title);
37
+ }
38
+ /**
39
+ * Aggregate open threads across multiple recent sessions.
40
+ * Deduplicates by exact lowercase match. Excludes PROJECT STATE: threads
41
+ * (handled separately). Only includes sessions from the last maxAgeDays.
42
+ */
43
+ // aggregateOpenThreads replaced by aggregateThreads from thread-manager.ts (OD-thread-lifecycle)
44
+ /**
45
+ * Load the last CLOSED session for this agent.
46
+ * Filters out orphaned sessions (those without close_compliance).
47
+ * Uses _lite view for performance (OD-460 added arrays to view).
48
+ *
49
+ * OD-489: Returns timing and network call info for instrumentation.
50
+ */
51
+ async function loadLastSession(agent, project) {
52
+ const timer = new Timer();
53
+ try {
54
+ // Use _lite view for performance (excludes embedding)
55
+ // OD-460: View now includes decisions/open_threads arrays
56
+ const sessions = await supabase.listRecords({
57
+ table: "orchestra_sessions_lite",
58
+ filters: { agent, project },
59
+ limit: 10, // Get several to find a closed one + aggregate threads
60
+ orderBy: { column: "created_at", ascending: false },
61
+ });
62
+ // OD-623: Try loading threads from Supabase (source of truth) first
63
+ let aggregated_open_threads;
64
+ let recently_resolved_threads;
65
+ let displayInfo = [];
66
+ const supabaseThreads = await loadActiveThreadsFromSupabase(project);
67
+ if (supabaseThreads !== null) {
68
+ // Supabase is source of truth for threads
69
+ aggregated_open_threads = supabaseThreads.open;
70
+ recently_resolved_threads = supabaseThreads.recentlyResolved;
71
+ displayInfo = supabaseThreads.displayInfo;
72
+ console.error(`[session_start] Loaded threads from Supabase: ${aggregated_open_threads.length} open, ${recently_resolved_threads.length} recently resolved`);
73
+ // Phase 6: Auto-archive dormant threads (fire-and-forget)
74
+ archiveDormantThreads(project).catch(() => { });
75
+ }
76
+ else {
77
+ // Fallback: aggregate from session records (original behavior)
78
+ const threadResult = aggregateThreads(sessions);
79
+ aggregated_open_threads = threadResult.open;
80
+ recently_resolved_threads = threadResult.recently_resolved;
81
+ console.error(`[session_start] Aggregated threads from sessions: ${aggregated_open_threads.length} open, ${recently_resolved_threads.length} recently resolved (Supabase thread query failed)`);
82
+ }
83
+ const latency_ms = timer.stop();
84
+ if (sessions.length === 0) {
85
+ return { session: null, aggregated_open_threads, recently_resolved_threads, displayInfo, latency_ms, network_call: true };
86
+ }
87
+ // Find the most recent session that was properly closed
88
+ const closedSession = sessions.find((s) => s.close_compliance != null);
89
+ if (!closedSession) {
90
+ // Fall back to most recent if none are closed (shouldn't happen often)
91
+ console.error("[session_start] No closed sessions found, using most recent");
92
+ const session = sessions[0];
93
+ return {
94
+ session: {
95
+ id: session.id,
96
+ title: session.session_title || "Untitled Session",
97
+ date: formatDate(session.session_date),
98
+ key_decisions: normalizeDecisions(session.decisions || []),
99
+ open_threads: session.open_threads || [],
100
+ },
101
+ aggregated_open_threads,
102
+ recently_resolved_threads,
103
+ displayInfo,
104
+ latency_ms,
105
+ network_call: true,
106
+ };
107
+ }
108
+ return {
109
+ session: {
110
+ id: closedSession.id,
111
+ title: closedSession.session_title || "Untitled Session",
112
+ date: formatDate(closedSession.session_date),
113
+ key_decisions: normalizeDecisions(closedSession.decisions || []),
114
+ open_threads: closedSession.open_threads || [],
115
+ },
116
+ aggregated_open_threads,
117
+ recently_resolved_threads,
118
+ displayInfo,
119
+ latency_ms,
120
+ network_call: true, // Always hits Supabase (no caching for sessions yet)
121
+ };
122
+ }
123
+ catch (error) {
124
+ console.error("[session_start] Failed to load last session:", error);
125
+ return { session: null, aggregated_open_threads: [], recently_resolved_threads: [], displayInfo: [], latency_ms: timer.stop(), network_call: true };
126
+ }
127
+ }
128
+ /**
129
+ * Query relevant scars based on issue or session context
130
+ *
131
+ * OD-473: Uses local vector search for deterministic results.
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.
137
+ */
138
+ async function queryRelevantScars(issueTitle, issueDescription, issueLabels, project, lastSession) {
139
+ const proj = project || "orchestra_dev";
140
+ const timer = new Timer();
141
+ try {
142
+ // Build query from available context
143
+ const queryParts = [];
144
+ if (issueTitle)
145
+ queryParts.push(issueTitle);
146
+ if (issueDescription)
147
+ queryParts.push(issueDescription.slice(0, 200));
148
+ if (issueLabels?.length)
149
+ queryParts.push(issueLabels.join(" "));
150
+ // Use last session context if no issue context provided
151
+ // Include title, decisions, and open threads for richer scar matching
152
+ if (queryParts.length === 0 && lastSession) {
153
+ if (lastSession.title && lastSession.title !== "Interactive Session") {
154
+ queryParts.push(lastSession.title);
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,
193
+ }));
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
+ }
202
+ catch (error) {
203
+ console.error("[session_start] Failed to query scars:", error);
204
+ return {
205
+ scars: [],
206
+ local_search: false,
207
+ latency_ms: timer.stop(),
208
+ network_call: true, // Assume network was attempted
209
+ };
210
+ }
211
+ }
212
+ /**
213
+ * Load recent decisions with caching (OD-473)
214
+ *
215
+ * OD-489: Returns timing and network call info for instrumentation.
216
+ */
217
+ async function loadRecentDecisions(project, limit = 5) {
218
+ const timer = new Timer();
219
+ try {
220
+ // Use cached decisions query (OD-473)
221
+ // Fetch extra to account for date filtering
222
+ const { data: decisions, cache_hit, cache_age_ms } = await supabase.cachedListDecisions(project, limit + 5);
223
+ // Filter by project in memory if needed (ww-mcp filters may not work with views)
224
+ const filtered = project
225
+ ? decisions.filter((d) => d.project === project)
226
+ : decisions;
227
+ // Time-scope to last 5 days — stale decisions add noise, not context
228
+ const decisionCutoff = new Date();
229
+ decisionCutoff.setDate(decisionCutoff.getDate() - 5);
230
+ const decisionCutoffStr = decisionCutoff.toISOString().split("T")[0];
231
+ const timeScoped = filtered.filter((d) => d.decision_date >= decisionCutoffStr);
232
+ const latency_ms = timer.stop();
233
+ console.error(`[session_start] Loaded ${decisions.length} decisions, ${filtered.length} after project filter, ${timeScoped.length} after 5-day scope, cache_hit=${cache_hit}`);
234
+ const result = timeScoped.slice(0, limit).map((d) => ({
235
+ id: d.id,
236
+ title: d.title,
237
+ decision: d.decision,
238
+ date: formatDate(d.decision_date),
239
+ }));
240
+ return {
241
+ decisions: result,
242
+ cache_hit,
243
+ cache_age_ms,
244
+ latency_ms,
245
+ network_call: !cache_hit, // Network call only if cache miss
246
+ };
247
+ }
248
+ catch (error) {
249
+ console.error("[session_start] Failed to load decisions:", error);
250
+ return {
251
+ decisions: [],
252
+ cache_hit: false,
253
+ latency_ms: timer.stop(),
254
+ network_call: true,
255
+ };
256
+ }
257
+ }
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
+ }
303
+ /**
304
+ * Create a new session record
305
+ *
306
+ * OD-489: Returns timing and network call info for instrumentation.
307
+ */
308
+ async function createSessionRecord(agent, project, linearIssue) {
309
+ const sessionId = uuidv4();
310
+ const today = new Date().toISOString().split("T")[0];
311
+ const timer = new Timer();
312
+ try {
313
+ // OD-cast: Capture asciinema recording path from Docker entrypoint
314
+ const recordingPath = process.env.GITMEM_RECORDING_PATH || null;
315
+ await supabase.directUpsert("orchestra_sessions", {
316
+ id: sessionId,
317
+ session_date: today,
318
+ session_title: linearIssue ? `Session for ${linearIssue}` : "Interactive Session",
319
+ project,
320
+ agent,
321
+ linear_issue: linearIssue || null,
322
+ recording_path: recordingPath,
323
+ // Will be populated on close
324
+ decisions: [],
325
+ open_threads: [],
326
+ closing_reflection: null,
327
+ close_compliance: null,
328
+ });
329
+ return {
330
+ session_id: sessionId,
331
+ latency_ms: timer.stop(),
332
+ network_call: true, // Always writes to Supabase
333
+ };
334
+ }
335
+ catch (error) {
336
+ console.error("[session_start] Failed to create session record:", error);
337
+ // Return ID anyway - session can be created on close
338
+ return {
339
+ session_id: sessionId,
340
+ latency_ms: timer.stop(),
341
+ network_call: true, // Network was attempted
342
+ };
343
+ }
344
+ }
345
+ /**
346
+ * Free tier session_start — all-local, no Supabase
347
+ */
348
+ async function sessionStartFree(params, env, agent, project, timer, metricsId, existingSessionId, existingStartedAt) {
349
+ const storage = getStorage();
350
+ const isResuming = !!existingSessionId;
351
+ const sessionId = existingSessionId || uuidv4();
352
+ const today = new Date().toISOString().split("T")[0];
353
+ // Load last session from local storage
354
+ let lastSession = null;
355
+ let freeAggregatedThreads = [];
356
+ let freeRecentlyResolved = [];
357
+ try {
358
+ const sessions = await storage.query("sessions", {
359
+ order: "session_date.desc",
360
+ limit: 10,
361
+ });
362
+ // Aggregate threads across recent sessions (OD-thread-lifecycle)
363
+ const freeThreadResult = aggregateThreads(sessions);
364
+ freeAggregatedThreads = freeThreadResult.open;
365
+ freeRecentlyResolved = freeThreadResult.recently_resolved;
366
+ const closedSession = sessions.find((s) => s.close_compliance != null) || sessions[0];
367
+ if (closedSession) {
368
+ lastSession = {
369
+ id: closedSession.id,
370
+ title: closedSession.session_title || "Untitled Session",
371
+ date: formatDate(closedSession.session_date),
372
+ key_decisions: normalizeDecisions(closedSession.decisions || []),
373
+ open_threads: closedSession.open_threads || [],
374
+ };
375
+ }
376
+ }
377
+ catch (error) {
378
+ console.error("[session_start] Failed to load last session:", error);
379
+ }
380
+ // Query scars using keyword search
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
+ }
401
+ // Load recent decisions from local storage (time-scoped to 5 days)
402
+ let decisions = [];
403
+ try {
404
+ const decisionRecords = await storage.query("decisions", {
405
+ order: "decision_date.desc",
406
+ limit: 8,
407
+ });
408
+ const freeDecisionCutoff = new Date();
409
+ freeDecisionCutoff.setDate(freeDecisionCutoff.getDate() - 5);
410
+ const freeDecisionCutoffStr = freeDecisionCutoff.toISOString().split("T")[0];
411
+ decisions = decisionRecords
412
+ .filter((d) => d.decision_date >= freeDecisionCutoffStr)
413
+ .slice(0, 3)
414
+ .map((d) => ({
415
+ id: d.id,
416
+ title: d.title,
417
+ decision: d.decision,
418
+ date: formatDate(d.decision_date),
419
+ }));
420
+ }
421
+ catch (error) {
422
+ console.error("[session_start] Failed to load decisions:", error);
423
+ }
424
+ // Load recent wins from local storage (last 7 days)
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
+ }
448
+ // Create session record locally (skip if resuming existing session)
449
+ if (!isResuming) {
450
+ try {
451
+ await storage.upsert("sessions", {
452
+ id: sessionId,
453
+ session_date: today,
454
+ session_title: params.linear_issue ? `Session for ${params.linear_issue}` : "Interactive Session",
455
+ project,
456
+ agent,
457
+ linear_issue: params.linear_issue || null,
458
+ decisions: [],
459
+ open_threads: [],
460
+ closing_reflection: null,
461
+ close_compliance: null,
462
+ });
463
+ }
464
+ catch (error) {
465
+ console.error("[session_start] Failed to create session record:", error);
466
+ }
467
+ }
468
+ else {
469
+ console.error(`[session_start] Resuming session ${sessionId} — skipping record creation`);
470
+ }
471
+ const latencyMs = timer.stop();
472
+ const projectState = lastSession?.open_threads
473
+ ?.map((t) => typeof t === "string" ? t : t.text)
474
+ .find((t) => t.startsWith("PROJECT STATE:"))
475
+ ?.replace(/^PROJECT STATE:\s*/, "");
476
+ const performance = buildPerformanceData("session_start", latencyMs, scars.length + decisions.length + (lastSession ? 1 : 0), {
477
+ memoriesSurfaced: scars.map((s) => s.id),
478
+ similarityScores: scars.map((s) => s.similarity),
479
+ search_mode: "local",
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
+ }));
489
+ // GIT-20: Persist to per-session dir, legacy file, and registry
490
+ // writeSessionFiles merges with existing file threads to preserve mid-session creations
491
+ let freeMergedThreads = freeAggregatedThreads;
492
+ try {
493
+ freeMergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, freeAggregatedThreads);
494
+ }
495
+ catch (error) {
496
+ console.warn("[session_start] Failed to persist session files:", error);
497
+ }
498
+ // t-f7c2fa01: On resume, preserve original startedAt so session_close duration is accurate
499
+ setCurrentSession({
500
+ sessionId,
501
+ linearIssue: params.linear_issue,
502
+ agent,
503
+ startedAt: (isResuming && existingStartedAt) || new Date(),
504
+ surfacedScars,
505
+ threads: freeMergedThreads,
506
+ });
507
+ const freeResult = {
508
+ session_id: sessionId,
509
+ agent,
510
+ ...(isResuming && { resumed: true }),
511
+ detected_environment: env,
512
+ last_session: lastSession,
513
+ ...(projectState && { project_state: projectState }),
514
+ ...(freeMergedThreads.length > 0 && { open_threads: freeMergedThreads }),
515
+ ...(freeRecentlyResolved.length > 0 && { recently_resolved: freeRecentlyResolved }),
516
+ relevant_scars: scars,
517
+ recent_decisions: decisions,
518
+ ...(freeWins.length > 0 && { recent_wins: freeWins }),
519
+ performance,
520
+ };
521
+ freeResult.display = formatStartDisplay(freeResult);
522
+ // Write display to per-session dir
523
+ try {
524
+ const sessionFilePath = getSessionPath(sessionId, "session.json");
525
+ const sessionData = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
526
+ sessionData.display = freeResult.display;
527
+ fs.writeFileSync(sessionFilePath, JSON.stringify(sessionData, null, 2));
528
+ }
529
+ catch { /* non-critical */ }
530
+ return freeResult;
531
+ }
532
+ /**
533
+ * Read session state from per-session directory.
534
+ */
535
+ function readSessionFile(sessionId) {
536
+ try {
537
+ const sessionFilePath = getSessionPath(sessionId, "session.json");
538
+ if (fs.existsSync(sessionFilePath)) {
539
+ return JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
540
+ }
541
+ }
542
+ catch { /* fall through */ }
543
+ return null;
544
+ }
545
+ /**
546
+ * Restore in-memory session state from a session data object.
547
+ * Shared by checkExistingSession() for both registry and legacy paths.
548
+ */
549
+ function restoreSessionState(existing, fallbackAgent) {
550
+ const startedAt = existing.started_at ? new Date(existing.started_at) : undefined;
551
+ setCurrentSession({
552
+ sessionId: existing.session_id,
553
+ linearIssue: existing.linear_issue,
554
+ agent: existing.agent || fallbackAgent,
555
+ startedAt: startedAt || new Date(),
556
+ surfacedScars: existing.surfaced_scars || [],
557
+ });
558
+ if (Array.isArray(existing.surfaced_scars) && existing.surfaced_scars.length) {
559
+ addSurfacedScars(existing.surfaced_scars);
560
+ }
561
+ return {
562
+ sessionId: existing.session_id,
563
+ agent: existing.agent || fallbackAgent,
564
+ linearIssue: existing.linear_issue,
565
+ startedAt,
566
+ };
567
+ }
568
+ /**
569
+ * GIT-20 / OD-558: Check for existing active session and return it if found.
570
+ *
571
+ * Uses the active-sessions registry (hostname+PID) to identify THIS process's
572
+ * session, preventing cross-process session theft on shared filesystems.
573
+ * Falls back to legacy active-session.json for backward compatibility.
574
+ */
575
+ function checkExistingSession(agent, force) {
576
+ if (force) {
577
+ console.error("[session_start] force=true, skipping active session guard");
578
+ return null;
579
+ }
580
+ try {
581
+ // GIT-23: Migrate from old active-session.json format if needed
582
+ migrateFromLegacy();
583
+ // GIT-20: Prune stale sessions from crashed/dead containers
584
+ pruneStale();
585
+ // GIT-20: Check registry for THIS process's session (hostname + PID match)
586
+ const mySession = findSessionByHostPid(os.hostname(), process.pid);
587
+ if (mySession) {
588
+ console.error(`[session_start] Found own session in registry: ${mySession.session_id} (host: ${mySession.hostname}, pid: ${mySession.pid})`);
589
+ const data = readSessionFile(mySession.session_id);
590
+ if (data && data.session_id) {
591
+ console.error(`[session_start] Resuming own session: ${mySession.session_id}`);
592
+ return restoreSessionState(data, agent);
593
+ }
594
+ // Registry entry exists but session file is missing — fall through to create new
595
+ console.warn(`[session_start] Registry entry found but session file missing for ${mySession.session_id}`);
596
+ }
597
+ // Legacy active-session.json fallback removed — per-session dirs + registry
598
+ // are the source of truth (Phase 1 multi-session isolation)
599
+ }
600
+ catch (error) {
601
+ console.error("[session_start] Error checking existing sessions:", error);
602
+ }
603
+ return null;
604
+ }
605
+ /**
606
+ * GIT-20: Write session state to per-session directory, legacy file, and registry.
607
+ * Consolidates write logic used by session_start (main + free) and session_refresh.
608
+ */
609
+ function writeSessionFiles(sessionId, agent, project, surfacedScars, threads, recordingPath, isRefresh) {
610
+ const gitmemDir = path.join(process.cwd(), ".gitmem");
611
+ if (!fs.existsSync(gitmemDir)) {
612
+ fs.mkdirSync(gitmemDir, { recursive: true });
613
+ }
614
+ setGitmemDir(gitmemDir);
615
+ const data = {
616
+ session_id: sessionId,
617
+ agent,
618
+ started_at: new Date().toISOString(),
619
+ project,
620
+ hostname: os.hostname(),
621
+ pid: process.pid,
622
+ surfaced_scars: surfacedScars,
623
+ threads,
624
+ ...(recordingPath && { recording_path: recordingPath }),
625
+ ...(isRefresh && { last_refreshed: new Date().toISOString() }),
626
+ };
627
+ // 1. Per-session directory (GIT-20)
628
+ try {
629
+ const sessionFilePath = getSessionPath(sessionId, "session.json");
630
+ fs.writeFileSync(sessionFilePath, JSON.stringify(data, null, 2));
631
+ console.error(`[session_start] Session state written to ${sessionFilePath}`);
632
+ }
633
+ catch (error) {
634
+ console.warn("[session_start] Failed to write per-session file:", error);
635
+ }
636
+ // Legacy active-session.json write removed — per-session dir is the source of truth
637
+ // 3. Register in active-sessions registry (skip on refresh — already registered)
638
+ if (!isRefresh) {
639
+ registerSession({
640
+ session_id: sessionId,
641
+ agent,
642
+ started_at: data.started_at,
643
+ hostname: os.hostname(),
644
+ pid: process.pid,
645
+ project,
646
+ });
647
+ }
648
+ // 4. Threads file — merge with existing to preserve mid-session creations AND resolutions.
649
+ // mergeThreadStates prefers resolved over open (so local resolve_thread calls survive
650
+ // even if Supabase still has the thread as "open" from an older/unclosed session).
651
+ // It also preserves local-only threads (created mid-session via create_thread).
652
+ const existingFileThreads = loadThreadsFile();
653
+ const merged = existingFileThreads.length > 0
654
+ ? mergeThreadStates(threads, existingFileThreads)
655
+ : threads;
656
+ if (merged.length > 0) {
657
+ saveThreadsFile(merged);
658
+ }
659
+ return merged;
660
+ }
661
+ /**
662
+ * Format pre-formatted display string for session_start/session_refresh results.
663
+ * Agents echo this verbatim for consistent CLI output.
664
+ */
665
+ function formatStartDisplay(result, displayInfoMap) {
666
+ const lines = [];
667
+ // Header
668
+ const label = result.refreshed ? "SESSION REFRESH" : (result.resumed ? "SESSION RESUMED" : "SESSION START");
669
+ lines.push(`## ${label} — ACTIVE`);
670
+ lines.push(`**Session:** \`${result.session_id.slice(0, 8)}\` | **Agent:** ${result.agent}`);
671
+ // Last session
672
+ if (result.last_session) {
673
+ const title = result.last_session.title.length > 70
674
+ ? result.last_session.title.slice(0, 67) + "..."
675
+ : result.last_session.title;
676
+ lines.push("");
677
+ lines.push(`### Last Session`);
678
+ lines.push(`"${title}" (${result.last_session.date})`);
679
+ if (result.last_session.key_decisions?.length) {
680
+ for (const d of result.last_session.key_decisions.slice(0, 3)) {
681
+ lines.push(`- Decision: ${d}`);
682
+ }
683
+ }
684
+ }
685
+ // Open threads (Phase 6: enriched with vitality info when available)
686
+ if (result.open_threads?.length) {
687
+ lines.push("");
688
+ lines.push(`### Open Threads (${result.open_threads.length})`);
689
+ for (const t of result.open_threads.slice(0, 7)) {
690
+ const text = t.text.length > 55 ? t.text.slice(0, 52) + "..." : t.text;
691
+ const info = displayInfoMap?.get(t.id);
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*`);
717
+ }
718
+ lines.push(`\nUse \`promote_suggestion\` or \`dismiss_suggestion\` to manage.`);
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}`);
728
+ }
729
+ }
730
+ // Recent decisions
731
+ if (result.recent_decisions?.length) {
732
+ lines.push("");
733
+ lines.push(`### Recent Decisions (${result.recent_decisions.length})`);
734
+ for (const d of result.recent_decisions.slice(0, 3)) {
735
+ lines.push(`- ${d.title} *(${d.date})*`);
736
+ }
737
+ }
738
+ // Recent wins
739
+ if (result.recent_wins?.length) {
740
+ lines.push("");
741
+ lines.push(`### Recent Wins (${result.recent_wins.length})`);
742
+ for (const w of result.recent_wins.slice(0, 3)) {
743
+ lines.push(`- ${w.title} *(${w.date})*`);
744
+ }
745
+ }
746
+ return lines.join("\n");
747
+ }
748
+ /**
749
+ * Execute session_start tool
750
+ *
751
+ * OD-489: Returns detailed performance breakdown for test harness validation.
752
+ * Key metrics: network_calls_made, fully_local, breakdown per component.
753
+ *
754
+ * OD-558: Guards against overwriting existing active sessions.
755
+ * Returns existing session if active-session.json exists (idempotent).
756
+ * Pass force=true to override.
757
+ */
758
+ export async function sessionStart(params) {
759
+ const timer = new Timer();
760
+ const metricsId = uuidv4();
761
+ // 1. Detect agent (or use provided)
762
+ const env = detectAgent();
763
+ const agent = params.agent_identity || env.agent;
764
+ const project = params.project || "orchestra_dev";
765
+ // OD-558: Check for existing active session — reuse session_id but still load full context
766
+ const existingSession = checkExistingSession(agent, params.force);
767
+ const isResuming = existingSession !== null;
768
+ // Free tier: all-local path
769
+ if (!hasSupabase()) {
770
+ return sessionStartFree(params, env, agent, project, timer, metricsId, existingSession?.sessionId, existingSession?.startedAt);
771
+ }
772
+ // 2. Load last session first (needed for scar context)
773
+ // OD-489: Track timing and network calls
774
+ const lastSessionResult = await loadLastSession(agent, project);
775
+ const lastSession = lastSessionResult.session;
776
+ // 3. Load scars, decisions, and wins in parallel
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),
782
+ loadRecentDecisions(project, 3),
783
+ loadRecentWins(project, 3, 7),
784
+ ]);
785
+ const scars = scarsResult.scars;
786
+ const decisions = decisionsResult.decisions;
787
+ const wins = winsResult.wins;
788
+ const usedLocalSearch = scarsResult.local_search;
789
+ // OD-552: Build surfaced scar list for tracking
790
+ const surfacedAt = new Date().toISOString();
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)
799
+ let sessionId;
800
+ let sessionCreateResult;
801
+ if (isResuming) {
802
+ sessionId = existingSession.sessionId;
803
+ sessionCreateResult = { session_id: sessionId, latency_ms: 0, network_call: false };
804
+ console.error(`[session_start] Resuming session ${sessionId} — skipping record creation`);
805
+ }
806
+ else {
807
+ sessionCreateResult = await createSessionRecord(agent, project, params.linear_issue);
808
+ sessionId = sessionCreateResult.session_id;
809
+ }
810
+ const latencyMs = timer.stop();
811
+ const memoriesSurfaced = scars.map((s) => s.id);
812
+ const similarityScores = scars.map((s) => s.similarity);
813
+ // OD-534: Extract PROJECT STATE from last session if present
814
+ const projectState = lastSession?.open_threads
815
+ ?.map((t) => typeof t === "string" ? t : t.text)
816
+ .find(t => t.startsWith("PROJECT STATE:"))
817
+ ?.replace(/^PROJECT STATE:\s*/, "");
818
+ // OD-489: Build detailed performance breakdown for test harness
819
+ const breakdown = {
820
+ last_session: buildComponentPerformance(lastSessionResult.latency_ms, "supabase", // Last session always from Supabase (no caching yet)
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"),
823
+ 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
+ };
829
+ // Build performance data with detailed breakdown
830
+ const performance = buildPerformanceData("session_start", latencyMs, scars.length + decisions.length + wins.length + (lastSession ? 1 : 0), {
831
+ memoriesSurfaced,
832
+ similarityScores,
833
+ search_mode: usedLocalSearch ? "local" : "remote",
834
+ breakdown,
835
+ });
836
+ // Capture recording path from Docker entrypoint env var
837
+ const recordingPath = process.env.GITMEM_RECORDING_PATH || undefined;
838
+ const aggregatedThreads = lastSessionResult.aggregated_open_threads;
839
+ const recentlyResolvedThreads = lastSessionResult.recently_resolved_threads;
840
+ const threadDisplayInfo = lastSessionResult.displayInfo;
841
+ // GIT-20: Persist to per-session dir, legacy file, and active-sessions registry
842
+ // writeSessionFiles merges aggregated threads with existing file threads to preserve
843
+ // mid-session creations (e.g. create_thread calls that haven't been session_closed yet)
844
+ let mergedThreads = aggregatedThreads;
845
+ try {
846
+ mergedThreads = writeSessionFiles(sessionId, agent, project, surfacedScars, aggregatedThreads, recordingPath);
847
+ }
848
+ catch (error) {
849
+ console.warn("[session_start] Failed to persist session files:", error);
850
+ }
851
+ // OD-547: Set active session for variant assignment in recall
852
+ // OD-552: Initialize with surfaced scars for auto-bridge at close time
853
+ // 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
855
+ setCurrentSession({
856
+ sessionId,
857
+ linearIssue: params.linear_issue,
858
+ agent,
859
+ startedAt: (isResuming && existingSession?.startedAt) || new Date(),
860
+ surfacedScars,
861
+ threads: mergedThreads,
862
+ });
863
+ const result = {
864
+ session_id: sessionId,
865
+ agent,
866
+ ...(isResuming && { resumed: true }),
867
+ detected_environment: env,
868
+ last_session: lastSession,
869
+ ...(projectState && { project_state: projectState }), // OD-534
870
+ ...(mergedThreads.length > 0 && {
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,
881
+ recent_decisions: decisions,
882
+ ...(wins.length > 0 && { recent_wins: wins }),
883
+ ...(recordingPath && { recording_path: recordingPath }),
884
+ performance,
885
+ };
886
+ // Record metrics
887
+ recordMetrics({
888
+ id: metricsId,
889
+ session_id: sessionId,
890
+ agent: agent,
891
+ tool_name: "session_start",
892
+ query_text: [params.issue_title, params.issue_description].filter(Boolean).join(" ").slice(0, 500),
893
+ tables_searched: usedLocalSearch
894
+ ? ["orchestra_sessions_lite", "orchestra_decisions_lite", "orchestra_learnings_lite"]
895
+ : ["orchestra_sessions_lite", "orchestra_learnings", "orchestra_decisions_lite", "orchestra_learnings_lite"],
896
+ latency_ms: latencyMs,
897
+ result_count: scars.length,
898
+ similarity_scores: similarityScores,
899
+ context_bytes: calculateContextBytes(result),
900
+ phase_tag: "session_start",
901
+ linear_issue: params.linear_issue,
902
+ memories_surfaced: memoriesSurfaced,
903
+ metadata: {
904
+ project,
905
+ has_last_session: !!lastSession,
906
+ scars_count: scars.length,
907
+ decisions_count: decisions.length,
908
+ wins_count: wins.length,
909
+ open_threads_count: lastSessionResult.aggregated_open_threads.length,
910
+ used_local_search: usedLocalSearch, // OD-473: deterministic local search
911
+ decisions_cache_hit: decisionsResult.cache_hit,
912
+ // OD-489: Detailed instrumentation
913
+ network_calls_made: performance.network_calls_made,
914
+ fully_local: performance.fully_local,
915
+ },
916
+ }).catch(() => { });
917
+ // Phase 6: Build display info map for enriched thread rendering
918
+ const displayInfoMap = new Map();
919
+ for (const info of threadDisplayInfo) {
920
+ displayInfoMap.set(info.thread.id, info);
921
+ }
922
+ result.display = formatStartDisplay(result, displayInfoMap);
923
+ // Write display to per-session dir
924
+ try {
925
+ const sessionFilePath = getSessionPath(sessionId, "session.json");
926
+ const sessionData = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
927
+ sessionData.display = result.display;
928
+ fs.writeFileSync(sessionFilePath, JSON.stringify(sessionData, null, 2));
929
+ }
930
+ catch { /* non-critical */ }
931
+ return result;
932
+ }
933
+ export async function sessionRefresh(params) {
934
+ const timer = new Timer();
935
+ const metricsId = uuidv4();
936
+ // 1. Get active session — in-memory first, then file fallback
937
+ const currentSession = getCurrentSession();
938
+ let sessionId;
939
+ let agent;
940
+ let project;
941
+ if (currentSession) {
942
+ sessionId = currentSession.sessionId;
943
+ agent = currentSession.agent || "CLI";
944
+ project = params.project || "orchestra_dev";
945
+ }
946
+ else {
947
+ // GIT-20: Fallback — check registry for this process, then legacy file
948
+ const mySession = findSessionByHostPid(os.hostname(), process.pid);
949
+ let raw = null;
950
+ if (mySession) {
951
+ raw = readSessionFile(mySession.session_id);
952
+ }
953
+ if (!raw || !raw.session_id) {
954
+ return {
955
+ session_id: "",
956
+ agent: "CLI",
957
+ refreshed: true,
958
+ message: "No active session — call session_start first",
959
+ performance: buildPerformanceData("session_refresh", timer.stop(), 0),
960
+ };
961
+ }
962
+ sessionId = raw.session_id;
963
+ agent = raw.agent || "CLI";
964
+ project = params.project || raw.project || "orchestra_dev";
965
+ }
966
+ // Free tier: all-local path (reuse session_start free path)
967
+ if (!hasSupabase()) {
968
+ return {
969
+ session_id: sessionId,
970
+ agent,
971
+ refreshed: true,
972
+ message: "Free tier — limited context available. Use recall for scar queries.",
973
+ performance: buildPerformanceData("session_refresh", timer.stop(), 0),
974
+ };
975
+ }
976
+ // 2. Run context pipeline in parallel (same as session_start lines 735-752)
977
+ const lastSessionResult = await loadLastSession(agent, project);
978
+ const lastSession = lastSessionResult.session;
979
+ const [scarsResult, decisionsResult, winsResult] = await Promise.all([
980
+ queryRelevantScars(undefined, undefined, undefined, project, lastSession),
981
+ loadRecentDecisions(project, 3),
982
+ loadRecentWins(project, 3, 7),
983
+ ]);
984
+ const scars = scarsResult.scars;
985
+ const decisions = decisionsResult.decisions;
986
+ const wins = winsResult.wins;
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);
999
+ const refreshAggregatedThreads = lastSessionResult.aggregated_open_threads;
1000
+ const recentlyResolvedThreads = lastSessionResult.recently_resolved_threads;
1001
+ const refreshDisplayInfo = lastSessionResult.displayInfo;
1002
+ // 4. Extract PROJECT STATE (OD-534)
1003
+ const projectState = lastSession?.open_threads
1004
+ ?.map((t) => typeof t === "string" ? t : t.text)
1005
+ .find(t => t.startsWith("PROJECT STATE:"))
1006
+ ?.replace(/^PROJECT STATE:\s*/, "");
1007
+ // 5. Build performance breakdown
1008
+ const latencyMs = timer.stop();
1009
+ const breakdown = {
1010
+ 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
+ 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
+ };
1015
+ const memoriesSurfaced = scars.map((s) => s.id);
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 });
1018
+ const recordingPath = process.env.GITMEM_RECORDING_PATH || undefined;
1019
+ const result = {
1020
+ session_id: sessionId,
1021
+ agent,
1022
+ refreshed: true,
1023
+ detected_environment: detectAgent(),
1024
+ last_session: lastSession,
1025
+ ...(projectState && { project_state: projectState }),
1026
+ // open_threads and setCurrentSession filled after merge below
1027
+ relevant_scars: scars,
1028
+ recent_decisions: decisions,
1029
+ ...(wins.length > 0 && { recent_wins: wins }),
1030
+ ...(recordingPath && { recording_path: recordingPath }),
1031
+ performance,
1032
+ };
1033
+ // GIT-20: Update per-session dir and legacy file with refreshed context
1034
+ // writeSessionFiles merges with existing file threads to preserve mid-session creations
1035
+ let refreshMergedThreads = refreshAggregatedThreads;
1036
+ try {
1037
+ const allSurfacedScars = [...(Array.isArray(getSurfacedScars()) ? getSurfacedScars() : []), ...newSurfacedScars];
1038
+ refreshMergedThreads = writeSessionFiles(sessionId, agent, project, allSurfacedScars, refreshAggregatedThreads, recordingPath, true);
1039
+ console.error(`[session_refresh] Context refreshed for session ${sessionId}`);
1040
+ }
1041
+ catch (error) {
1042
+ console.warn("[session_refresh] Failed to update session files:", error);
1043
+ }
1044
+ // Add merged threads to result
1045
+ if (refreshMergedThreads.length > 0) {
1046
+ result.open_threads = refreshMergedThreads;
1047
+ }
1048
+ if (recentlyResolvedThreads.length > 0) {
1049
+ result.recently_resolved = recentlyResolvedThreads;
1050
+ }
1051
+ // 7. Update in-memory session state with merged threads
1052
+ setCurrentSession({
1053
+ sessionId,
1054
+ agent,
1055
+ startedAt: currentSession?.startedAt || new Date(),
1056
+ surfacedScars: [...(currentSession?.surfacedScars || []), ...newSurfacedScars],
1057
+ threads: refreshMergedThreads,
1058
+ linearIssue: currentSession?.linearIssue,
1059
+ });
1060
+ // Record metrics
1061
+ recordMetrics({
1062
+ id: metricsId,
1063
+ session_id: sessionId,
1064
+ agent: agent,
1065
+ tool_name: "session_refresh",
1066
+ query_text: "mid-session context refresh",
1067
+ tables_searched: usedLocalSearch
1068
+ ? ["orchestra_sessions_lite", "orchestra_decisions_lite", "orchestra_learnings_lite"]
1069
+ : ["orchestra_sessions_lite", "orchestra_learnings", "orchestra_decisions_lite", "orchestra_learnings_lite"],
1070
+ latency_ms: latencyMs,
1071
+ result_count: scars.length,
1072
+ similarity_scores: similarityScores,
1073
+ context_bytes: calculateContextBytes(result),
1074
+ phase_tag: "session_refresh",
1075
+ memories_surfaced: memoriesSurfaced,
1076
+ metadata: {
1077
+ project,
1078
+ has_last_session: !!lastSession,
1079
+ scars_count: scars.length,
1080
+ decisions_count: decisions.length,
1081
+ wins_count: wins.length,
1082
+ open_threads_count: refreshMergedThreads.length,
1083
+ used_local_search: usedLocalSearch,
1084
+ network_calls_made: performance.network_calls_made,
1085
+ fully_local: performance.fully_local,
1086
+ },
1087
+ }).catch(() => { });
1088
+ // Phase 6: Build display info map for enriched thread rendering
1089
+ const refreshDisplayInfoMap = new Map();
1090
+ for (const info of refreshDisplayInfo) {
1091
+ refreshDisplayInfoMap.set(info.thread.id, info);
1092
+ }
1093
+ result.display = formatStartDisplay(result, refreshDisplayInfoMap);
1094
+ // Write display to per-session dir
1095
+ try {
1096
+ const sessionFilePath = getSessionPath(sessionId, "session.json");
1097
+ const sessionData = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
1098
+ sessionData.display = result.display;
1099
+ fs.writeFileSync(sessionFilePath, JSON.stringify(sessionData, null, 2));
1100
+ }
1101
+ catch { /* non-critical */ }
1102
+ return result;
1103
+ }
1104
+ //# sourceMappingURL=session-start.js.map