gitmem-mcp 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md.template +63 -55
- package/README.md +79 -163
- package/bin/gitmem.js +233 -109
- package/bin/init-wizard.js +642 -0
- package/bin/uninstall.js +288 -0
- package/dist/commands/check.js +20 -20
- package/dist/commands/check.js.map +1 -1
- package/dist/constants/closing-questions.d.ts +6 -0
- package/dist/constants/closing-questions.d.ts.map +1 -1
- package/dist/constants/closing-questions.js +65 -0
- package/dist/constants/closing-questions.js.map +1 -1
- package/dist/hooks/format-utils.d.ts +52 -0
- package/dist/hooks/format-utils.d.ts.map +1 -0
- package/dist/hooks/format-utils.js +89 -0
- package/dist/hooks/format-utils.js.map +1 -0
- package/dist/hooks/quick-retrieve.d.ts +30 -0
- package/dist/hooks/quick-retrieve.d.ts.map +1 -0
- package/dist/hooks/quick-retrieve.js +149 -0
- package/dist/hooks/quick-retrieve.js.map +1 -0
- package/dist/schemas/active-sessions.d.ts +8 -8
- package/dist/schemas/analyze.d.ts +3 -3
- package/dist/schemas/common.d.ts +2 -2
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/schemas/common.js +1 -1
- package/dist/schemas/common.js.map +1 -1
- package/dist/schemas/create-decision.d.ts +3 -3
- package/dist/schemas/create-learning.d.ts +13 -13
- package/dist/schemas/log.d.ts +3 -3
- package/dist/schemas/prepare-context.d.ts +3 -3
- package/dist/schemas/recall.d.ts +3 -3
- package/dist/schemas/record-scar-usage-batch.d.ts +8 -3
- package/dist/schemas/record-scar-usage-batch.d.ts.map +1 -1
- package/dist/schemas/record-scar-usage.d.ts +3 -0
- package/dist/schemas/record-scar-usage.d.ts.map +1 -1
- package/dist/schemas/record-scar-usage.js +1 -0
- package/dist/schemas/record-scar-usage.js.map +1 -1
- package/dist/schemas/registry.d.ts +18 -0
- package/dist/schemas/registry.d.ts.map +1 -0
- package/dist/schemas/registry.js +158 -0
- package/dist/schemas/registry.js.map +1 -0
- package/dist/schemas/save-transcript.d.ts +3 -3
- package/dist/schemas/search-transcripts.d.ts +33 -0
- package/dist/schemas/search-transcripts.d.ts.map +1 -0
- package/dist/schemas/search-transcripts.js +26 -0
- package/dist/schemas/search-transcripts.js.map +1 -0
- package/dist/schemas/search.d.ts +3 -3
- package/dist/schemas/session-close.d.ts +43 -15
- package/dist/schemas/session-close.d.ts.map +1 -1
- package/dist/schemas/session-close.js +7 -2
- package/dist/schemas/session-close.js.map +1 -1
- package/dist/schemas/session-start.d.ts +3 -3
- package/dist/schemas/thread.d.ts +3 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +82 -28
- package/dist/server.js.map +1 -1
- package/dist/services/active-sessions.d.ts +2 -1
- package/dist/services/active-sessions.d.ts.map +1 -1
- package/dist/services/active-sessions.js +130 -84
- package/dist/services/active-sessions.js.map +1 -1
- package/dist/services/analytics.d.ts.map +1 -1
- package/dist/services/analytics.js +1 -0
- package/dist/services/analytics.js.map +1 -1
- package/dist/services/behavioral-decay.d.ts +40 -0
- package/dist/services/behavioral-decay.d.ts.map +1 -0
- package/dist/services/behavioral-decay.js +110 -0
- package/dist/services/behavioral-decay.js.map +1 -0
- package/dist/services/bm25.d.ts +39 -0
- package/dist/services/bm25.d.ts.map +1 -0
- package/dist/services/bm25.js +132 -0
- package/dist/services/bm25.js.map +1 -0
- package/dist/services/cache.d.ts.map +1 -1
- package/dist/services/cache.js +9 -8
- package/dist/services/cache.js.map +1 -1
- package/dist/services/cache.test.js +17 -17
- package/dist/services/cache.test.js.map +1 -1
- package/dist/services/compliance-validator.d.ts.map +1 -1
- package/dist/services/compliance-validator.js +12 -1
- package/dist/services/compliance-validator.js.map +1 -1
- package/dist/services/display-protocol.d.ts +31 -0
- package/dist/services/display-protocol.d.ts.map +1 -0
- package/dist/services/display-protocol.js +73 -0
- package/dist/services/display-protocol.js.map +1 -0
- package/dist/services/effect-tracker.d.ts +81 -0
- package/dist/services/effect-tracker.d.ts.map +1 -0
- package/dist/services/effect-tracker.js +181 -0
- package/dist/services/effect-tracker.js.map +1 -0
- package/dist/services/file-lock.d.ts +31 -0
- package/dist/services/file-lock.d.ts.map +1 -0
- package/dist/services/file-lock.js +124 -0
- package/dist/services/file-lock.js.map +1 -0
- package/dist/services/gitmem-dir.d.ts +7 -0
- package/dist/services/gitmem-dir.d.ts.map +1 -1
- package/dist/services/gitmem-dir.js +21 -0
- package/dist/services/gitmem-dir.js.map +1 -1
- package/dist/services/local-file-storage.d.ts +3 -2
- package/dist/services/local-file-storage.d.ts.map +1 -1
- package/dist/services/local-file-storage.js +30 -43
- package/dist/services/local-file-storage.js.map +1 -1
- package/dist/services/local-vector-search.d.ts +10 -9
- package/dist/services/local-vector-search.d.ts.map +1 -1
- package/dist/services/local-vector-search.js +28 -23
- package/dist/services/local-vector-search.js.map +1 -1
- package/dist/services/metrics.d.ts +7 -2
- package/dist/services/metrics.d.ts.map +1 -1
- package/dist/services/metrics.js +41 -33
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/session-state.d.ts +8 -0
- package/dist/services/session-state.d.ts.map +1 -1
- package/dist/services/session-state.js +9 -2
- package/dist/services/session-state.js.map +1 -1
- package/dist/services/startup.d.ts +12 -13
- package/dist/services/startup.d.ts.map +1 -1
- package/dist/services/startup.js +104 -57
- package/dist/services/startup.js.map +1 -1
- package/dist/services/supabase-client.d.ts +2 -1
- package/dist/services/supabase-client.d.ts.map +1 -1
- package/dist/services/supabase-client.js +22 -16
- package/dist/services/supabase-client.js.map +1 -1
- package/dist/services/thread-dedup.d.ts +9 -0
- package/dist/services/thread-dedup.d.ts.map +1 -1
- package/dist/services/thread-dedup.js +27 -0
- package/dist/services/thread-dedup.js.map +1 -1
- package/dist/services/thread-manager.d.ts.map +1 -1
- package/dist/services/thread-manager.js +38 -16
- package/dist/services/thread-manager.js.map +1 -1
- package/dist/services/thread-suggestions.d.ts.map +1 -1
- package/dist/services/thread-suggestions.js +1 -1
- package/dist/services/thread-suggestions.js.map +1 -1
- package/dist/services/thread-supabase.d.ts +0 -1
- package/dist/services/thread-supabase.d.ts.map +1 -1
- package/dist/services/thread-supabase.js +83 -54
- package/dist/services/thread-supabase.js.map +1 -1
- package/dist/services/timezone.d.ts.map +1 -1
- package/dist/services/timezone.js +1 -0
- package/dist/services/timezone.js.map +1 -1
- package/dist/services/transcript-chunker.d.ts.map +1 -1
- package/dist/services/transcript-chunker.js +18 -4
- package/dist/services/transcript-chunker.js.map +1 -1
- package/dist/services/variant-generation.d.ts +41 -0
- package/dist/services/variant-generation.d.ts.map +1 -0
- package/dist/services/variant-generation.js +263 -0
- package/dist/services/variant-generation.js.map +1 -0
- package/dist/tools/absorb-observations.d.ts.map +1 -1
- package/dist/tools/absorb-observations.js +9 -0
- package/dist/tools/absorb-observations.js.map +1 -1
- package/dist/tools/analyze.d.ts.map +1 -1
- package/dist/tools/analyze.js +13 -2
- package/dist/tools/analyze.js.map +1 -1
- package/dist/tools/archive-learning.d.ts +28 -0
- package/dist/tools/archive-learning.d.ts.map +1 -0
- package/dist/tools/archive-learning.js +81 -0
- package/dist/tools/archive-learning.js.map +1 -0
- package/dist/tools/cleanup-threads.d.ts +1 -0
- package/dist/tools/cleanup-threads.d.ts.map +1 -1
- package/dist/tools/cleanup-threads.js +111 -18
- package/dist/tools/cleanup-threads.js.map +1 -1
- package/dist/tools/confirm-scars.d.ts.map +1 -1
- package/dist/tools/confirm-scars.js +8 -2
- package/dist/tools/confirm-scars.js.map +1 -1
- package/dist/tools/create-decision.d.ts.map +1 -1
- package/dist/tools/create-decision.js +11 -8
- package/dist/tools/create-decision.js.map +1 -1
- package/dist/tools/create-learning.d.ts.map +1 -1
- package/dist/tools/create-learning.js +35 -11
- package/dist/tools/create-learning.js.map +1 -1
- package/dist/tools/create-thread.d.ts +2 -1
- package/dist/tools/create-thread.d.ts.map +1 -1
- package/dist/tools/create-thread.js +9 -4
- package/dist/tools/create-thread.js.map +1 -1
- package/dist/tools/definitions.d.ts +785 -34
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +239 -95
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/dismiss-suggestion.d.ts +1 -0
- package/dist/tools/dismiss-suggestion.d.ts.map +1 -1
- package/dist/tools/dismiss-suggestion.js +4 -0
- package/dist/tools/dismiss-suggestion.js.map +1 -1
- package/dist/tools/graph-traverse.d.ts +1 -0
- package/dist/tools/graph-traverse.d.ts.map +1 -1
- package/dist/tools/graph-traverse.js +24 -9
- package/dist/tools/graph-traverse.js.map +1 -1
- package/dist/tools/list-threads.d.ts.map +1 -1
- package/dist/tools/list-threads.js +49 -5
- package/dist/tools/list-threads.js.map +1 -1
- package/dist/tools/log.d.ts +1 -0
- package/dist/tools/log.d.ts.map +1 -1
- package/dist/tools/log.js +84 -17
- package/dist/tools/log.js.map +1 -1
- package/dist/tools/prepare-context.d.ts +1 -0
- package/dist/tools/prepare-context.d.ts.map +1 -1
- package/dist/tools/prepare-context.js +15 -85
- package/dist/tools/prepare-context.js.map +1 -1
- package/dist/tools/promote-suggestion.d.ts +1 -0
- package/dist/tools/promote-suggestion.d.ts.map +1 -1
- package/dist/tools/promote-suggestion.js +5 -0
- package/dist/tools/promote-suggestion.js.map +1 -1
- package/dist/tools/recall.d.ts +2 -0
- package/dist/tools/recall.d.ts.map +1 -1
- package/dist/tools/recall.js +43 -10
- package/dist/tools/recall.js.map +1 -1
- package/dist/tools/recall.test.js +6 -6
- package/dist/tools/recall.test.js.map +1 -1
- package/dist/tools/record-scar-usage-batch.d.ts.map +1 -1
- package/dist/tools/record-scar-usage-batch.js +13 -0
- package/dist/tools/record-scar-usage-batch.js.map +1 -1
- package/dist/tools/record-scar-usage.d.ts.map +1 -1
- package/dist/tools/record-scar-usage.js +6 -0
- package/dist/tools/record-scar-usage.js.map +1 -1
- package/dist/tools/resolve-thread.d.ts.map +1 -1
- package/dist/tools/resolve-thread.js +57 -6
- package/dist/tools/resolve-thread.js.map +1 -1
- package/dist/tools/save-transcript.d.ts +1 -0
- package/dist/tools/save-transcript.d.ts.map +1 -1
- package/dist/tools/save-transcript.js +3 -1
- package/dist/tools/save-transcript.js.map +1 -1
- package/dist/tools/search-transcripts.d.ts +44 -0
- package/dist/tools/search-transcripts.d.ts.map +1 -0
- package/dist/tools/search-transcripts.js +158 -0
- package/dist/tools/search-transcripts.js.map +1 -0
- package/dist/tools/search.d.ts +1 -0
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +74 -3
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/session-close.d.ts.map +1 -1
- package/dist/tools/session-close.js +563 -326
- package/dist/tools/session-close.js.map +1 -1
- package/dist/tools/session-start.d.ts +10 -6
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +319 -426
- package/dist/tools/session-start.js.map +1 -1
- package/dist/types/index.d.ts +37 -4
- package/dist/types/index.d.ts.map +1 -1
- package/hooks/hooks/hooks.json +8 -37
- package/hooks/scripts/auto-retrieve-hook.sh +163 -0
- package/hooks/scripts/post-tool-use.sh +0 -16
- package/hooks/scripts/recall-check.sh +0 -11
- package/hooks/scripts/session-close-check.sh +1 -1
- package/hooks/scripts/session-start.sh +89 -13
- package/hooks/tests/test-hooks.sh +3 -49
- package/package.json +3 -2
- package/schema/setup.sql +1 -1
|
@@ -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
|
|
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
|
-
|
|
230
|
-
lines.push(
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
256
|
+
const ok = "\u2713";
|
|
257
|
+
const no = "\u2717";
|
|
235
258
|
if (compliance.close_type === "standard") {
|
|
236
|
-
lines.push(
|
|
237
|
-
lines.push(
|
|
238
|
-
lines.push(
|
|
239
|
-
lines.push(
|
|
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(
|
|
245
|
-
|
|
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(
|
|
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
|
-
//
|
|
261
|
-
|
|
324
|
+
// Write health — only surface when there are failures (dev diagnostic)
|
|
325
|
+
const healthReport = getEffectTracker().getHealthReport();
|
|
326
|
+
if (healthReport.overall.failed > 0) {
|
|
262
327
|
lines.push("");
|
|
263
|
-
lines.push(
|
|
264
|
-
lines.push(
|
|
328
|
+
lines.push(`${B}Write Health${R} (${healthReport.overall.failed} failure${healthReport.overall.failed > 1 ? "s" : ""})`);
|
|
329
|
+
lines.push(getEffectTracker().formatSummary());
|
|
265
330
|
}
|
|
266
|
-
//
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
//
|
|
331
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
518
|
-
|
|
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 ? "
|
|
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
|
-
//
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
|
692
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
805
|
-
console.error("[session_close] Thread suggestion detection failed (non-fatal):", err);
|
|
806
|
-
}
|
|
807
|
-
})();
|
|
1040
|
+
});
|
|
808
1041
|
}
|
|
809
|
-
//
|
|
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
|
-
? "
|
|
1046
|
+
? "default"
|
|
815
1047
|
: existingSession.project;
|
|
816
|
-
|
|
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 (
|
|
825
|
-
updateRelevanceData(sessionId,
|
|
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
|
-
|
|
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,
|