gitmem-mcp 0.2.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 (249) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/CLAUDE.md.template +63 -55
  3. package/README.md +149 -120
  4. package/bin/gitmem.js +377 -25
  5. package/bin/init-wizard.js +642 -0
  6. package/bin/uninstall.js +288 -0
  7. package/dist/commands/check.js +20 -20
  8. package/dist/commands/check.js.map +1 -1
  9. package/dist/constants/closing-questions.d.ts +6 -0
  10. package/dist/constants/closing-questions.d.ts.map +1 -1
  11. package/dist/constants/closing-questions.js +65 -0
  12. package/dist/constants/closing-questions.js.map +1 -1
  13. package/dist/hooks/format-utils.d.ts +52 -0
  14. package/dist/hooks/format-utils.d.ts.map +1 -0
  15. package/dist/hooks/format-utils.js +89 -0
  16. package/dist/hooks/format-utils.js.map +1 -0
  17. package/dist/hooks/quick-retrieve.d.ts +30 -0
  18. package/dist/hooks/quick-retrieve.d.ts.map +1 -0
  19. package/dist/hooks/quick-retrieve.js +149 -0
  20. package/dist/hooks/quick-retrieve.js.map +1 -0
  21. package/dist/index.js +0 -0
  22. package/dist/schemas/active-sessions.d.ts +8 -8
  23. package/dist/schemas/analyze.d.ts +3 -3
  24. package/dist/schemas/common.d.ts +2 -2
  25. package/dist/schemas/common.d.ts.map +1 -1
  26. package/dist/schemas/common.js +1 -1
  27. package/dist/schemas/common.js.map +1 -1
  28. package/dist/schemas/create-decision.d.ts +3 -3
  29. package/dist/schemas/create-learning.d.ts +13 -13
  30. package/dist/schemas/log.d.ts +3 -3
  31. package/dist/schemas/prepare-context.d.ts +3 -3
  32. package/dist/schemas/recall.d.ts +3 -3
  33. package/dist/schemas/record-scar-usage-batch.d.ts +8 -3
  34. package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -1
  35. package/dist/schemas/record-scar-usage.d.ts +3 -0
  36. package/dist/schemas/record-scar-usage.d.ts.map +1 -1
  37. package/dist/schemas/record-scar-usage.js +1 -0
  38. package/dist/schemas/record-scar-usage.js.map +1 -1
  39. package/dist/schemas/registry.d.ts +18 -0
  40. package/dist/schemas/registry.d.ts.map +1 -0
  41. package/dist/schemas/registry.js +158 -0
  42. package/dist/schemas/registry.js.map +1 -0
  43. package/dist/schemas/save-transcript.d.ts +3 -3
  44. package/dist/schemas/search-transcripts.d.ts +33 -0
  45. package/dist/schemas/search-transcripts.d.ts.map +1 -0
  46. package/dist/schemas/search-transcripts.js +26 -0
  47. package/dist/schemas/search-transcripts.js.map +1 -0
  48. package/dist/schemas/search.d.ts +3 -3
  49. package/dist/schemas/session-close.d.ts +43 -15
  50. package/dist/schemas/session-close.d.ts.map +1 -1
  51. package/dist/schemas/session-close.js +7 -2
  52. package/dist/schemas/session-close.js.map +1 -1
  53. package/dist/schemas/session-start.d.ts +3 -3
  54. package/dist/schemas/thread.d.ts +3 -3
  55. package/dist/server.d.ts.map +1 -1
  56. package/dist/server.js +82 -28
  57. package/dist/server.js.map +1 -1
  58. package/dist/services/active-sessions.d.ts +2 -1
  59. package/dist/services/active-sessions.d.ts.map +1 -1
  60. package/dist/services/active-sessions.js +130 -84
  61. package/dist/services/active-sessions.js.map +1 -1
  62. package/dist/services/analytics.d.ts.map +1 -1
  63. package/dist/services/analytics.js +1 -0
  64. package/dist/services/analytics.js.map +1 -1
  65. package/dist/services/behavioral-decay.d.ts +40 -0
  66. package/dist/services/behavioral-decay.d.ts.map +1 -0
  67. package/dist/services/behavioral-decay.js +110 -0
  68. package/dist/services/behavioral-decay.js.map +1 -0
  69. package/dist/services/bm25.d.ts +39 -0
  70. package/dist/services/bm25.d.ts.map +1 -0
  71. package/dist/services/bm25.js +132 -0
  72. package/dist/services/bm25.js.map +1 -0
  73. package/dist/services/cache.d.ts.map +1 -1
  74. package/dist/services/cache.js +9 -8
  75. package/dist/services/cache.js.map +1 -1
  76. package/dist/services/cache.test.js +17 -17
  77. package/dist/services/cache.test.js.map +1 -1
  78. package/dist/services/compliance-validator.d.ts.map +1 -1
  79. package/dist/services/compliance-validator.js +12 -1
  80. package/dist/services/compliance-validator.js.map +1 -1
  81. package/dist/services/display-protocol.d.ts +31 -0
  82. package/dist/services/display-protocol.d.ts.map +1 -0
  83. package/dist/services/display-protocol.js +73 -0
  84. package/dist/services/display-protocol.js.map +1 -0
  85. package/dist/services/effect-tracker.d.ts +81 -0
  86. package/dist/services/effect-tracker.d.ts.map +1 -0
  87. package/dist/services/effect-tracker.js +181 -0
  88. package/dist/services/effect-tracker.js.map +1 -0
  89. package/dist/services/file-lock.d.ts +31 -0
  90. package/dist/services/file-lock.d.ts.map +1 -0
  91. package/dist/services/file-lock.js +124 -0
  92. package/dist/services/file-lock.js.map +1 -0
  93. package/dist/services/gitmem-dir.d.ts +7 -0
  94. package/dist/services/gitmem-dir.d.ts.map +1 -1
  95. package/dist/services/gitmem-dir.js +21 -0
  96. package/dist/services/gitmem-dir.js.map +1 -1
  97. package/dist/services/local-file-storage.d.ts +3 -2
  98. package/dist/services/local-file-storage.d.ts.map +1 -1
  99. package/dist/services/local-file-storage.js +30 -43
  100. package/dist/services/local-file-storage.js.map +1 -1
  101. package/dist/services/local-vector-search.d.ts +10 -9
  102. package/dist/services/local-vector-search.d.ts.map +1 -1
  103. package/dist/services/local-vector-search.js +28 -23
  104. package/dist/services/local-vector-search.js.map +1 -1
  105. package/dist/services/metrics.d.ts +7 -2
  106. package/dist/services/metrics.d.ts.map +1 -1
  107. package/dist/services/metrics.js +41 -33
  108. package/dist/services/metrics.js.map +1 -1
  109. package/dist/services/session-state.d.ts +8 -0
  110. package/dist/services/session-state.d.ts.map +1 -1
  111. package/dist/services/session-state.js +9 -2
  112. package/dist/services/session-state.js.map +1 -1
  113. package/dist/services/startup.d.ts +12 -13
  114. package/dist/services/startup.d.ts.map +1 -1
  115. package/dist/services/startup.js +104 -57
  116. package/dist/services/startup.js.map +1 -1
  117. package/dist/services/supabase-client.d.ts +2 -1
  118. package/dist/services/supabase-client.d.ts.map +1 -1
  119. package/dist/services/supabase-client.js +22 -16
  120. package/dist/services/supabase-client.js.map +1 -1
  121. package/dist/services/thread-dedup.d.ts +9 -0
  122. package/dist/services/thread-dedup.d.ts.map +1 -1
  123. package/dist/services/thread-dedup.js +27 -0
  124. package/dist/services/thread-dedup.js.map +1 -1
  125. package/dist/services/thread-manager.d.ts.map +1 -1
  126. package/dist/services/thread-manager.js +38 -16
  127. package/dist/services/thread-manager.js.map +1 -1
  128. package/dist/services/thread-suggestions.d.ts.map +1 -1
  129. package/dist/services/thread-suggestions.js +1 -1
  130. package/dist/services/thread-suggestions.js.map +1 -1
  131. package/dist/services/thread-supabase.d.ts +0 -1
  132. package/dist/services/thread-supabase.d.ts.map +1 -1
  133. package/dist/services/thread-supabase.js +83 -54
  134. package/dist/services/thread-supabase.js.map +1 -1
  135. package/dist/services/timezone.d.ts.map +1 -1
  136. package/dist/services/timezone.js +1 -0
  137. package/dist/services/timezone.js.map +1 -1
  138. package/dist/services/transcript-chunker.d.ts.map +1 -1
  139. package/dist/services/transcript-chunker.js +18 -4
  140. package/dist/services/transcript-chunker.js.map +1 -1
  141. package/dist/services/variant-generation.d.ts +41 -0
  142. package/dist/services/variant-generation.d.ts.map +1 -0
  143. package/dist/services/variant-generation.js +263 -0
  144. package/dist/services/variant-generation.js.map +1 -0
  145. package/dist/tools/absorb-observations.d.ts.map +1 -1
  146. package/dist/tools/absorb-observations.js +9 -0
  147. package/dist/tools/absorb-observations.js.map +1 -1
  148. package/dist/tools/analyze.d.ts.map +1 -1
  149. package/dist/tools/analyze.js +13 -2
  150. package/dist/tools/analyze.js.map +1 -1
  151. package/dist/tools/archive-learning.d.ts +28 -0
  152. package/dist/tools/archive-learning.d.ts.map +1 -0
  153. package/dist/tools/archive-learning.js +81 -0
  154. package/dist/tools/archive-learning.js.map +1 -0
  155. package/dist/tools/cleanup-threads.d.ts +1 -0
  156. package/dist/tools/cleanup-threads.d.ts.map +1 -1
  157. package/dist/tools/cleanup-threads.js +111 -18
  158. package/dist/tools/cleanup-threads.js.map +1 -1
  159. package/dist/tools/confirm-scars.d.ts.map +1 -1
  160. package/dist/tools/confirm-scars.js +8 -2
  161. package/dist/tools/confirm-scars.js.map +1 -1
  162. package/dist/tools/create-decision.d.ts.map +1 -1
  163. package/dist/tools/create-decision.js +11 -8
  164. package/dist/tools/create-decision.js.map +1 -1
  165. package/dist/tools/create-learning.d.ts.map +1 -1
  166. package/dist/tools/create-learning.js +35 -11
  167. package/dist/tools/create-learning.js.map +1 -1
  168. package/dist/tools/create-linear-issue.d.ts +18 -0
  169. package/dist/tools/create-linear-issue.d.ts.map +1 -0
  170. package/dist/tools/create-linear-issue.js +197 -0
  171. package/dist/tools/create-linear-issue.js.map +1 -0
  172. package/dist/tools/create-thread.d.ts +2 -1
  173. package/dist/tools/create-thread.d.ts.map +1 -1
  174. package/dist/tools/create-thread.js +9 -4
  175. package/dist/tools/create-thread.js.map +1 -1
  176. package/dist/tools/definitions.d.ts +785 -34
  177. package/dist/tools/definitions.d.ts.map +1 -1
  178. package/dist/tools/definitions.js +239 -95
  179. package/dist/tools/definitions.js.map +1 -1
  180. package/dist/tools/dismiss-suggestion.d.ts +1 -0
  181. package/dist/tools/dismiss-suggestion.d.ts.map +1 -1
  182. package/dist/tools/dismiss-suggestion.js +4 -0
  183. package/dist/tools/dismiss-suggestion.js.map +1 -1
  184. package/dist/tools/graph-traverse.d.ts +1 -0
  185. package/dist/tools/graph-traverse.d.ts.map +1 -1
  186. package/dist/tools/graph-traverse.js +24 -9
  187. package/dist/tools/graph-traverse.js.map +1 -1
  188. package/dist/tools/list-threads.d.ts.map +1 -1
  189. package/dist/tools/list-threads.js +49 -5
  190. package/dist/tools/list-threads.js.map +1 -1
  191. package/dist/tools/log.d.ts +1 -0
  192. package/dist/tools/log.d.ts.map +1 -1
  193. package/dist/tools/log.js +84 -17
  194. package/dist/tools/log.js.map +1 -1
  195. package/dist/tools/prepare-context.d.ts +1 -0
  196. package/dist/tools/prepare-context.d.ts.map +1 -1
  197. package/dist/tools/prepare-context.js +15 -85
  198. package/dist/tools/prepare-context.js.map +1 -1
  199. package/dist/tools/promote-suggestion.d.ts +1 -0
  200. package/dist/tools/promote-suggestion.d.ts.map +1 -1
  201. package/dist/tools/promote-suggestion.js +5 -0
  202. package/dist/tools/promote-suggestion.js.map +1 -1
  203. package/dist/tools/recall.d.ts +2 -0
  204. package/dist/tools/recall.d.ts.map +1 -1
  205. package/dist/tools/recall.js +43 -10
  206. package/dist/tools/recall.js.map +1 -1
  207. package/dist/tools/recall.test.js +6 -6
  208. package/dist/tools/recall.test.js.map +1 -1
  209. package/dist/tools/record-scar-usage-batch.d.ts.map +1 -1
  210. package/dist/tools/record-scar-usage-batch.js +13 -0
  211. package/dist/tools/record-scar-usage-batch.js.map +1 -1
  212. package/dist/tools/record-scar-usage.d.ts.map +1 -1
  213. package/dist/tools/record-scar-usage.js +6 -0
  214. package/dist/tools/record-scar-usage.js.map +1 -1
  215. package/dist/tools/resolve-thread.d.ts.map +1 -1
  216. package/dist/tools/resolve-thread.js +57 -6
  217. package/dist/tools/resolve-thread.js.map +1 -1
  218. package/dist/tools/save-transcript.d.ts +1 -0
  219. package/dist/tools/save-transcript.d.ts.map +1 -1
  220. package/dist/tools/save-transcript.js +3 -1
  221. package/dist/tools/save-transcript.js.map +1 -1
  222. package/dist/tools/search-transcripts.d.ts +44 -0
  223. package/dist/tools/search-transcripts.d.ts.map +1 -0
  224. package/dist/tools/search-transcripts.js +158 -0
  225. package/dist/tools/search-transcripts.js.map +1 -0
  226. package/dist/tools/search.d.ts +1 -0
  227. package/dist/tools/search.d.ts.map +1 -1
  228. package/dist/tools/search.js +74 -3
  229. package/dist/tools/search.js.map +1 -1
  230. package/dist/tools/session-close.d.ts.map +1 -1
  231. package/dist/tools/session-close.js +563 -326
  232. package/dist/tools/session-close.js.map +1 -1
  233. package/dist/tools/session-start.d.ts +10 -6
  234. package/dist/tools/session-start.d.ts.map +1 -1
  235. package/dist/tools/session-start.js +317 -426
  236. package/dist/tools/session-start.js.map +1 -1
  237. package/dist/types/index.d.ts +37 -4
  238. package/dist/types/index.d.ts.map +1 -1
  239. package/hooks/.claude-plugin/plugin.json +8 -0
  240. package/hooks/README.md +107 -0
  241. package/hooks/hooks/hooks.json +123 -0
  242. package/hooks/scripts/auto-retrieve-hook.sh +163 -0
  243. package/hooks/scripts/post-tool-use.sh +112 -0
  244. package/hooks/scripts/recall-check.sh +213 -0
  245. package/hooks/scripts/session-close-check.sh +116 -0
  246. package/hooks/scripts/session-start.sh +233 -0
  247. package/hooks/tests/test-hooks.sh +577 -0
  248. package/package.json +4 -2
  249. package/schema/setup.sql +1 -1
