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