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