@@ -12,12 +12,16 @@ import * as supabase from "../services/supabase-client.js";
12
12
  import { embed, isEmbeddingAvailable } from "../services/embedding.js";
13
13
  import { hasSupabase } from "../services/tier.js";
14
14
  import { getStorage } from "../services/storage.js";
15
- import { clearCurrentSession, getSurfacedScars, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js"; // OD-547, OD-552, v2 Phase 2
16
- import { normalizeThreads, mergeThreadStates, migrateStringThread } from "../services/thread-manager.js"; // OD-thread-lifecycle
15
+ import { clearCurrentSession, getSurfacedScars, getConfirmations, getObservations, getChildren, getThreads, getSessionActivity } from "../services/session-state.js"; // OD-547, OD-552, v2 Phase 2
16
+ import { normalizeThreads, mergeThreadStates, migrateStringThread, saveThreadsFile } from "../services/thread-manager.js"; // OD-thread-lifecycle
17
+ import { deduplicateThreadList } from "../services/thread-dedup.js"; // OD-641
17
18
  import { syncThreadsToSupabase, loadOpenThreadEmbeddings } from "../services/thread-supabase.js"; // OD-624
18
19
  import { validateSessionClose, buildCloseCompliance, } from "../services/compliance-validator.js";
20
+ import { normalizeReflectionKeys } from "../constants/closing-questions.js";
19
21
  import { Timer, recordMetrics, buildPerformanceData, updateRelevanceData, } from "../services/metrics.js";
22
+ import { wrapDisplay, truncate } from "../services/display-protocol.js";
20
23
  import { recordScarUsageBatch } from "./record-scar-usage-batch.js";
24
+ import { getEffectTracker } from "../services/effect-tracker.js";
21
25
  import { saveTranscript } from "./save-transcript.js";
22
26
  import { processTranscript } from "../services/transcript-chunker.js";
23
27
  import * as fs from "fs";
@@ -26,6 +30,28 @@ import * as os from "os";
26
30
  import { getGitmemPath, getGitmemDir, getSessionPath } from "../services/gitmem-dir.js";
27
31
  import { unregisterSession, findSessionByHostPid } from "../services/active-sessions.js";
28
32
  import { loadSuggestions, saveSuggestions, detectSuggestedThreads, loadRecentSessionEmbeddings } from "../services/thread-suggestions.js";
33
+ /**
34
+ * Normalize scars_applied to string[].
35
+ * Handles both string (prose answer from agents) and string[] (schema-correct array).
36
+ * When agents write Q6 as prose, splits on common delimiters.
37
+ */
38
+ function normalizeScarsApplied(scarsApplied) {
39
+ if (!scarsApplied)
40
+ return [];
41
+ if (Array.isArray(scarsApplied))
42
+ return scarsApplied;
43
+ const trimmed = scarsApplied.trim();
44
+ if (!trimmed)
45
+ return [];
46
+ const parts = trimmed.split(/(?:\.\s+|\;\s*|\s+—\s+)/).filter(p => p.trim().length > 0);
47
+ return parts.length > 0 ? parts : [trimmed];
48
+ }
49
+ /**
50
+ * Count scars applied from closing_reflection.scars_applied.
51
+ */
52
+ function countScarsApplied(scarsApplied) {
53
+ return normalizeScarsApplied(scarsApplied).length;
54
+ }
29
55
  /**
30
56
  * Find the most recently modified transcript file in Claude Code projects directory
31
57
  * OD-538: Search by recency, not by filename matching (supports post-compaction)
@@ -121,7 +147,7 @@ async function sessionCloseFree(params, timer) {
121
147
  questions_answered_by_agent: !!params.closing_reflection,
122
148
  human_asked_for_corrections: !!params.human_corrections || params.human_corrections === "",
123
149
  learnings_stored: learningsCount,
124
- scars_applied: params.closing_reflection?.scars_applied?.length || 0,
150
+ scars_applied: countScarsApplied(params.closing_reflection?.scars_applied),
125
151
  };
126
152
  try {
127
153
  // Load existing session if available
@@ -146,8 +172,8 @@ async function sessionCloseFree(params, timer) {
146
172
  if (params.open_threads && params.open_threads.length > 0) {
147
173
  const normalized = normalizeThreads(params.open_threads, params.session_id);
148
174
  const merged = freeSessionThreads.length > 0
149
- ? mergeThreadStates(normalized, freeSessionThreads)
150
- : normalized;
175
+ ? deduplicateThreadList(mergeThreadStates(normalized, freeSessionThreads))
176
+ : deduplicateThreadList(normalized);
151
177
  sessionData.open_threads = merged;
152
178
  }
153
179
  else if (freeSessionThreads.length > 0) {
@@ -210,66 +236,110 @@ async function sessionCloseFree(params, timer) {
210
236
  };
211
237
  }
212
238
  }
213
- /**
214
- * Build a pre-formatted display string for consistent CLI output.
215
- * Agents echo this string directly instead of formatting ad-hoc.
216
- */
217
- function formatCloseDisplay(sessionId, compliance, params, learningsCount, success, errors) {
239
+ function formatCloseDisplay(sessionId, compliance, params, learningsCount, success, errors, transcriptStatus) {
218
240
  const lines = [];
219
- if (!success) {
220
- lines.push("**Session close FAILED.**");
221
- if (errors?.length) {
222
- for (const e of errors)
223
- lines.push(`- Error: ${e}`);
224
- }
225
- lines.push("");
226
- }
227
241
  // Header
242
+ const B = "\x1b[1m"; // bold on
243
+ const D = "\x1b[2m"; // dim on
244
+ const R = "\x1b[0m"; // reset
228
245
  const closeLabel = compliance.close_type.toUpperCase();
229
- lines.push(`## ${closeLabel} CLOSE — ${success ? "COMPLETE" : "FAILED"}`);
230
- lines.push(`**Session:** \`${sessionId.slice(0, 8)}\` | **Agent:** ${compliance.agent}`);
231
- // Checklist
232
- const check = (ok) => ok ? "done" : "missing";
246
+ const status = success ? "COMPLETE" : "FAILED";
247
+ lines.push(`${B}${closeLabel} CLOSE ${status}${R}`);
248
+ lines.push(`Session ${sessionId.slice(0, 8)} · ${compliance.agent}`);
249
+ if (!success && errors?.length) {
250
+ lines.push("");
251
+ for (const e of errors)
252
+ lines.push(` !! ${e}`);
253
+ }
254
+ // Checklist (compact)
233
255
  lines.push("");
234
- lines.push(`### Checklist`);
256
+ const ok = "\u2713";
257
+ const no = "\u2717";
235
258
  if (compliance.close_type === "standard") {
236
- lines.push(`- [${check(compliance.checklist_displayed)}] Read active-session.json`);
237
- lines.push(`- [${check(compliance.questions_answered_by_agent)}] Agent answered 7 questions`);
238
- lines.push(`- [${check(compliance.human_asked_for_corrections)}] Human asked for corrections`);
239
- lines.push(`- [${check(learningsCount > 0)}] Created learning entries (${learningsCount})`);
240
- lines.push(`- [${check(compliance.scars_applied > 0)}] Recorded scar usage (${compliance.scars_applied})`);
241
- lines.push(`- [${check(success)}] Session persisted`);
259
+ lines.push(` ${ok} Session state read`);
260
+ lines.push(` ${compliance.questions_answered_by_agent ? ok : no} Reflection (9 questions)`);
261
+ lines.push(` ${compliance.human_asked_for_corrections ? ok : no} Human corrections`);
262
+ lines.push(` ${success ? ok : no} Persisted`);
242
263
  }
243
264
  else {
244
- lines.push(`- [${check(success)}] Session persisted`);
245
- lines.push(`- Agent: ${compliance.agent} | Close type: ${compliance.close_type}`);
265
+ lines.push(` ${success ? ok : no} Persisted (${compliance.close_type})`);
266
+ }
267
+ // Decisions table
268
+ if (params.decisions?.length) {
269
+ lines.push("");
270
+ lines.push(`${B}Decisions${R}`);
271
+ for (const d of params.decisions) {
272
+ lines.push(` ${truncate(d.title, 60)}`);
273
+ lines.push(` ${truncate(d.decision, 70)}`);
274
+ }
275
+ }
276
+ // Learnings created
277
+ if (params.learnings_created?.length) {
278
+ lines.push("");
279
+ lines.push(`${B}Learnings (${params.learnings_created.length})${R}`);
280
+ for (const l of params.learnings_created) {
281
+ // learnings_created is (string | Record)[] — show truncated ID or object key
282
+ const label = typeof l === "string" ? (l.length > 12 ? l.slice(0, 8) : l) : String(l);
283
+ lines.push(` ${label}`);
284
+ }
285
+ }
286
+ // Scars applied
287
+ if (params.scars_to_record?.length) {
288
+ const acknowledged = params.scars_to_record.filter(s => s.reference_type !== "none");
289
+ const ignored = params.scars_to_record.length - acknowledged.length;
290
+ lines.push("");
291
+ lines.push(`${B}Scars (${acknowledged.length} applied${ignored > 0 ? `, ${ignored} surfaced-only` : ""})${R}`);
292
+ for (const s of acknowledged) {
293
+ const ref = s.reference_type === "explicit" ? "applied" :
294
+ s.reference_type === "implicit" ? "implicit" :
295
+ s.reference_type === "acknowledged" ? "ack'd" :
296
+ s.reference_type === "refuted" ? "REFUTED" : s.reference_type;
297
+ const id = s.scar_identifier.length > 12 ? s.scar_identifier.slice(0, 8) : s.scar_identifier;
298
+ lines.push(` ${id} ${ref.padEnd(8)} ${truncate(s.reference_context, 50)}`);
299
+ }
300
+ }
301
+ // Transcript status
302
+ if (transcriptStatus) {
303
+ lines.push("");
304
+ lines.push(`### Transcript`);
305
+ if (transcriptStatus.saved) {
306
+ let line = `- [done] Saved (${transcriptStatus.size_kb}KB) -> ${transcriptStatus.path}`;
307
+ if (transcriptStatus.patch_warning) {
308
+ line += ` (warning: session record not updated)`;
309
+ }
310
+ lines.push(line);
311
+ }
312
+ else {
313
+ lines.push(`- [FAILED] ${transcriptStatus.error || "Unknown error"}`);
314
+ }
246
315
  }
247
316
  // Threads summary
248
317
  const threads = params.open_threads || [];
249
318
  if (threads.length > 0) {
250
- const openCount = threads.filter(t => {
251
- if (typeof t === "string")
252
- return true;
253
- return t.status === "open";
254
- }).length;
319
+ const openCount = threads.filter(t => typeof t === "string" || t.status === "open").length;
255
320
  const resolvedCount = threads.length - openCount;
256
321
  lines.push("");
257
- lines.push(`### Threads`);
258
- lines.push(`${openCount} open, ${resolvedCount} resolved, ${threads.length} total`);
322
+ lines.push(`${B}Threads${R}: ${openCount} open${resolvedCount > 0 ? `, ${resolvedCount} resolved` : ""}`);
259
323
  }
260
- // Decisions
261
- if (params.decisions?.length) {
324
+ // Write health (compact)
325
+ const healthSummary = getEffectTracker().formatSummary();
326
+ if (healthSummary && healthSummary !== "No tracked effects this session.") {
262
327
  lines.push("");
263
- lines.push(`### Decisions`);
264
- lines.push(`${params.decisions.length} captured`);
328
+ lines.push(`${B}Write Health${R}`);
329
+ lines.push(healthSummary);
265
330
  }
266
- // Learnings
267
- if (learningsCount > 0) {
268
- lines.push("");
269
- lines.push(`### Learnings`);
270
- lines.push(`${learningsCount} created`);
331
+ // Reflection highlights (Q4 what worked, Q3 do differently)
332
+ if (params.closing_reflection) {
333
+ const r = params.closing_reflection;
334
+ if (r.what_worked) {
335
+ lines.push("");
336
+ lines.push(`${B}What worked${R}: ${truncate(r.what_worked, 80)}`);
337
+ }
338
+ if (r.do_differently) {
339
+ lines.push(`${B}Next time${R}: ${truncate(r.do_differently, 80)}`);
340
+ }
271
341
  }
272
- return lines.join("\n");
342
+ return wrapDisplay(lines.join("\n"));
273
343
  }
274
344
  /**
275
345
  * GIT-21: Clean up all session files for a closed session.
@@ -296,12 +366,339 @@ function cleanupSessionFiles(sessionId) {
296
366
  }
297
367
  // Legacy active-session.json cleanup removed — file is no longer written
298
368
  }
369
+ // UUID and short-ID format validation for session_id (OD-548)
370
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
371
+ const SHORT_ID_REGEX = /^[0-9a-f]{8}$/i;
372
+ function isValidSessionId(id) {
373
+ return UUID_REGEX.test(id) || SHORT_ID_REGEX.test(id);
374
+ }
375
+ /**
376
+ * Build the session data record from params and existing session state.
377
+ * Handles retroactive vs normal mode, reflection, decisions, threads,
378
+ * observations, children, linear_issue, and title updates.
379
+ */
380
+ function buildSessionRecord(params, existingSession, isRetroactive, agentIdentity, closeCompliance, sessionId) {
381
+ let sessionData;
382
+ if (isRetroactive) {
383
+ const now = new Date().toISOString();
384
+ sessionData = {
385
+ id: sessionId,
386
+ agent: agentIdentity,
387
+ project: "default",
388
+ session_title: "Retroactive Session",
389
+ session_date: now,
390
+ created_at: now,
391
+ close_compliance: closeCompliance,
392
+ };
393
+ }
394
+ else {
395
+ // Remove embedding from existing to avoid re-embedding unchanged text
396
+ const { embedding: _embedding, ...existingWithoutEmbedding } = existingSession;
397
+ sessionData = {
398
+ ...existingWithoutEmbedding,
399
+ close_compliance: closeCompliance,
400
+ };
401
+ }
402
+ // Add closing reflection if provided
403
+ if (params.closing_reflection) {
404
+ const reflection = { ...params.closing_reflection };
405
+ if (params.human_corrections) {
406
+ reflection.human_additions = params.human_corrections;
407
+ }
408
+ sessionData.closing_reflection = reflection;
409
+ // OD-666: Distill Q8+Q9 into rapport_summary for cross-agent surfacing
410
+ const q8 = params.closing_reflection.collaborative_dynamic;
411
+ const q9 = params.closing_reflection.rapport_notes;
412
+ if (q8 || q9) {
413
+ const parts = [q8, q9].filter(Boolean);
414
+ sessionData.rapport_summary = parts.join(" | ");
415
+ }
416
+ }
417
+ // Add decisions if provided
418
+ if (params.decisions && params.decisions.length > 0) {
419
+ sessionData.decisions = params.decisions.map((d) => d.title);
420
+ }
421
+ // OD-thread-lifecycle: Normalize and merge open threads
422
+ const sessionThreads = getThreads();
423
+ if (params.open_threads && params.open_threads.length > 0) {
424
+ const normalized = normalizeThreads(params.open_threads, params.session_id);
425
+ const merged = sessionThreads.length > 0
426
+ ? deduplicateThreadList(mergeThreadStates(normalized, sessionThreads))
427
+ : deduplicateThreadList(normalized);
428
+ sessionData.open_threads = merged;
429
+ }
430
+ else if (sessionThreads.length > 0) {
431
+ sessionData.open_threads = sessionThreads;
432
+ }
433
+ // OD-534: If project_state provided, prepend it to open_threads as a ThreadObject
434
+ if (params.project_state) {
435
+ const projectStateText = `PROJECT STATE: ${params.project_state}`;
436
+ const existing = (sessionData.open_threads || []);
437
+ const filtered = existing.filter(t => {
438
+ const text = typeof t === "string" ? t : t.text;
439
+ return !text.startsWith("PROJECT STATE:");
440
+ });
441
+ sessionData.open_threads = [migrateStringThread(projectStateText, params.session_id), ...filtered];
442
+ }
443
+ // v2 Phase 2: Persist observations and children from multi-agent work
444
+ const observations = getObservations();
445
+ if (observations.length > 0) {
446
+ sessionData.task_observations = observations;
447
+ }
448
+ const sessionChildren = getChildren();
449
+ if (sessionChildren.length > 0) {
450
+ sessionData.children = sessionChildren;
451
+ }
452
+ // Add linear issue if provided
453
+ if (params.linear_issue) {
454
+ sessionData.linear_issue = params.linear_issue;
455
+ }
456
+ // Update session title if we have meaningful content
457
+ if (params.closing_reflection?.what_worked || params.decisions?.length) {
458
+ const titleParts = [];
459
+ if (params.linear_issue) {
460
+ titleParts.push(params.linear_issue);
461
+ }
462
+ if (params.decisions?.length) {
463
+ titleParts.push(params.decisions[0].title);
464
+ }
465
+ else if (params.closing_reflection?.what_worked) {
466
+ titleParts.push(params.closing_reflection.what_worked.slice(0, 50));
467
+ }
468
+ if (titleParts.length > 0 &&
469
+ (sessionData.session_title === "Interactive Session" ||
470
+ sessionData.session_title === "Retroactive Session")) {
471
+ sessionData.session_title = titleParts.join(" - ");
472
+ }
473
+ }
474
+ return sessionData;
475
+ }
476
+ /**
477
+ * Capture and save the conversation transcript for a session.
478
+ * Returns transcript status and optionally the Claude Code session ID.
479
+ */
480
+ async function captureSessionTranscript(sessionId, params, existingSession, isRetroactive) {
481
+ try {
482
+ let transcriptFilePath = null;
483
+ // Option 1: Explicit transcript path provided
484
+ if (params.transcript_path) {
485
+ if (fs.existsSync(params.transcript_path)) {
486
+ transcriptFilePath = params.transcript_path;
487
+ console.error(`[session_close] Using explicit transcript path: ${transcriptFilePath}`);
488
+ }
489
+ else {
490
+ console.warn(`[session_close] Explicit transcript path does not exist: ${params.transcript_path}`);
491
+ }
492
+ }
493
+ // Option 2: Auto-detect by searching for most recent transcript
494
+ if (!transcriptFilePath) {
495
+ const homeDir = os.homedir();
496
+ const projectsDir = path.join(homeDir, ".claude", "projects");
497
+ const cwd = process.cwd();
498
+ const projectDirName = path.basename(cwd);
499
+ transcriptFilePath = findMostRecentTranscript(projectsDir, projectDirName, cwd);
500
+ if (transcriptFilePath) {
501
+ console.error(`[session_close] Auto-detected transcript: ${transcriptFilePath}`);
502
+ }
503
+ else {
504
+ console.error(`[session_close] No transcript file found in ${projectsDir}`);
505
+ }
506
+ }
507
+ if (!transcriptFilePath)
508
+ return {};
509
+ const transcriptContent = fs.readFileSync(transcriptFilePath, "utf-8");
510
+ // Extract Claude Code session ID for traceability (sync, fast)
511
+ const claudeSessionId = extractClaudeSessionId(transcriptContent, transcriptFilePath) || undefined;
512
+ if (claudeSessionId) {
513
+ console.error(`[session_close] Extracted Claude session ID: ${claudeSessionId}`);
514
+ }
515
+ // Deterministic transcript save — await to guarantee persistence
516
+ const transcriptProject = isRetroactive ? "default" : existingSession?.project;
517
+ const saveResult = await saveTranscript({
518
+ session_id: sessionId,
519
+ transcript: transcriptContent,
520
+ format: "json",
521
+ project: transcriptProject,
522
+ });
523
+ if (saveResult.success && saveResult.transcript_path) {
524
+ const status = {
525
+ saved: true,
526
+ path: saveResult.transcript_path,
527
+ size_kb: saveResult.size_kb,
528
+ patch_warning: saveResult.patch_warning,
529
+ };
530
+ console.error(`[session_close] Transcript saved: ${saveResult.transcript_path} (${saveResult.size_kb}KB)`);
531
+ // OD-540: Process transcript for semantic search (fire-and-forget — chunking is expensive)
532
+ processTranscript(sessionId, transcriptContent, transcriptProject)
533
+ .then(result => {
534
+ if (result.success) {
535
+ console.error(`[session_close] Transcript chunking completed: ${result.chunksCreated} chunks created`);
536
+ }
537
+ else {
538
+ console.warn(`[session_close] Transcript chunking failed: ${result.error}`);
539
+ }
540
+ }).catch(err => {
541
+ console.error("[session_close] Transcript chunking error:", err);
542
+ });
543
+ return { status, claudeSessionId };
544
+ }
545
+ else {
546
+ console.warn(`[session_close] Failed to save transcript: ${saveResult.error}`);
547
+ return {
548
+ status: { saved: false, error: saveResult.error || "Unknown save error" },
549
+ claudeSessionId,
550
+ };
551
+ }
552
+ }
553
+ catch (error) {
554
+ const msg = error instanceof Error ? error.message : String(error);
555
+ console.error("[session_close] Exception during transcript capture:", msg);
556
+ return {
557
+ status: { saved: false, error: `Exception during transcript capture: ${msg}` },
558
+ };
559
+ }
560
+ }
561
+ /**
562
+ * Map confirmation decisions to scar_usage reference_type.
563
+ * APPLYING = explicit compliance, N_A = acknowledged but not applicable, REFUTED = overridden.
564
+ */
565
+ function decisionToRefType(decision) {
566
+ switch (decision) {
567
+ case "APPLYING": return "explicit";
568
+ case "N_A": return "acknowledged";
569
+ case "REFUTED": return "refuted";
570
+ default: return "acknowledged";
571
+ }
572
+ }
573
+ /**
574
+ * Auto-bridge Q6 answers (closing_reflection.scars_applied) to scar_usage records.
575
+ * Uses three-pass matching:
576
+ * 1. Structured confirmations from confirm_scars (preferred, includes variant_id)
577
+ * 2. Q6 text matching for scars without confirmations (fallback)
578
+ * 3. Unmatched surfaced scars recorded as "none"
579
+ * Returns empty array if no surfaced scars available.
580
+ */
581
+ function bridgeScarsToUsageRecords(normalizedScarsApplied, sessionId, agentIdentity) {
582
+ try {
583
+ // Load surfaced scars: prefer in-memory, fall back to per-session dir, then legacy file
584
+ let surfacedScars = getSurfacedScars();
585
+ if (surfacedScars.length === 0 && sessionId) {
586
+ // GIT-21: Try per-session directory first
587
+ try {
588
+ const sessionFilePath = getSessionPath(sessionId, "session.json");
589
+ if (fs.existsSync(sessionFilePath)) {
590
+ const fileData = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
591
+ if (fileData.surfaced_scars && Array.isArray(fileData.surfaced_scars)) {
592
+ surfacedScars = fileData.surfaced_scars;
593
+ console.error(`[session_close] Loaded ${surfacedScars.length} surfaced scars from per-session file`);
594
+ }
595
+ }
596
+ }
597
+ catch { /* per-session file read failed */ }
598
+ }
599
+ if (surfacedScars.length === 0) {
600
+ console.error("[session_close] No surfaced scars available for auto-bridge");
601
+ return [];
602
+ }
603
+ const autoBridgedScars = [];
604
+ const matchedScarIds = new Set();
605
+ // Load structured confirmations from confirm_scars (preferred source)
606
+ const confirmations = getConfirmations();
607
+ const confirmationMap = new Map();
608
+ for (const conf of confirmations) {
609
+ confirmationMap.set(conf.scar_id, conf);
610
+ }
611
+ // First pass: match surfaced scars against structured confirmations
612
+ for (const scar of surfacedScars) {
613
+ const confirmation = confirmationMap.get(scar.scar_id);
614
+ if (confirmation) {
615
+ matchedScarIds.add(scar.scar_id);
616
+ autoBridgedScars.push({
617
+ scar_identifier: scar.scar_id,
618
+ session_id: sessionId,
619
+ agent: agentIdentity,
620
+ surfaced_at: scar.surfaced_at,
621
+ reference_type: decisionToRefType(confirmation.decision),
622
+ reference_context: `Confirmed via confirm_scars: ${confirmation.decision} — ${confirmation.evidence.slice(0, 100)}`,
623
+ variant_id: scar.variant_id,
624
+ });
625
+ }
626
+ }
627
+ // Second pass: fallback to Q6 text matching for scars without confirmations
628
+ for (const scarApplied of normalizedScarsApplied) {
629
+ const lowerApplied = scarApplied.toLowerCase();
630
+ const match = surfacedScars.find((s) => {
631
+ if (matchedScarIds.has(s.scar_id))
632
+ return false;
633
+ return (s.scar_id === scarApplied ||
634
+ s.scar_title.toLowerCase().includes(lowerApplied) ||
635
+ lowerApplied.includes(s.scar_title.toLowerCase()));
636
+ });
637
+ if (match) {
638
+ matchedScarIds.add(match.scar_id);
639
+ autoBridgedScars.push({
640
+ scar_identifier: match.scar_id,
641
+ session_id: sessionId,
642
+ agent: agentIdentity,
643
+ surfaced_at: match.surfaced_at,
644
+ reference_type: "acknowledged",
645
+ reference_context: `Auto-bridged from Q6 answer: "${scarApplied}"`,
646
+ variant_id: match.variant_id,
647
+ });
648
+ }
649
+ }
650
+ // For surfaced scars NOT matched by either method, record as "none"
651
+ for (const scar of surfacedScars) {
652
+ if (!matchedScarIds.has(scar.scar_id)) {
653
+ autoBridgedScars.push({
654
+ scar_identifier: scar.scar_id,
655
+ session_id: sessionId,
656
+ agent: agentIdentity,
657
+ surfaced_at: scar.surfaced_at,
658
+ reference_type: "none",
659
+ reference_context: `Surfaced during ${scar.source} but not mentioned in closing reflection`,
660
+ variant_id: scar.variant_id,
661
+ });
662
+ }
663
+ }
664
+ if (autoBridgedScars.length > 0) {
665
+ console.error(`[session_close] Auto-bridged ${autoBridgedScars.length} scar usage records (${matchedScarIds.size} acknowledged, ${autoBridgedScars.length - matchedScarIds.size} unmentioned)`);
666
+ }
667
+ return autoBridgedScars;
668
+ }
669
+ catch (bridgeError) {
670
+ console.error("[session_close] Auto-bridge failed (non-fatal):", bridgeError);
671
+ return [];
672
+ }
673
+ }
299
674
  /**
300
675
  * Execute session_close tool
301
676
  */
302
677
  export async function sessionClose(params) {
303
678
  const timer = new Timer();
304
679
  const metricsId = uuidv4();
680
+ // OD-548: Validate session_id format before any DB calls
681
+ if (params.session_id && !isValidSessionId(params.session_id)) {
682
+ const latencyMs = timer.stop();
683
+ const perfData = buildPerformanceData("session_close", latencyMs, 0);
684
+ return {
685
+ success: false,
686
+ session_id: params.session_id,
687
+ close_compliance: {
688
+ close_type: params.close_type,
689
+ agent: "Unknown",
690
+ checklist_displayed: false,
691
+ questions_answered_by_agent: false,
692
+ human_asked_for_corrections: false,
693
+ learnings_stored: 0,
694
+ scars_applied: 0,
695
+ },
696
+ validation_errors: [
697
+ `Invalid session_id format: "${params.session_id}". Expected UUID (e.g., '393adb34-a80c-4c3a-b71a-bc0053b7a7ea') or short form (e.g., '393adb34'). Run session_start first.`,
698
+ ],
699
+ performance: perfData,
700
+ };
701
+ }
305
702
  // GIT-21: Recover session_id from active-sessions registry (hostname+PID) or legacy file
306
703
  if (!params.session_id && params.close_type !== "retroactive") {
307
704
  // Try registry first (GIT-20 writes here)
@@ -321,22 +718,38 @@ export async function sessionClose(params) {
321
718
  // merge it with inline params (inline params take precedence).
322
719
  // This keeps the visible MCP tool call small: just session_id + close_type.
323
720
  const payloadPath = getGitmemPath("closing-payload.json");
721
+ let payloadConsumed = false;
324
722
  try {
325
723
  if (fs.existsSync(payloadPath)) {
326
724
  const filePayload = JSON.parse(fs.readFileSync(payloadPath, "utf-8"));
327
725
  // File provides defaults; inline params override
328
726
  params = { ...filePayload, ...params };
727
+ payloadConsumed = true;
329
728
  console.error(`[session_close] Loaded closing payload from ${payloadPath}`);
330
- // Clean up payload file
331
- try {
332
- fs.unlinkSync(payloadPath);
333
- }
334
- catch { /* ignore */ }
729
+ // Payload file is cleaned up AFTER successful close (see end of function).
730
+ // If the tool crashes, the payload survives for retry.
335
731
  }
336
732
  }
337
733
  catch (error) {
338
734
  console.warn("[session_close] Failed to read closing-payload.json:", error);
339
735
  }
736
+ // Normalize closing_reflection field aliases (q1_broke → what_broke, etc.)
737
+ // Agents frequently guess field names instead of using canonical keys from CLOSING_QUESTIONS.
738
+ if (params.closing_reflection && typeof params.closing_reflection === "object") {
739
+ params.closing_reflection = normalizeReflectionKeys(params.closing_reflection);
740
+ }
741
+ // Normalize task_completion: if agent passed a string, wrap it in the expected object shape
742
+ if (params.task_completion && typeof params.task_completion === "string") {
743
+ const now = new Date().toISOString();
744
+ const fiveSecsAgo = new Date(Date.now() - 5000).toISOString();
745
+ params.task_completion = {
746
+ questions_displayed_at: fiveSecsAgo,
747
+ reflection_completed_at: fiveSecsAgo,
748
+ human_asked_at: fiveSecsAgo,
749
+ human_response_at: now,
750
+ human_response: "auto-normalized from string payload",
751
+ };
752
+ }
340
753
  // Close type auto-detection: reject mismatched close types based on session activity.
341
754
  // Standard close on a short/trivial session is wasteful; quick close on a long session loses data.
342
755
  // t-f7c2fa01: If closing_reflection is already present (agent answered 7 questions),
@@ -493,13 +906,52 @@ export async function sessionClose(params) {
493
906
  sessionId = uuidv4();
494
907
  }
495
908
  else {
496
- // Normal mode: require existing session (guaranteed to exist by step 0 above)
909
+ // Normal mode: require existing session
497
910
  sessionId = params.session_id;
911
+ // Try Supabase first
498
912
  try {
499
913
  existingSession = await supabase.getRecord("orchestra_sessions", sessionId);
500
914
  }
501
915
  catch {
502
- // Session might not exist yet, which is fine
916
+ // Supabase might not be configured (free tier) or session not found
917
+ }
918
+ // Fall back to local per-session file if Supabase didn't find it
919
+ if (!existingSession) {
920
+ try {
921
+ const localSessionPath = getSessionPath(sessionId, "session.json");
922
+ if (fs.existsSync(localSessionPath)) {
923
+ const localData = JSON.parse(fs.readFileSync(localSessionPath, "utf-8"));
924
+ existingSession = {
925
+ id: sessionId,
926
+ session_date: localData.started_at?.split("T")[0] || new Date().toISOString().split("T")[0],
927
+ agent: localData.agent,
928
+ project: localData.project,
929
+ ...localData,
930
+ };
931
+ console.error(`[session_close] Session found in local file (not in Supabase)`);
932
+ }
933
+ }
934
+ catch {
935
+ // Local file read failed
936
+ }
937
+ }
938
+ // Fall back to active-sessions registry as last resort
939
+ if (!existingSession) {
940
+ try {
941
+ const mySession = findSessionByHostPid(os.hostname(), process.pid);
942
+ if (mySession && mySession.session_id === sessionId) {
943
+ existingSession = {
944
+ id: sessionId,
945
+ session_date: mySession.started_at?.split("T")[0] || new Date().toISOString().split("T")[0],
946
+ agent: mySession.agent,
947
+ project: mySession.project,
948
+ };
949
+ console.error(`[session_close] Session found in registry (not in Supabase or local file)`);
950
+ }
951
+ }
952
+ catch {
953
+ // Registry lookup failed
954
+ }
503
955
  }
504
956
  if (!existingSession) {
505
957
  const latencyMs = timer.stop();
@@ -508,266 +960,58 @@ export async function sessionClose(params) {
508
960
  success: false,
509
961
  session_id: sessionId,
510
962
  close_compliance: closeCompliance,
511
- validation_errors: [`Session ${sessionId} not found. Was session_start called?`],
963
+ validation_errors: [`Session ${sessionId} not found in Supabase, local files, or registry. Was session_start called?`],
512
964
  performance: perfData,
513
965
  };
514
966
  }
515
967
  }
516
968
  // 5. Build session data (merge with existing or create from scratch)
517
- let sessionData;
518
- if (isRetroactive) {
519
- // Retroactive mode: create minimal session from scratch
520
- const now = new Date().toISOString();
521
- sessionData = {
522
- id: sessionId,
523
- agent: agentIdentity,
524
- project: "orchestra_dev", // Default for retroactive
525
- session_title: "Retroactive Session", // Will be updated below if we have content
526
- session_date: now,
527
- created_at: now,
528
- close_compliance: closeCompliance,
529
- };
530
- }
531
- else {
532
- // Normal mode: merge with existing session
533
- // Remove embedding from existing to avoid re-embedding unchanged text
534
- const { embedding: _embedding, ...existingWithoutEmbedding } = existingSession;
535
- sessionData = {
536
- ...existingWithoutEmbedding,
537
- close_compliance: closeCompliance,
538
- };
539
- }
540
- // Add closing reflection if provided
541
- if (params.closing_reflection) {
542
- const reflection = { ...params.closing_reflection };
543
- // Add human corrections to reflection if provided
544
- if (params.human_corrections) {
545
- reflection.human_additions = params.human_corrections;
546
- }
547
- sessionData.closing_reflection = reflection;
548
- }
549
- // Add decisions if provided
550
- if (params.decisions && params.decisions.length > 0) {
551
- sessionData.decisions = params.decisions.map((d) => d.title);
552
- }
553
- // OD-thread-lifecycle: Normalize and merge open threads
554
- const sessionThreads = getThreads(); // Mid-session thread state (may have resolutions)
555
- if (params.open_threads && params.open_threads.length > 0) {
556
- const normalized = normalizeThreads(params.open_threads, params.session_id);
557
- // Merge incoming with mid-session state (preserves resolutions from resolve_thread calls)
558
- const merged = sessionThreads.length > 0
559
- ? mergeThreadStates(normalized, sessionThreads)
560
- : normalized;
561
- sessionData.open_threads = merged;
562
- }
563
- else if (sessionThreads.length > 0) {
564
- // No new threads from close payload, but we have mid-session state (e.g., resolutions)
565
- sessionData.open_threads = sessionThreads;
566
- }
567
- // OD-534: If project_state provided, prepend it to open_threads as a ThreadObject
568
- if (params.project_state) {
569
- const projectStateText = `PROJECT STATE: ${params.project_state}`;
570
- const existing = (sessionData.open_threads || []);
571
- // Replace existing PROJECT STATE if present, otherwise prepend
572
- const filtered = existing.filter(t => {
573
- const text = typeof t === "string" ? t : t.text;
574
- return !text.startsWith("PROJECT STATE:");
575
- });
576
- sessionData.open_threads = [migrateStringThread(projectStateText, params.session_id), ...filtered];
577
- }
578
- // OD-624: Sync threads to Supabase (source of truth)
579
- // New threads get created, resolved threads get updated, existing threads get touched.
580
- // This runs async — does not block session close on failure.
969
+ const sessionData = buildSessionRecord(params, existingSession, isRetroactive, agentIdentity, closeCompliance, sessionId);
970
+ // OD-624: Sync threads to Supabase (fire-and-forget, non-blocking)
581
971
  const closeThreads = (sessionData.open_threads || []);
582
972
  if (closeThreads.length > 0) {
583
- const closeProject = isRetroactive ? "orchestra_dev" : existingSession?.project || "orchestra_dev";
973
+ const closeProject = isRetroactive ? "default" : existingSession?.project || "default";
584
974
  syncThreadsToSupabase(closeThreads, closeProject, sessionId).catch((err) => {
585
975
  console.error("[session_close] Thread Supabase sync failed (non-fatal):", err);
586
976
  });
587
977
  }
588
- // v2 Phase 2: Persist observations and children from multi-agent work
589
- const observations = getObservations();
590
- if (observations.length > 0) {
591
- sessionData.task_observations = observations;
592
- }
593
- const sessionChildren = getChildren();
594
- if (sessionChildren.length > 0) {
595
- sessionData.children = sessionChildren;
596
- }
597
- // Add linear issue if provided
598
- if (params.linear_issue) {
599
- sessionData.linear_issue = params.linear_issue;
978
+ // Prune threads.json: only keep open threads
979
+ try {
980
+ const openThreadsOnly = closeThreads.filter(t => t.status === "open" || !t.status);
981
+ saveThreadsFile(openThreadsOnly);
982
+ console.error(`[session_close] Pruned threads.json: ${openThreadsOnly.length} open threads (removed ${closeThreads.length - openThreadsOnly.length} resolved/archived)`);
600
983
  }
601
- // Update session title if we have meaningful content
602
- if (params.closing_reflection?.what_worked || params.decisions?.length) {
603
- const titleParts = [];
604
- if (params.linear_issue) {
605
- titleParts.push(params.linear_issue);
606
- }
607
- if (params.decisions?.length) {
608
- titleParts.push(params.decisions[0].title);
609
- }
610
- else if (params.closing_reflection?.what_worked) {
611
- // Use first 50 chars of what_worked as title hint
612
- titleParts.push(params.closing_reflection.what_worked.slice(0, 50));
613
- }
614
- if (titleParts.length > 0 &&
615
- (sessionData.session_title === "Interactive Session" ||
616
- sessionData.session_title === "Retroactive Session")) {
617
- sessionData.session_title = titleParts.join(" - ");
618
- }
984
+ catch (err) {
985
+ console.error("[session_close] Failed to prune threads.json (non-fatal):", err);
619
986
  }
620
987
  // OD-538: Capture transcript if enabled (default true for CLI/DAC)
988
+ let transcriptStatus;
621
989
  const shouldCaptureTranscript = params.capture_transcript !== false &&
622
990
  (agentIdentity === "CLI" || agentIdentity === "DAC");
623
991
  if (shouldCaptureTranscript) {
624
- try {
625
- let transcriptFilePath = null;
626
- // Option 1: Explicit transcript path provided (overrides auto-detection)
627
- if (params.transcript_path) {
628
- if (fs.existsSync(params.transcript_path)) {
629
- transcriptFilePath = params.transcript_path;
630
- console.error(`[session_close] Using explicit transcript path: ${transcriptFilePath}`);
631
- }
632
- else {
633
- console.warn(`[session_close] Explicit transcript path does not exist: ${params.transcript_path}`);
634
- }
635
- }
636
- // Option 2: Auto-detect by searching for most recent transcript
637
- if (!transcriptFilePath) {
638
- const homeDir = os.homedir();
639
- const projectsDir = path.join(homeDir, ".claude", "projects");
640
- const cwd = process.cwd();
641
- const projectDirName = path.basename(cwd);
642
- transcriptFilePath = findMostRecentTranscript(projectsDir, projectDirName, cwd);
643
- if (transcriptFilePath) {
644
- console.error(`[session_close] Auto-detected transcript: ${transcriptFilePath}`);
645
- }
646
- else {
647
- console.error(`[session_close] No transcript file found in ${projectsDir}`);
648
- }
649
- }
650
- // If we found a transcript, capture it
651
- if (transcriptFilePath) {
652
- const transcriptContent = fs.readFileSync(transcriptFilePath, "utf-8");
653
- // Extract Claude Code session ID for traceability
654
- const claudeSessionId = extractClaudeSessionId(transcriptContent, transcriptFilePath);
655
- if (claudeSessionId) {
656
- sessionData.claude_code_session_id = claudeSessionId;
657
- console.error(`[session_close] Extracted Claude session ID: ${claudeSessionId}`);
658
- }
659
- // Call save_transcript tool
660
- const saveResult = await saveTranscript({
661
- session_id: sessionId,
662
- transcript: transcriptContent,
663
- format: "json",
664
- project: isRetroactive ? "orchestra_dev" : existingSession?.project,
665
- });
666
- if (saveResult.success && saveResult.transcript_path) {
667
- sessionData.transcript_path = saveResult.transcript_path;
668
- console.error(`[session_close] Transcript saved: ${saveResult.transcript_path} (${saveResult.size_kb}KB)`);
669
- // OD-540: Process transcript for semantic search (async, don't block session close)
670
- processTranscript(sessionId, transcriptContent, isRetroactive ? "orchestra_dev" : existingSession?.project).then(result => {
671
- if (result.success) {
672
- console.error(`[session_close] Transcript chunking completed: ${result.chunksCreated} chunks created`);
673
- }
674
- else {
675
- console.warn(`[session_close] Transcript chunking failed: ${result.error}`);
676
- }
677
- }).catch(err => {
678
- console.error("[session_close] Transcript chunking error:", err);
679
- });
680
- }
681
- else {
682
- console.warn(`[session_close] Failed to save transcript: ${saveResult.error}`);
683
- }
684
- }
685
- }
686
- catch (error) {
687
- // Don't fail session close if transcript capture fails
688
- console.error("[session_close] Exception during transcript capture:", error);
992
+ const transcriptResult = await captureSessionTranscript(sessionId, params, existingSession, isRetroactive);
993
+ transcriptStatus = transcriptResult.status;
994
+ if (transcriptResult.claudeSessionId) {
995
+ sessionData.claude_code_session_id = transcriptResult.claudeSessionId;
689
996
  }
690
997
  }
691
- // OD-552: Auto-bridge Q6 answers (closing_reflection.scars_applied) to scar_usage records
692
- // This is the core fix: CLI/DAC sessions answer Q6 with scar names but these never
693
- // became structured scar_usage records. Now we match Q6 answers against surfaced scars.
998
+ // OD-552: Auto-bridge Q6 answers to scar_usage records
999
+ const normalizedScarsApplied = normalizeScarsApplied(params.closing_reflection?.scars_applied);
694
1000
  if ((!params.scars_to_record || params.scars_to_record.length === 0) &&
695
- params.closing_reflection?.scars_applied?.length) {
696
- try {
697
- // Load surfaced scars: prefer in-memory, fall back to per-session dir, then legacy file
698
- let surfacedScars = getSurfacedScars();
699
- if (surfacedScars.length === 0 && params.session_id) {
700
- // GIT-21: Try per-session directory first
701
- try {
702
- const sessionFilePath = getSessionPath(params.session_id, "session.json");
703
- if (fs.existsSync(sessionFilePath)) {
704
- const sessionData = JSON.parse(fs.readFileSync(sessionFilePath, "utf-8"));
705
- if (sessionData.surfaced_scars && Array.isArray(sessionData.surfaced_scars)) {
706
- surfacedScars = sessionData.surfaced_scars;
707
- console.error(`[session_close] Loaded ${surfacedScars.length} surfaced scars from per-session file`);
708
- }
709
- }
710
- }
711
- catch { /* per-session file read failed */ }
712
- }
713
- if (surfacedScars.length > 0) {
714
- const autoBridgedScars = [];
715
- const matchedScarIds = new Set();
716
- // For each Q6 answer, try to match against surfaced scars
717
- for (const scarApplied of params.closing_reflection.scars_applied) {
718
- const lowerApplied = scarApplied.toLowerCase();
719
- // Match by UUID or title substring
720
- const match = surfacedScars.find((s) => {
721
- if (matchedScarIds.has(s.scar_id))
722
- return false; // Don't double-match
723
- return (s.scar_id === scarApplied || // Exact UUID match
724
- s.scar_title.toLowerCase().includes(lowerApplied) || // Title contains answer
725
- lowerApplied.includes(s.scar_title.toLowerCase()) // Answer contains title
726
- );
727
- });
728
- if (match) {
729
- matchedScarIds.add(match.scar_id);
730
- autoBridgedScars.push({
731
- scar_identifier: match.scar_id,
732
- session_id: sessionId,
733
- agent: agentIdentity,
734
- surfaced_at: match.surfaced_at,
735
- reference_type: "acknowledged",
736
- reference_context: `Auto-bridged from Q6 answer: "${scarApplied}"`,
737
- });
738
- }
739
- }
740
- // For surfaced scars NOT mentioned in Q6, record as "none" (surfaced but ignored)
741
- for (const scar of surfacedScars) {
742
- if (!matchedScarIds.has(scar.scar_id)) {
743
- autoBridgedScars.push({
744
- scar_identifier: scar.scar_id,
745
- session_id: sessionId,
746
- agent: agentIdentity,
747
- surfaced_at: scar.surfaced_at,
748
- reference_type: "none",
749
- reference_context: `Surfaced during ${scar.source} but not mentioned in closing reflection`,
750
- });
751
- }
752
- }
753
- if (autoBridgedScars.length > 0) {
754
- params = { ...params, scars_to_record: autoBridgedScars };
755
- console.error(`[session_close] Auto-bridged ${autoBridgedScars.length} scar usage records (${matchedScarIds.size} acknowledged, ${autoBridgedScars.length - matchedScarIds.size} unmentioned)`);
756
- }
757
- }
758
- else {
759
- console.error("[session_close] No surfaced scars available for auto-bridge");
760
- }
761
- }
762
- catch (bridgeError) {
763
- console.error("[session_close] Auto-bridge failed (non-fatal):", bridgeError);
1001
+ normalizedScarsApplied.length > 0) {
1002
+ const bridgedScars = bridgeScarsToUsageRecords(normalizedScarsApplied, sessionId, agentIdentity);
1003
+ if (bridgedScars.length > 0) {
1004
+ params = { ...params, scars_to_record: bridgedScars };
764
1005
  }
765
1006
  }
766
1007
  // 6. Persist to Supabase (direct REST API, bypasses ww-mcp)
767
1008
  try {
768
- // Generate embedding for session data
1009
+ // OD-646: Upsert session WITHOUT embedding (fast path)
1010
+ // Embedding + thread detection run fire-and-forget after
1011
+ await supabase.directUpsert("orchestra_sessions", sessionData);
1012
+ // OD-646: Tracked fire-and-forget embedding generation + session update + thread detection
769
1013
  if (isEmbeddingAvailable()) {
770
- try {
1014
+ getEffectTracker().track("embedding", "session_close", async () => {
771
1015
  const embeddingParts = [
772
1016
  sessionData.session_title || "",
773
1017
  params.closing_reflection?.what_worked || "",
@@ -778,51 +1022,39 @@ export async function sessionClose(params) {
778
1022
  if (embeddingText.length > 10) {
779
1023
  const embeddingVector = await embed(embeddingText);
780
1024
  if (embeddingVector) {
781
- sessionData.embedding = JSON.stringify(embeddingVector);
782
- }
783
- }
784
- }
785
- catch (embError) {
786
- console.warn("[session_close] Embedding generation failed (non-fatal):", embError);
787
- }
788
- }
789
- await supabase.directUpsert("orchestra_sessions", sessionData);
790
- // Phase 5: Implicit thread detection (fire-and-forget)
791
- if (sessionData.embedding) {
792
- (async () => {
793
- try {
794
- const sessionEmb = JSON.parse(sessionData.embedding);
795
- const suggestProject = existingSession?.project || "orchestra_dev";
796
- const recentSessions = await loadRecentSessionEmbeddings(suggestProject, 30, 20);
797
- const threadEmbs = await loadOpenThreadEmbeddings(suggestProject);
798
- if (recentSessions && threadEmbs) {
799
- const existing = loadSuggestions();
800
- const updated = detectSuggestedThreads({ session_id: sessionId, title: sessionData.session_title, embedding: sessionEmb }, recentSessions, threadEmbs, existing);
801
- saveSuggestions(updated);
1025
+ const embeddingJson = JSON.stringify(embeddingVector);
1026
+ // Update session with embedding (PATCH, not upsert — row already exists)
1027
+ await supabase.directPatch("orchestra_sessions", { id: sessionId }, { embedding: embeddingJson });
1028
+ console.error("[session_close] Embedding saved to session");
1029
+ // Phase 5: Implicit thread detection (chained after embedding)
1030
+ const suggestProject = existingSession?.project || "default";
1031
+ const recentSessions = await loadRecentSessionEmbeddings(suggestProject, 30, 20);
1032
+ const threadEmbs = await loadOpenThreadEmbeddings(suggestProject);
1033
+ if (recentSessions && threadEmbs) {
1034
+ const existing = loadSuggestions();
1035
+ const updated = detectSuggestedThreads({ session_id: sessionId, title: sessionData.session_title, embedding: embeddingVector }, recentSessions, threadEmbs, existing);
1036
+ saveSuggestions(updated);
1037
+ }
802
1038
  }
803
1039
  }
804
- catch (err) {
805
- console.error("[session_close] Thread suggestion detection failed (non-fatal):", err);
806
- }
807
- })();
1040
+ });
808
1041
  }
809
- // 7. Record scar usage if provided (parallel with metrics)
1042
+ // OD-646: Tracked fire-and-forget scar usage recording (was blocking ~200-500ms)
810
1043
  // OD-552: scars_to_record may now come from auto-bridge above
811
- let scarRecordingResults;
812
1044
  if (params.scars_to_record && params.scars_to_record.length > 0) {
813
1045
  const project = isRetroactive
814
- ? "orchestra_dev"
1046
+ ? "default"
815
1047
  : existingSession.project;
816
- scarRecordingResults = await recordScarUsageBatch({
1048
+ getEffectTracker().track("scar_usage", "session_close_batch", () => recordScarUsageBatch({
817
1049
  scars: params.scars_to_record,
818
1050
  project,
819
- });
1051
+ }));
820
1052
  }
821
1053
  const latencyMs = timer.stop();
822
1054
  const perfData = buildPerformanceData("session_close", latencyMs, 1);
823
1055
  // Update relevance data for memories applied during session
824
- if (params.closing_reflection?.scars_applied?.length) {
825
- updateRelevanceData(sessionId, params.closing_reflection.scars_applied).catch(() => { });
1056
+ if (normalizedScarsApplied.length > 0) {
1057
+ updateRelevanceData(sessionId, normalizedScarsApplied).catch(() => { });
826
1058
  }
827
1059
  // Record metrics
828
1060
  recordMetrics({
@@ -842,8 +1074,6 @@ export async function sessionClose(params) {
842
1074
  decisions_count: params.decisions?.length || 0,
843
1075
  open_threads_count: params.open_threads?.length || 0,
844
1076
  ceremony_duration_ms: params.ceremony_duration_ms,
845
- scars_recorded_batch: scarRecordingResults?.resolved_count || 0,
846
- scars_failed_batch: scarRecordingResults?.failed_count || 0,
847
1077
  retroactive: isRetroactive,
848
1078
  },
849
1079
  }).catch(() => { });
@@ -851,7 +1081,14 @@ export async function sessionClose(params) {
851
1081
  clearCurrentSession();
852
1082
  // GIT-21: Clean up session files (registry, per-session dir, legacy file)
853
1083
  cleanupSessionFiles(sessionId);
854
- const display = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, true, validation.warnings.length > 0 ? validation.warnings : undefined);
1084
+ // Clean up payload file AFTER successful close (not before crash safety)
1085
+ if (payloadConsumed) {
1086
+ try {
1087
+ fs.unlinkSync(payloadPath);
1088
+ }
1089
+ catch { /* already gone */ }
1090
+ }
1091
+ const display = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, true, validation.warnings.length > 0 ? validation.warnings : undefined, transcriptStatus);
855
1092
  return {
856
1093
  success: true,
857
1094
  session_id: sessionId,
@@ -867,7 +1104,7 @@ export async function sessionClose(params) {
867
1104
  const perfData = buildPerformanceData("session_close", latencyMs, 0);
868
1105
  // OD-547: Clear session state even on error (session is done either way)
869
1106
  clearCurrentSession();
870
- const errorDisplay = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, false, [`Failed to persist session: ${errorMessage}`]);
1107
+ const errorDisplay = formatCloseDisplay(sessionId, closeCompliance, params, learningsCount, false, [`Failed to persist session: ${errorMessage}`], transcriptStatus);
871
1108
  return {
872
1109
  success: false,
873
1110
  session_id: sessionId,