gitmem-mcp 1.0.0 → 1.0.1

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